glimmer 0.9.0 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.0
1
+ 0.9.5
@@ -5,6 +5,10 @@
5
5
  require 'logger'
6
6
  require 'set'
7
7
 
8
+ $LOAD_PATH.unshift(File.expand_path('..', __FILE__))
9
+
10
+ require 'glimmer/config'
11
+
8
12
  # Glimmer provides a JRuby Desktop UI DSL + Data-Binding functionality
9
13
  #
10
14
  # A desktop UI application class must include Glimmer to gain access to Glimmer DSL
@@ -14,14 +18,45 @@ require 'set'
14
18
  module Glimmer
15
19
  #TODO make it configurable to include or not include perhaps reverting to using included
16
20
  REGEX_METHODS_EXCLUDED = /^(to_|\[)/
21
+
22
+ # TODO add loop detection support to avoid infinite loops (perhaps breaks after 3 repetitions and provides an option to allow it if intentional)
23
+ class << self
24
+ attr_accessor :loop_last_data
25
+
26
+ def loop_reset!(including_loop_last_data = false)
27
+ @loop_last_data = nil if including_loop_last_data
28
+ @loop = 1
29
+ end
30
+
31
+ def loop
32
+ @loop ||= loop_reset!
33
+ end
34
+
35
+ def loop_increment!
36
+ @loop = loop + 1
37
+ end
38
+ end
17
39
 
18
40
  def method_missing(method_symbol, *args, &block)
41
+ new_loop_data = [method_symbol, args, block]
42
+ if new_loop_data == Glimmer.loop_last_data
43
+ Glimmer.loop_increment!
44
+ if Glimmer.loop == Config.loop_max_count
45
+ raise "Glimmer looped #{Config.loop_max_count} times with keyword '#{new_loop_data[0]}'! Check code for errors."
46
+ end
47
+ else
48
+ Glimmer.loop_reset!
49
+ end
50
+ Glimmer.loop_last_data = new_loop_data
19
51
  # This if statement speeds up Glimmer in girb or whenever directly including on main object
20
52
  if method_symbol.to_s.match(REGEX_METHODS_EXCLUDED)
21
- raise InvalidKeywordError, "Glimmer excluded keyword: #{method_symbol}"
53
+ raise ExcludedKeywordError, "Glimmer excluded keyword: #{method_symbol}"
22
54
  end
23
55
  Glimmer::Config.logger&.debug "Interpreting keyword: #{method_symbol}"
24
56
  Glimmer::DSL::Engine.interpret(method_symbol, *args, &block)
57
+ rescue ExcludedKeywordError => e
58
+ # TODO add a feature to show excluded keywords optionally for debugging purposes
59
+ super(method_symbol, *args, &block)
25
60
  rescue InvalidKeywordError => e
26
61
  if !method_symbol.to_s.match(REGEX_METHODS_EXCLUDED)
27
62
  Glimmer::Config.logger&.error e.message
@@ -31,8 +66,7 @@ module Glimmer
31
66
  end
32
67
  end
33
68
 
34
- $LOAD_PATH.unshift(File.expand_path('..', __FILE__))
35
-
36
- require 'glimmer/config'
37
69
  require 'glimmer/error'
70
+ require 'glimmer/excluded_keyword_error'
38
71
  require 'glimmer/invalid_keyword_error'
72
+ require 'glimmer/dsl/engine'
@@ -1,6 +1,14 @@
1
1
  module Glimmer
2
2
  module Config
3
3
  class << self
4
+ LOOP_MAX_COUNT_DEFAULT = 100
5
+
6
+ attr_writer :loop_max_count
7
+
8
+ def loop_max_count
9
+ @loop_max_count ||= LOOP_MAX_COUNT_DEFAULT
10
+ end
11
+
4
12
  # Returns Glimmer logger (standard Ruby logger)
5
13
  def logger
6
14
  # unless defined? @@logger
@@ -0,0 +1,248 @@
1
+ require 'glimmer/data_binding/observable'
2
+ require 'glimmer/data_binding/observer'
3
+
4
+ module Glimmer
5
+ module DataBinding
6
+ class ModelBinding
7
+ include Observable
8
+ include Observer
9
+
10
+ attr_reader :binding_options
11
+
12
+ def initialize(base_model, property_name_expression, binding_options = nil)
13
+ @base_model = base_model
14
+ @property_name_expression = property_name_expression
15
+ @binding_options = binding_options || {}
16
+ if computed?
17
+ @computed_model_bindings = computed_by.map do |computed_by_property_expression|
18
+ self.class.new(base_model, computed_by_property_expression)
19
+ end
20
+ end
21
+ end
22
+
23
+ def model
24
+ nested_property? ? nested_model : base_model
25
+ end
26
+
27
+ # e.g. person.address.state returns [person, person.address]
28
+ def nested_models
29
+ @nested_models = [base_model]
30
+ model_property_names.reduce(base_model) do |reduced_model, nested_model_property_name|
31
+ if reduced_model.nil?
32
+ nil
33
+ else
34
+ invoke_property_reader(reduced_model, nested_model_property_name).tap do |new_reduced_model|
35
+ @nested_models << new_reduced_model
36
+ end
37
+ end
38
+ end
39
+ @nested_models
40
+ end
41
+
42
+ def nested_model
43
+ nested_models.last
44
+ end
45
+
46
+ def base_model
47
+ @base_model
48
+ end
49
+
50
+ def property_name
51
+ nested_property? ? nested_property_name : property_name_expression
52
+ end
53
+
54
+ def convert_on_read(value)
55
+ apply_converter(@binding_options[:on_read], value)
56
+ end
57
+
58
+ def convert_on_write(value)
59
+ apply_converter(@binding_options[:on_write], value)
60
+ end
61
+
62
+ def apply_converter(converter, value)
63
+ if converter.nil?
64
+ value
65
+ elsif converter.is_a?(String) || converter.is_a?(Symbol)
66
+ if value.respond_to?(converter)
67
+ value.send(converter)
68
+ else
69
+ raise Glimmer::Error, "Unsupported bind converter: #{converter.inspect}"
70
+ end
71
+ elsif converter.respond_to?(:call, value)
72
+ converter.call(value)
73
+ else
74
+ raise Glimmer::Error, "Unsupported bind converter: #{converter.inspect}"
75
+ end
76
+ end
77
+
78
+ # All nested property names
79
+ # e.g. property name expression "address.state" gives ['address', 'state']
80
+ # If there are any indexed property names, this returns indexes as properties.
81
+ # e.g. property name expression "addresses[1].state" gives ['addresses', '[1]', 'state']
82
+ def nested_property_names
83
+ @nested_property_names ||= property_name_expression.split(".").map {|pne| pne.match(/([^\[]+)(\[[^\]]+\])?/).to_a.drop(1)}.flatten.compact
84
+ end
85
+
86
+ # Final nested property name
87
+ # e.g. property name expression "address.state" gives :state
88
+ def nested_property_name
89
+ nested_property_names.last
90
+ end
91
+
92
+ # Model representing nested property names
93
+ # e.g. property name expression "address.state" gives [:address]
94
+ def model_property_names
95
+ nested_property_names[0...-1]
96
+ end
97
+
98
+ def nested_property?
99
+ property_name_expression.match(/[.\[]/)
100
+ end
101
+
102
+ def property_name_expression
103
+ @property_name_expression
104
+ end
105
+
106
+ def computed?
107
+ !computed_by.empty?
108
+ end
109
+
110
+ def computed_by
111
+ [@binding_options[:computed_by]].flatten.compact
112
+ end
113
+
114
+ def nested_property_observers_for(observer)
115
+ @nested_property_observers_collection ||= {}
116
+ unless @nested_property_observers_collection.has_key?(observer)
117
+ @nested_property_observers_collection[observer] = nested_property_names.reduce({}) do |output, property_name|
118
+ output.merge(
119
+ property_name => Observer.proc do |new_value|
120
+ # Ensure reattaching observers when a higher level nested property is updated (e.g. person.address changes reattaches person.address.street observer)
121
+ add_observer(observer)
122
+ observer.call(evaluate_property)
123
+ end
124
+ )
125
+ end
126
+ end
127
+ @nested_property_observers_collection[observer]
128
+ end
129
+
130
+ def add_observer(observer)
131
+ if computed?
132
+ add_computed_observers(observer)
133
+ elsif nested_property?
134
+ add_nested_observers(observer)
135
+ else
136
+ model_binding_observer = Observer.proc do |new_value|
137
+ observer.call(evaluate_property)
138
+ end
139
+ observer_registration = model_binding_observer.observe(model, property_name)
140
+ my_registration = observer.registration_for(self)
141
+ observer.add_dependent(my_registration => observer_registration)
142
+ end
143
+ end
144
+
145
+ def remove_observer(observer)
146
+ if computed?
147
+ @computed_model_bindings.each do |computed_model_binding|
148
+ computed_observer_for(observer).unobserve(computed_model_binding)
149
+ end
150
+ @computed_observer_collection.delete(observer)
151
+ elsif nested_property?
152
+ nested_property_observers_for(observer).clear
153
+ else
154
+ observer.unobserve(model, property_name)
155
+ end
156
+ end
157
+
158
+ def computed_observer_for(observer)
159
+ @computed_observer_collection ||= {}
160
+ unless @computed_observer_collection.has_key?(observer)
161
+ @computed_observer_collection[observer] = Observer.proc do |new_value|
162
+ observer.call(evaluate_property)
163
+ end
164
+ end
165
+ @computed_observer_collection[observer]
166
+ end
167
+
168
+ def add_computed_observers(observer)
169
+ @computed_model_bindings.each do |computed_model_binding|
170
+ observer_registration = computed_observer_for(observer).observe(computed_model_binding)
171
+ my_registration = observer.registration_for(self)
172
+ observer.add_dependent(my_registration => observer_registration)
173
+ end
174
+ end
175
+
176
+ def add_nested_observers(observer)
177
+ nested_property_observers = nested_property_observers_for(observer)
178
+ nested_models.zip(nested_property_names).each_with_index do |zip, i|
179
+ model, property_name = zip
180
+ nested_property_observer = nested_property_observers[property_name]
181
+ previous_index = i - 1
182
+ parent_model = previous_index.negative? ? self : nested_models[previous_index]
183
+ parent_property_name = previous_index.negative? ? nil : nested_property_names[previous_index]
184
+ parent_observer = previous_index.negative? ? observer : nested_property_observers[parent_property_name]
185
+ parent_property_name = nil if parent_property_name.to_s.start_with?('[')
186
+ unless model.nil?
187
+ if property_indexed?(property_name)
188
+ # TODO figure out a way to deal with this more uniformly
189
+ observer_registration = nested_property_observer.observe(model)
190
+ else
191
+ observer_registration = nested_property_observer.observe(model, property_name)
192
+ end
193
+ parent_registration = parent_observer.registration_for(parent_model, parent_property_name)
194
+ parent_observer.add_dependent(parent_registration => observer_registration)
195
+ end
196
+ end
197
+ end
198
+
199
+ def call(value)
200
+ return if model.nil?
201
+ converted_value = value
202
+ invoke_property_writer(model, "#{property_name}=", converted_value) unless evaluate_property == converted_value
203
+ end
204
+
205
+ def evaluate_property
206
+ value = nil
207
+ value = invoke_property_reader(model, property_name) unless model.nil?
208
+ convert_on_read(value)
209
+ end
210
+
211
+ def evaluate_options_property
212
+ model.send(options_property_name) unless model.nil?
213
+ end
214
+
215
+ def options_property_name
216
+ self.property_name + "_options"
217
+ end
218
+
219
+ def property_indexed?(property_expression)
220
+ property_expression.to_s.start_with?('[')
221
+ end
222
+
223
+ def invoke_property_reader(object, property_expression)
224
+ if property_indexed?(property_expression)
225
+ property_method = '[]'
226
+ property_argument = property_expression[1...-1]
227
+ property_argument = property_argument.to_i if property_argument.match(/\d+/)
228
+ object.send(property_method, property_argument)
229
+ else
230
+ object.send(property_expression)
231
+ end
232
+ end
233
+
234
+ def invoke_property_writer(object, property_expression, value)
235
+ return if @binding_options[:read_only]
236
+ value = convert_on_write(value)
237
+ if property_indexed?(property_expression)
238
+ property_method = '[]='
239
+ property_argument = property_expression[1...-2]
240
+ property_argument = property_argument.to_i if property_argument.match(/\d+/)
241
+ object.send(property_method, property_argument, value)
242
+ else
243
+ object.send(property_expression, value)
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,21 @@
1
+ require 'glimmer/error'
2
+
3
+ module Glimmer
4
+ module DataBinding
5
+ module Observable
6
+ # TODO rename methods to observe/unobserve
7
+ def add_observer(observer, property_or_properties=nil)
8
+ raise Error, 'Not implemented!'
9
+ end
10
+
11
+ def remove_observer(observer, property_or_properties=nil)
12
+ raise Error, 'Not implemented!'
13
+ end
14
+
15
+ # Overriding inspect to avoid printing very long observer hierarchies
16
+ def inspect
17
+ "#<#{self.class.name}:0x#{self.hash.to_s(16)}>"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,86 @@
1
+ require 'set'
2
+
3
+ require 'glimmer/data_binding/observable'
4
+
5
+ module Glimmer
6
+ module DataBinding
7
+ # TODO prefix utility methods with double-underscore
8
+ module ObservableArray
9
+ include Observable
10
+
11
+ def add_observer(observer, element_properties=nil)
12
+ return observer if has_observer?(observer) && element_properties.nil?
13
+ property_observer_list << observer
14
+ [element_properties].flatten.compact.each do |property|
15
+ each do |element|
16
+ observer.observe(element, property)
17
+ end
18
+ end
19
+ observer
20
+ end
21
+
22
+ def remove_observer(observer, element_properties=nil)
23
+ property_observer_list.delete(observer)
24
+ [element_properties].flatten.compact.each do |property|
25
+ each do |element|
26
+ observer.unobserve(element, property)
27
+ end
28
+ end
29
+ observer
30
+ end
31
+
32
+ def has_observer?(observer)
33
+ property_observer_list.include?(observer)
34
+ end
35
+
36
+ def property_observer_list
37
+ @property_observer_list ||= Set.new
38
+ end
39
+
40
+ def notify_observers
41
+ property_observer_list.each {|observer| observer.call}
42
+ end
43
+
44
+ def <<(element)
45
+ super(element)
46
+ notify_observers
47
+ end
48
+
49
+ def []=(index, value)
50
+ old_value = self[index]
51
+ unregister_dependent_observers(old_value)
52
+ super(index, value)
53
+ notify_observers
54
+ end
55
+
56
+ def delete(element)
57
+ unregister_dependent_observers(element)
58
+ super(element)
59
+ notify_observers
60
+ end
61
+
62
+ def delete_at(index)
63
+ old_value = self[index]
64
+ unregister_dependent_observers(old_value)
65
+ super(index)
66
+ notify_observers
67
+ end
68
+
69
+ def clear
70
+ each do |old_value|
71
+ unregister_dependent_observers(old_value)
72
+ end
73
+ super()
74
+ notify_observers
75
+ end
76
+
77
+ def unregister_dependent_observers(old_value)
78
+ # TODO look into optimizing this
79
+ return unless old_value.is_a?(ObservableModel) || old_value.is_a?(ObservableArray)
80
+ property_observer_list.each do |observer|
81
+ observer.unregister_dependents_with_observable(observer.registration_for(self), old_value)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,104 @@
1
+ require 'glimmer/data_binding/observable'
2
+ require 'glimmer/data_binding/observer'
3
+
4
+ module Glimmer
5
+ module DataBinding
6
+ # TODO prefix utility methods with double-underscore
7
+ module ObservableModel
8
+ include Observable
9
+
10
+ class Notifier
11
+ include Observer
12
+ def initialize(observable_model, property_name)
13
+ @observable_model = observable_model
14
+ @property_name = property_name
15
+ end
16
+ def call(new_value=nil)
17
+ @observable_model.notify_observers(@property_name)
18
+ end
19
+ end
20
+
21
+ def add_observer(observer, property_name)
22
+ return observer if has_observer?(observer, property_name)
23
+ property_observer_list(property_name) << observer
24
+ add_property_writer_observers(property_name)
25
+ observer
26
+ end
27
+
28
+ def remove_observer(observer, property_name)
29
+ property_observer_list(property_name).delete(observer)
30
+ end
31
+
32
+ def has_observer?(observer, property_name)
33
+ property_observer_list(property_name).include?(observer)
34
+ end
35
+
36
+ def has_observer_for_any_property?(observer)
37
+ property_observer_hash.values.map(&:to_a).sum.include?(observer)
38
+ end
39
+
40
+ def property_observer_hash
41
+ @property_observers ||= Hash.new
42
+ end
43
+
44
+ def property_observer_list(property_name)
45
+ property_observer_hash[property_name.to_sym] = Set.new unless property_observer_hash[property_name.to_sym]
46
+ property_observer_hash[property_name.to_sym]
47
+ end
48
+
49
+ def notify_observers(property_name)
50
+ property_observer_list(property_name).each {|observer| observer.call(send(property_name))}
51
+ end
52
+ #TODO upon updating values, make sure dependent observers are cleared (not added as dependents here)
53
+
54
+ def add_property_writer_observers(property_name)
55
+ property_writer_name = "#{property_name}="
56
+ method(property_writer_name)
57
+ ensure_array_object_observer(property_name, send(property_name))
58
+ begin
59
+ singleton_method("__original_#{property_writer_name}")
60
+ rescue
61
+ old_method = self.class.instance_method(property_writer_name) rescue self.method(property_writer_name)
62
+ define_singleton_method("__original_#{property_writer_name}", old_method)
63
+ define_singleton_method(property_writer_name) do |value|
64
+ old_value = self.send(property_name)
65
+ unregister_dependent_observers(property_name, old_value)
66
+ self.send("__original_#{property_writer_name}", value)
67
+ notify_observers(property_name)
68
+ ensure_array_object_observer(property_name, value, old_value)
69
+ end
70
+ end
71
+ rescue => e
72
+ #ignore writing if no property writer exists
73
+ Glimmer::Config.logger&.debug "No need to observe property writer: #{property_writer_name}\n#{e.message}\n#{e.backtrace.join("\n")}"
74
+ end
75
+
76
+ def unregister_dependent_observers(property_name, old_value)
77
+ # TODO look into optimizing this
78
+ return unless old_value.is_a?(ObservableModel) || old_value.is_a?(ObservableArray)
79
+ property_observer_list(property_name).each do |observer|
80
+ observer.unregister_dependents_with_observable(observer.registration_for(self, property_name), old_value)
81
+ end
82
+ end
83
+
84
+ def ensure_array_object_observer(property_name, object, old_object = nil)
85
+ return unless object.is_a?(Array)
86
+ array_object_observer = array_object_observer_for(property_name)
87
+ array_observer_registration = array_object_observer.observe(object)
88
+ property_observer_list(property_name).each do |observer|
89
+ my_registration = observer.registration_for(self, property_name) # TODO eliminate repetition
90
+ observer.add_dependent(my_registration => array_observer_registration)
91
+ end
92
+ array_object_observer_for(property_name).unregister(old_object) if old_object.is_a?(ObservableArray)
93
+ end
94
+
95
+ def array_object_observer_for(property_name)
96
+ @array_object_observers ||= {}
97
+ unless @array_object_observers.has_key?(property_name)
98
+ @array_object_observers[property_name] = ObservableModel::Notifier.new(self, property_name)
99
+ end
100
+ @array_object_observers[property_name]
101
+ end
102
+ end
103
+ end
104
+ end