glimmer 0.9.1 → 0.10.0

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.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