glimmer 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.1
1
+ 0.10.0
@@ -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,26 +18,55 @@ 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
- Glimmer::Config.logger&.debug "Interpreting keyword: #{method_symbol}"
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
- Glimmer::Config.logger&.error e.message
62
+ Glimmer::Config.logger.error {e.message}
28
63
  end
29
- Glimmer::Config.logger&.debug "#{e.message}\n#{e.backtrace.join("\n")}"
64
+ Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
30
65
  super(method_symbol, *args, &block)
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'
39
72
  require 'glimmer/dsl/engine'
@@ -1,22 +1,33 @@
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
- # unless defined? @@logger
7
- # @@logger = Logger.new(STDOUT).tap {|logger| logger.level = Logger::WARN}
8
- # end
9
- @@logger if defined? @@logger
14
+ reset_logger! unless defined? @@logger
15
+ @@logger
16
+ end
17
+
18
+ def logger=(custom_logger)
19
+ @@logger = custom_logger
10
20
  end
11
21
 
12
- def enable_logging
13
- @@logger = Logger.new(STDOUT).tap {|logger| logger.level = Logger::WARN}
22
+ def reset_logger!
23
+ self.logger = Logger.new(STDOUT).tap do |logger|
24
+ logger.level = Logger::ERROR
25
+ end
14
26
  end
15
27
  end
16
28
  end
17
29
  end
18
30
 
19
31
  if ENV['GLIMMER_LOGGER_LEVEL']
20
- Glimmer::Config.enable_logging
21
32
  Glimmer::Config.logger.level = ENV['GLIMMER_LOGGER_LEVEL'].downcase
22
33
  end
@@ -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