ducktape 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
- Ducktape
2
- ========
1
+ # ducktape
3
2
 
4
- A [truly outrageous](http://youtu.be/dSPb56-_I98) gem for bindable attributes.
3
+ A [truly outrageous](http://youtu.be/dSPb56-_I98) gem for bindable attributes and event notification.
5
4
 
6
5
  To install:
7
6
 
@@ -9,472 +8,6 @@ To install:
9
8
  gem install ducktape
10
9
  ```
11
10
 
12
- Bindable attributes
13
- -------------------
11
+ Additional documentation can be found in the [Wiki](https://github.com/SilverPhoenix99/ducktape/wiki).
14
12
 
15
- Bindable attributes (BA) work just like normal attributes. To assign a BA to a class you just need to declare it like an attr_accessor:
16
-
17
- ```ruby
18
- require 'ducktape'
19
-
20
- class X
21
- include Ducktape::Bindable
22
-
23
- bindable :name
24
-
25
- def initialize(name)
26
- self.name = name
27
- end
28
- end
29
- ```
30
-
31
- ### Binding
32
-
33
- BA's, like the name hints, can be bound to other BA's:
34
-
35
- ```ruby
36
- class X
37
- include Ducktape::Bindable
38
-
39
- bindable :name
40
- end
41
-
42
- class Y
43
- include Ducktape::Bindable
44
-
45
- bindable :other_name
46
- end
47
-
48
- x = X.new
49
-
50
- y = Y.new
51
- y.other_name = Ducktape::BindingSource.new(x, :name)
52
-
53
- x.name = 'Richard'
54
-
55
- puts y.other_name
56
- ```
57
-
58
- Output:
59
- ```ruby
60
- => "Richard"
61
- ```
62
-
63
- There are three types of direction for `BindingSource`:
64
- * `:both` - (default) Changes apply in both directions.
65
- * `:forward` - Changes only apply from the binding source BA to the destination BA.
66
- * `:reverse` - Changes only apply from the destination BA to the binding source BA.
67
-
68
- Here's an example of `:reverse` binding (very similar to the previous):
69
-
70
- ```ruby
71
- class X
72
- include Ducktape::Bindable
73
-
74
- bindable :name
75
- end
76
-
77
- class Y
78
- include Ducktape::Bindable
79
-
80
- bindable :other_name
81
- end
82
-
83
- x = X.new
84
-
85
- y = Y.new
86
- y.other_name = Ducktape::BindingSource.new(x, :name, :reverse)
87
-
88
- y.other_name = 'Mary'
89
-
90
- puts x.name
91
- puts y.other_name
92
- ```
93
-
94
- Output:
95
- ```ruby
96
- => "Mary"
97
- => "Mary"
98
- ```
99
-
100
- But if you then do:
101
-
102
- ```ruby
103
- x.name = 'John'
104
-
105
- puts x.name
106
- puts y.other_name
107
- ```
108
-
109
- the output will be:
110
- ```ruby
111
- => "John"
112
- => "Mary"
113
- ```
114
-
115
- ### Removing bindings
116
-
117
- To remove a previously defined binding call the `#unbind_source(attr_name)` method. To remove all bindings for a given object, call the `#clear_bindings` method.
118
-
119
- ### Read only / Write only
120
-
121
- BA's can be read-only and write-only. Read-only BA's are useful for reverse only bindings or when the object itself changes the value through the protected method `#set_value`. On the other hand, write-only BA's are best used as sources, though the owner object can call the `#get_value` to get the value of the BA.
122
-
123
- To define a BA as read-only or write-only, use the `:access` modifier.
124
-
125
- Here's an example of read-only and write-only BA's:
126
-
127
- ```ruby
128
- class X
129
- include Ducktape::Bindable
130
-
131
- bindable :name, access: :readonly
132
-
133
- #no need for this now
134
- #
135
- #def initialize(name = 'John')
136
- # self.name = name
137
- #end
138
- end
139
-
140
- class Y
141
- include Ducktape::Bindable
142
-
143
- bindable :other_name, access: :writeonly
144
- end
145
-
146
- x = X.new
147
- y = Y.new
148
-
149
- y.other_name = Ducktape::BindingSource.new(x, :name, :reverse)
150
- y.other_name = 'Alex'
151
-
152
- puts x.name
153
- ```
154
-
155
- Output:
156
- ```ruby
157
- => "Alex"
158
- ```
159
-
160
- ### Default values
161
-
162
- You can set default values for your BA:
163
-
164
- ```ruby
165
- class X
166
- include Ducktape::Bindable
167
-
168
- bindable :name, default: 'John'
169
-
170
- #we don't need to do this now:
171
- #
172
- #def initialize(name = 'John')
173
- # self.name = name
174
- #end
175
- end
176
- ```
177
-
178
- ### Validation
179
-
180
- You can do validation on a BA.
181
-
182
- Following the last example we could validate `:name` as a String or a Symbol:
183
-
184
- ```ruby
185
- class X
186
- include Ducktape::Bindable
187
-
188
- bindable :name, validate: [String, Symbol]
189
-
190
- def initialize(name)
191
- self.name = name
192
- end
193
- end
194
- ```
195
-
196
- Validation works with procs as well. In this case, a proc must have a single parameter which is the new value.
197
-
198
- ```ruby
199
- class X
200
- include Ducktape::Bindable
201
-
202
- bindable :name, validate: ->(value){ !value.nil? }
203
-
204
- def initialize(name)
205
- self.name = name
206
- end
207
- end
208
- ```
209
-
210
- Additionally, it has built-in support for regular expressions:
211
-
212
- ```ruby
213
- class X
214
- include Ducktape::Bindable
215
-
216
- bindable :name, validate: /ruby/
217
-
218
- def initialize(name)
219
- self.name = name
220
- end
221
- end
222
-
223
- #passes
224
- X.new('rubygems')
225
-
226
- #fails
227
- begin
228
- X.new('diamonds')
229
- rescue => e
230
- puts e.message
231
- end
232
- ```
233
-
234
- Validation also works with any kind of objects. For example, to make an enumerable:
235
-
236
- ```ruby
237
- class X
238
- include Ducktape::Bindable
239
-
240
- # place attribute will only accept assignment to these three symbols:
241
- bindable :place, default: :first, validate: [:first, :middle, :last]
242
- end
243
- ```
244
-
245
- In short, you can have a single object or a proc, or an array of objects/procs to validate. If any of them returns "true" (i.e., not `nil` and not `false`), then the new value is accepted. Otherwise it will throw an `InvalidAttributeValueError`.
246
-
247
- ### Coercion
248
-
249
- While validation can help knowing when things aren't what we expect, sometimes what we really want is to force a value to remain in a domain. This is where coercion comes in.
250
-
251
- For example, we would like for a float value to remain between 0 and 1, inclusively. If the value goes out of scope, then we want to clamp it to remain between 0 and 1. Additionally, we want other numerical types to be valid, and converted to floats.
252
-
253
- ```ruby
254
- class X
255
- include Ducktape::Bindable
256
-
257
- bindable :my_float,
258
- validate: Numeric,
259
- default: 0.0,
260
- coerce: ->(owner, value) { value = value.to_f; value < 0.0 ? 0.0 : (value > 1.0 ? 1.0 : value) }
261
- end
262
-
263
- x = X.new
264
- x.my_float = 2.0
265
-
266
- puts x.my_float
267
- ```
268
-
269
- The output would be:
270
- ```ruby
271
- => 1.0
272
- ```
273
-
274
- Note that if validation is defined, then it will only happen after coercion is applied.
275
-
276
- ### Hookable change notifications
277
-
278
- You can watch for changes in a BA by using the public instance method `#on_changed(attr_name, &block)`. Here's an example:
279
-
280
- ```ruby
281
- def attribute_changed(event, owner, attr_name, new_value, old_value)
282
- puts "#{owner.class}<#{owner.object_id.to_s(16)}> called the event #{event.inspect} and changed the attribute #{attr_name.inspect} from #{old_value.inspect} to #{new_value.inspect}"
283
- end
284
-
285
- class X
286
- include Ducktape::Bindable
287
-
288
- bindable :name, validate: [String, Symbol]
289
- bindable :age, validate: Integer
290
- bindable :points, validate: Integer
291
-
292
- def initialize(name, age, points)
293
- self.name = name
294
- self.age = age
295
- self.points = points
296
-
297
- # You can hook for any method available
298
- %w'name age points'.each { |k, v| on_changed k, &method(:attribute_changed) }
299
- end
300
- end
301
-
302
- # oops, a misspelling...
303
- x = X.new('Richad', 23, 150)
304
-
305
- # It's also useful to see changes outside of the class:
306
- x.on_changed 'name', &->(_, _, _, _, new_value) { puts "Hello #{new_value}!" }
307
-
308
- x.name = 'Richard'
309
- ```
310
-
311
- After calling `#name=`, the output should be something like:
312
-
313
- ```ruby
314
- => "Hello Richard!"
315
- => "X<14e35b4> called the event \"on_changed\" and changed the attribute \"name\" from \"Richad\" to \"Richard\""
316
- ```
317
-
318
- The `on_changed` hook has the following arguments:
319
- * the name of the event (in this case, `'on_changed'`)
320
- * the caller/owner of the BA (the instance that sent the message),
321
- * the name of the BA (`name`, `age`, `points`, etc...),
322
- * the new value,
323
- * the old value
324
-
325
- Hooks
326
- -----
327
-
328
- Has you might have seen, Ducktape comes with hooks, which is what powers the `on_changed` for bindable attributes.
329
- You can easily define a hook by using `def_hook`:
330
-
331
- ```ruby
332
- def called_load(event, owner)
333
- puts "#{owner.class}<#{owner.object_id.to_s(16)}> called #{event.inspect}"
334
- end
335
-
336
- class X
337
- include Ducktape::Hookable
338
-
339
- def_hook :on_loaded #define one or more hooks
340
-
341
- def load
342
- # do other stuff here
343
-
344
- call_hooks(:on_loaded)
345
- end
346
- end
347
-
348
- x = X.new
349
-
350
- x.on_loaded method(:called_load)
351
-
352
- #if we didn't create a hook with def_hook we could still use:
353
- #x.add_hook :on_loaded, method(:called_load)
354
-
355
- x.load
356
- ```
357
-
358
- The output should be something like:
359
- ```ruby
360
- => "X<14e35b4> called \"on_loaded\""
361
- ```
362
-
363
- ### Named hooks
364
-
365
- It is possible to define a hook by providing a method name. This makes it possible to separate logic from definition.
366
- Note that the method must exist for the instance that calls the hooks (through `call_hooks` or `call_handlers`).
367
-
368
- ```ruby
369
- #class logic file
370
-
371
- class Y
372
- def called_load(event, owner)
373
- puts "loaded #{self}" #self == owner
374
- end
375
-
376
- def load
377
- #do some stuff here
378
-
379
- call_hooks(:on_loaded)
380
- end
381
- end
382
- ```
383
-
384
- ```ruby
385
- #instance definition file
386
-
387
- x = Y.tap do |y|
388
- y.on_loaded :called_load
389
- end
390
-
391
- x.load
392
- ```
393
-
394
- The output should be something like:
395
- ```ruby
396
- => "loaded Y<3539af>"
397
- ```
398
-
399
- This also allows to dynamically bind the hook by overriding the method.
400
-
401
- ```ruby
402
- #taking the same instance from before
403
-
404
- x.define_singleton_method(:called_load) { puts "singleton #{self} has loaded" }
405
- ```
406
-
407
- The output should now be:
408
- ```ruby
409
- => "singleton Y<3539af> has loaded"
410
- ```
411
-
412
- ### Removing hooks
413
-
414
- To remove all hooks from an object call the `#clear_hooks` method. To remove all hooks from a single event, pass the name of the event as a parameter. The next section has an example of this.
415
-
416
- To remove a single hook from an event, call the `#remove_hook` with the name of the event, and the hook name or hook proc corresponding with how the hook was added.
417
-
418
- ### Handlers
419
-
420
- ### Hookable arrays and hashes
421
-
422
- A `Ducktape::HookableArray` is a wrapper for arrays that allows you to add hooks to modifiers of the array. The same concept applies to `Ducktape::HookableHash` in relation to hashes.
423
- To add a hook to a specific modifier you just have to pass a block to a method that has the same name as the modifier, prefixed with `on_`.
424
-
425
- There are three exceptions to the naming convention:
426
- * `HookableArray#<<`: pass the hook through the `on_append` method.
427
- * `HookableArray#[]=`: pass the hook through the `on_store` method.
428
- * `HookableHash#[]=`: pass the hook through the `on_store` method.
429
-
430
- The parameters for all the hooks are very similar to the ones used for bindables:
431
- * the name of the event (for example, `'on_store'`)
432
- * the instance of `HookableArray` or `HookableHash` that triggered the hook
433
- * an array of the arguments that were passed to the method that triggered the hook (for example, the index and value of the `[]=` method)
434
- * and the result of the call to the method
435
-
436
- Additionally, there is a generic `on_changed` hook, that is called for every modifier. In this case, the parametes are:
437
- * the name of the event (for example, `'on_store'`)
438
- * the instance of `HookableArray` or `HookableHash` that triggered the hook
439
- * the name of the method (`"[]="`, `"<<"`, "sort!", etc...) that triggered the hook
440
- * an array of the arguments that were passed to the method that triggered the hook (for example, the index and value of the `[]=` method)
441
- * and the result of the call to the method
442
-
443
- Here is an example that shows how to use the hooks on a `HookableArray`:
444
-
445
- ```ruby
446
- a = Ducktape::HookableArray[1, :x] #same as new()
447
- a.on_append do |event, owner, args, result|
448
- puts "#{event.inspect}, #{owner.class}<#{owner.object_id.to_s(16)}>, #{args.inspect}, #{result.inspect}"
449
- end
450
-
451
- a.on_changed do |event, owner, name, args, result|
452
- puts "#{event.inspect}, #{owner.class}<#{owner.object_id.to_s(16)}>, #{name.inspect}, #{args.inspect}, #{result.inspect}"
453
- end
454
-
455
- a << 'hi'
456
- ```
457
-
458
- The output would be something like:
459
- ```ruby
460
- => "\"on_append\", Ducktape::HookableArray<37347c>, [\"hi\"], [1, :x, \"hi\"]"
461
- => "\"on_changed\", Ducktape::HookableArray<37347c>, \"<<\", [\"hi\"], [1, :x, \"hi\"]"
462
- ```
463
-
464
- If you then do:
465
- ```ruby
466
- a.clear_hooks('on_append')
467
-
468
- a << 'bye'
469
- ```
470
-
471
- The output will only be for the `on_changed` hook, which wasn't removed:
472
- ```ruby
473
- => "\"on_changed\", Ducktape::HookableArray<37347c>, \"<<\", [\"bye\"], [1, :x, \"hi\", \"bye\"]"
474
- ```
475
-
476
- Future work
477
- ===========
478
- * Multi-sourced BA's.
479
- * More complex binding source paths instead of just the member name (e.g.: ruby like 'a.b.c' or xml like 'a/b/c').
480
- * Add built-in support for hookable arrays and hashes in bindable attributes.
13
+ <font color=red>**WARNING**: Version 0.3.0 is incompatible with version 0.2.1 and below.</font>
@@ -41,38 +41,40 @@ module Ducktape
41
41
  raise 'Cannot extend, only include.'
42
42
  end
43
43
 
44
- def unbind_source(name)
45
- get_bindable_attr(name.to_s).remove_source(true)
44
+ def unbind_source(attr_name)
45
+ get_bindable_attr(attr_name).remove_source
46
46
  nil
47
47
  end
48
48
 
49
49
  def clear_bindings()
50
- bindable_attrs.each { |_,attr| attr.remove_source() }
50
+ bindable_attrs.each { |_,attr| attr.remove_source }
51
51
  nil
52
52
  end
53
53
 
54
54
  def on_changed(attr_name, hook = nil, &block)
55
55
  return nil unless block || hook
56
- get_bindable_attr(attr_name.to_s).on_changed(hook, &block)
56
+ get_bindable_attr(attr_name).on_changed(hook, &block)
57
57
  block
58
58
  end
59
59
 
60
60
  def unhook_on_changed(attr_name, block)
61
61
  return nil unless block
62
- get_bindable_attr(attr_name.to_s).send(:remove_hook, :on_changed, block)
62
+ get_bindable_attr(attr_name).remove_hook(:on_changed, block)
63
63
  block
64
64
  end
65
65
 
66
- protected
66
+ protected #--------------------------------------------------------------
67
+
67
68
  def get_value(attr_name)
68
- get_bindable_attr(attr_name.to_s).value
69
+ get_bindable_attr(attr_name).value
69
70
  end
70
71
 
71
72
  def set_value(attr_name, value)
72
- get_bindable_attr(attr_name.to_s).value = value
73
+ get_bindable_attr(attr_name).value = value
73
74
  end
74
75
 
75
- private
76
+ private #----------------------------------------------------------------
77
+
76
78
  def bindable_attrs
77
79
  @bindable_attrs ||= {}
78
80
  end
@@ -12,58 +12,63 @@ module Ducktape
12
12
 
13
13
  include Hookable
14
14
 
15
- attr_reader :owner, # Bindable
16
- :name, # String
17
- :source, # BindingSource
18
- #:targets, # { BindableAttribute => BindingSource }
19
- :value # Object
15
+ attr_reader :owner # Bindable
16
+ attr_reader :name # String
17
+ attr_reader :source # BindingSource
18
+ attr_reader :value # Object
20
19
 
21
- def_hook :on_changed
20
+ #attr_reader :targets # Hash{ BindableAttribute => BindingSource }
21
+
22
+ #def_hook :on_changed
22
23
 
23
24
  def initialize(owner, name)
24
- @owner, @name, @targets, @source = owner, name.to_s, {}, nil
25
- reset_value(false)
25
+ @owner, @name, @source, @targets = owner, name.to_s, nil, {}
26
+ reset_value
26
27
  end
27
28
 
28
29
  def metadata
29
30
  @owner.class.metadata(@name)
30
31
  end
31
32
 
33
+ def has_source?
34
+ !@source.nil?
35
+ end
36
+
32
37
  def value=(value)
33
38
  set_value(value)
34
39
  end
35
40
 
36
- def remove_source(propagate = true)
37
- detach(@source, self, propagate)
41
+ #If values are equal, it means that both attributes share the same value,
42
+ #if they are different, the values are independent.
43
+ #As such, self's value is reset if mode is both, or if mode is forward and values are equal;
44
+ #the source's value is reset of mode is reverse and values are equal.
45
+ def remove_source
46
+ return unless has_source?
47
+ bs, v = detach_source
48
+ reset_value if bs.mode == :both || (bs.mode == :forward && v == @value)
49
+ bs
38
50
  end
39
51
 
40
- def reset_value(propagate = true)
41
- meta = metadata
42
- value = @source ? @source.source.value : meta.default
43
-
44
- if propagate
45
- self.value = value
46
- else
47
- @value = value
48
- end
52
+ def reset_value
53
+ set_value(metadata.default)
54
+ end
49
55
 
50
- nil
56
+ def to_s
57
+ "#<#{self.class}:0x#{object_id.to_s(16)} @name=#{name}>"
51
58
  end
52
59
 
53
- protected #--------------------------------------------------------------
60
+ private #----------------------------------------------------------------
61
+
62
+ attr_reader :targets
54
63
 
55
64
  def set_value(value, exclusions = Set.new)
56
- return if exclusions.member? self
65
+ return if exclusions.include? self
57
66
  exclusions << self
58
67
 
59
68
  if value.is_a? BindingSource
60
- BindableAttribute.attach(value, self, false)
61
-
62
- #update value
63
- exclusions << @source.source
64
-
65
- #new value is the new source value
66
- value = @source.source.value
69
+ attach_source(value) #attach new binding source
70
+ exclusions << @source.source #update value
71
+ value = @source.source.value #new value is the new source value
67
72
  end
68
73
 
69
74
  #set effective value
@@ -73,43 +78,48 @@ module Ducktape
73
78
  raise InvalidAttributeValueError.new(@name, value) unless m.validate(value)
74
79
  old_value = @value
75
80
  @value = value
76
- call_hooks('on_changed', owner, name, @value, old_value)
77
- end
78
81
 
79
- #propagate value
80
- @source.source.set_value(value, exclusions) if propagate_to_source
81
- targets_to_propagate.each { |target, _| target.set_value(value, exclusions) }
82
- end
82
+ old_value.remove_hook('on_changed', method('hookable_value_changed')) if old_value.respond_to?('on_changed')
83
+ @value.on_changed(method('hookable_value_changed')) if @value.respond_to?('on_changed')
83
84
 
84
- private #----------------------------------------------------------------
85
+ call_hooks('on_changed', owner, attribute: name.dup, value: @value, old_value: old_value)
86
+ end
85
87
 
86
- def propagate_to_source
87
- return false unless @source
88
- BindingSource::PROPAGATE_TO_SOURCE.member? @source.mode
88
+ propagate_value(exclusions)
89
+ @value
89
90
  end
90
91
 
91
- def targets_to_propagate
92
- @targets.select { |_, b| BindingSource::PROPAGATE_TO_TARGETS.member? b.mode }
92
+ def detach_source
93
+ return unless @source
94
+ bs = @source
95
+ v = bs.source.value
96
+ @source = nil
97
+ bs.source.send(:targets).delete(self)
98
+ bs.source.reset_value if bs.mode == :reverse && v == @value
99
+ [bs, v]
93
100
  end
94
101
 
95
102
  # source: BindingSource
96
- def self.attach(source, target, propagate)
97
- target.instance_eval do
98
- detach(@source.source, self, false) if @source
99
- @source = source
100
- reset_value(propagate)
101
- end
102
-
103
- source.source.instance_eval { @targets[target] = source }
103
+ def attach_source(source)
104
+ detach_source
105
+ @source = source
106
+ source.source.send(:targets)[self] = source
107
+ nil
104
108
  end
105
109
 
106
- # source: BindableAttribute
107
- def self.detach(source, target, propagate)
108
- return unless target.source and target.source.source == source
110
+ def targets_to_propagate
111
+ targets = []
112
+ targets << @source.source if @source && BindingSource::PROPAGATE_TO_SOURCE.member?(@source.mode)
113
+ targets.concat(@targets.values.select { |b| BindingSource::PROPAGATE_TO_TARGETS.member?(b.mode) })
114
+ end
109
115
 
110
- source.instance_eval { @targets.delete(target) }
111
- target.instance_eval { @source = nil; reset_value(propagate) }
116
+ def propagate_value(exclusions)
117
+ targets_to_propagate.each { |target| target.send(:set_value, value, exclusions) }
118
+ nil
119
+ end
112
120
 
121
+ def hookable_value_changed(*_)
122
+ call_hooks('on_changed', owner, attribute: name, value: @value)
113
123
  nil
114
124
  end
115
125
  end
@@ -30,7 +30,7 @@ module Ducktape
30
30
  end
31
31
 
32
32
  def default
33
- @default.is_a?(Proc) ? @default.call : @default
33
+ @default.respond_to?('call') ? @default.call : @default
34
34
  end
35
35
 
36
36
  def validation(*options, &block)
@@ -41,9 +41,9 @@ module Ducktape
41
41
  def validate(value)
42
42
  return true unless @validation
43
43
  @validation.each do |v|
44
- return true if ( v.is_a?(Class) && value.is_a?(v) ) ||
45
- ( v.is_a?(Proc) && v.(value) ) ||
46
- ( v.is_a?(Regexp) && value =~ v ) ||
44
+ return true if ( v.is_a?(Class) && value.is_a?(v) ) ||
45
+ ( v.respond_to?('call') && v.(value) ) ||
46
+ ( v.is_a?(Regexp) && value =~ v ) ||
47
47
  value == v
48
48
  end
49
49
  false
@@ -0,0 +1,32 @@
1
+ class Array
2
+ include Ducktape::Hookable
3
+
4
+ make_hooks %w'clear
5
+ compact!
6
+ concat
7
+ delete
8
+ delete_at
9
+ delete_if
10
+ fill
11
+ flatten!
12
+ insert
13
+ keep_if
14
+ map!
15
+ pop
16
+ push
17
+ reject!
18
+ replace
19
+ reverse!
20
+ rotate!
21
+ select!
22
+ shift
23
+ shuffle!
24
+ slice!
25
+ sort!
26
+ sort_by!
27
+ uniq!
28
+ unshift',
29
+ '<<' => 'append',
30
+ '[]=' => 'store',
31
+ 'collect!' => 'map!'
32
+ end
@@ -0,0 +1,19 @@
1
+ class Hash
2
+ include Ducktape::Hookable
3
+
4
+ make_hooks %w'clear
5
+ default=
6
+ default_proc=
7
+ delete
8
+ delete_if
9
+ keep_if
10
+ merge!
11
+ rehash
12
+ reject!
13
+ replace
14
+ select!
15
+ shift
16
+ store',
17
+ '[]=' => 'store',
18
+ 'update' => 'merge!'
19
+ end
@@ -0,0 +1,33 @@
1
+ class String
2
+ include Ducktape::Hookable
3
+
4
+ make_hooks %w'concat
5
+ capitalize!
6
+ chomp!
7
+ chop!
8
+ clear
9
+ delete!
10
+ downcase!
11
+ encode!
12
+ force_encoding
13
+ gsub!
14
+ insert
15
+ lstrip!
16
+ next!
17
+ prepend
18
+ replace
19
+ reverse!
20
+ rstrip!
21
+ setbyte
22
+ slice!
23
+ squeeze!
24
+ strip!
25
+ sub!
26
+ swapcase!
27
+ tr!
28
+ tr_s!
29
+ upcase!',
30
+ '<<' => 'concat',
31
+ '[]=' => 'store',
32
+ 'succ!' => 'next!'
33
+ end
@@ -5,10 +5,41 @@ module Ducktape
5
5
  def def_hook(*events)
6
6
  events.each { |e| define_method e, ->(method_name = nil, &block){ add_hook(e, method_name, &block) } }
7
7
  end
8
+
9
+ %w'hook handler'.each do |type|
10
+ define_method "make_#{type}s" do |*args|
11
+ return if args.length == 0
12
+
13
+ #def_hook 'on_changed' unless method_defined?('on_changed')
14
+
15
+ names_hash = args.pop if args.last.is_a?(Hash)
16
+ names_hash ||= {}
17
+
18
+ #Reversed merge because names_hash has priority.
19
+ names_hash = Hash[args.flatten.map { |v| [v, v] }].merge!(names_hash)
20
+
21
+ names_hash.each do |name, aka|
22
+ aka = "on_#{aka}"
23
+ def_hook(aka) unless method_defined?(aka)
24
+
25
+ um = public_instance_method(name)
26
+ cm = "call_#{type}s"
27
+ define_method(name) do |*a, &block|
28
+ bm = um.bind(self)
29
+ r = bm.(*a, &block)
30
+ if !send(cm, aka, self, event: name, args: a, result: r) || type != 'handler'
31
+ send( cm, 'on_changed', self, event: name, args: a, result: r)
32
+ end
33
+ r
34
+ end
35
+ end
36
+ end
37
+ end
8
38
  end
9
39
 
10
40
  def self.included(base)
11
41
  base.extend(ClassMethods)
42
+ base.def_hook :on_changed unless base.method_defined? :on_changed
12
43
  end
13
44
 
14
45
  def self.extended(_)
@@ -18,13 +49,13 @@ module Ducktape
18
49
  def add_hook(event, hook = nil, &block)
19
50
  hook = block if block #block has precedence
20
51
  return unless hook
21
- hook = hook.to_s unless hook.is_a?(Proc)
52
+ hook = hook.to_s unless hook.respond_to?('call')
22
53
  self.hooks[event.to_s].unshift(hook)
23
54
  hook
24
55
  end
25
56
 
26
57
  def remove_hook(event, hook)
27
- hook = hook.to_s unless hook.is_a?(Proc)
58
+ hook = hook.to_s unless hook.respond_to?('call')
28
59
  self.hooks[event.to_s].delete(hook)
29
60
  end
30
61
 
@@ -36,29 +67,23 @@ module Ducktape
36
67
  end
37
68
  end
38
69
 
39
- protected
70
+ protected #--------------------------------------------------------------
71
+
40
72
  def hooks
41
73
  @hooks ||= Hash.new { |h,k| h[k.to_s] = [] }
42
74
  end
43
75
 
44
- def call_hooks(event, caller, *args)
45
- return unless self.hooks.has_key? event.to_s
46
- self.hooks[event.to_s].each do |hook|
47
- hook = caller.method(hook) unless hook.is_a?(Proc)
48
- hook.(event, caller, *args)
49
- end
50
- nil
51
- end
52
-
53
- # Similar to `call_hooks`, but stops calling other hooks when a hook returns a value other than nil or false.
54
- def call_handlers(event, caller, *args)
55
- return unless self.hooks.has_key? event.to_s
56
- self.hooks[event.to_s].each do |hook|
57
- hook = caller.method(hook) unless hook.is_a?(Proc)
58
- handled = hook.(event, caller, *args)
59
- return handled if handled
60
- end
61
- nil
76
+ # `#call_handlers` is similar to `#call_hooks`,
77
+ # but stops calling other hooks when a hook returns a value other than nil or false.
78
+ %w'hook handler'.each do |type|
79
+ define_method("call_#{type}s", ->(event, caller = self, parms = {}) do
80
+ return unless self.hooks.has_key? event.to_s
81
+ self.hooks[event.to_s].each do |hook|
82
+ hook = caller.method(hook) unless hook.respond_to?('call')
83
+ handled = hook.(event, caller, parms)
84
+ break handled if type == 'handler' && handled
85
+ end
86
+ end)
62
87
  end
63
88
  end
64
89
  end
data/lib/ducktape.rb CHANGED
@@ -3,16 +3,12 @@ require 'version'
3
3
  module Ducktape
4
4
  camelize = ->(f){ f.gsub(/(^|_)([^_]+)/) { |_| $2.capitalize } }
5
5
 
6
- Dir["#{File.expand_path('../ducktape', __FILE__)}/*.rb"].
7
- map { |f| File.basename(f, File.extname(f)) }.
8
- each { |f| autoload camelize.(f), "ducktape/#{f}" }
6
+ %w'ducktape'.each do |dir|
7
+ Dir["#{File.expand_path("../#{dir}", __FILE__)}/*.rb"].
8
+ map { |f| File.basename(f, File.extname(f)) }.
9
+ each { |f| autoload camelize.(f), "#{dir}/#{f}" }
10
+ end
11
+
12
+ %w'def_hookable'.each { |f| require "ext/#{f}" }
9
13
 
10
- #%w'bindable
11
- # bindable_attribute
12
- # bindable_attribute_metadata
13
- # binding_source
14
- # hookable
15
- # hookable_array
16
- # hookable_collection
17
- # hookable_hash'.each { |f| autoload camelize.(f), "ducktape/#{f}" }
18
14
  end
@@ -0,0 +1,113 @@
1
+ module Ducktape
2
+
3
+ @hookable_types = {}
4
+
5
+ def self.def_hookable(klass, *args)
6
+ return if args.length == 0
7
+
8
+ names_hash = args.pop if args.last.is_a?(Hash)
9
+ names_hash ||= {}
10
+
11
+ #Reversed merge because names_hash has priority.
12
+ @hookable_types[klass] = Hash[args.flatten.map { |v| [v, v] }].merge!(names_hash)
13
+
14
+ nil
15
+ end
16
+
17
+ def self.hookable(obj)
18
+ unless obj.is_a? Hookable
19
+ m = obj.class.ancestors.each do |c|
20
+ v = @hookable_types[c]
21
+ break v if v
22
+ end
23
+
24
+ if m
25
+ (class << obj
26
+ include Hookable
27
+ self
28
+ end).make_hooks(m)
29
+ end
30
+ end
31
+
32
+ obj
33
+ end
34
+
35
+ def_hookable Array,
36
+ %w'clear
37
+ compact!
38
+ concat
39
+ delete
40
+ delete_at
41
+ delete_if
42
+ fill
43
+ flatten!
44
+ insert
45
+ keep_if
46
+ map!
47
+ pop
48
+ push
49
+ reject!
50
+ replace
51
+ reverse!
52
+ rotate!
53
+ select!
54
+ shift
55
+ shuffle!
56
+ slice!
57
+ sort!
58
+ sort_by!
59
+ uniq!
60
+ unshift',
61
+ '<<' => 'append',
62
+ '[]=' => 'store',
63
+ 'collect!' => 'map!'
64
+
65
+ def_hookable Hash,
66
+ %w'clear
67
+ default=
68
+ default_proc=
69
+ delete
70
+ delete_if
71
+ keep_if
72
+ merge!
73
+ rehash
74
+ reject!
75
+ replace
76
+ select!
77
+ shift
78
+ store',
79
+ '[]=' => 'store',
80
+ 'update' => 'merge!'
81
+
82
+ def_hookable String,
83
+ %w'concat
84
+ capitalize!
85
+ chomp!
86
+ chop!
87
+ clear
88
+ delete!
89
+ downcase!
90
+ encode!
91
+ force_encoding
92
+ gsub!
93
+ insert
94
+ lstrip!
95
+ next!
96
+ prepend
97
+ replace
98
+ reverse!
99
+ rstrip!
100
+ setbyte
101
+ slice!
102
+ squeeze!
103
+ strip!
104
+ sub!
105
+ swapcase!
106
+ tr!
107
+ tr_s!
108
+ upcase!',
109
+ '<<' => 'concat',
110
+ '[]=' => 'store',
111
+ 'succ!' => 'next!'
112
+
113
+ end
data/lib/version.rb CHANGED
@@ -1,3 +1,7 @@
1
1
  module Ducktape
2
- VERSION = '0.2.1'
2
+ # Although against rubygems recomendation, while version is < 1.0.0, an increase in the minor version number
3
+ # may represent an incompatible implementation with the previous minor version, which should have been
4
+ # represented by a major version number increase.
5
+
6
+ VERSION = '0.3.0'
3
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ducktape
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-05-20 00:00:00.000000000 Z
13
+ date: 2012-06-03 00:00:00.000000000 Z
14
14
  dependencies: []
15
15
  description: Truly outrageous bindable attributes
16
16
  email:
@@ -24,11 +24,12 @@ files:
24
24
  - lib/ducktape/bindable_attribute.rb
25
25
  - lib/ducktape/bindable_attribute_metadata.rb
26
26
  - lib/ducktape/binding_source.rb
27
+ - lib/ducktape/ext/array.rb
28
+ - lib/ducktape/ext/hash.rb
29
+ - lib/ducktape/ext/string.rb
27
30
  - lib/ducktape/hookable.rb
28
- - lib/ducktape/hookable_array.rb
29
- - lib/ducktape/hookable_collection.rb
30
- - lib/ducktape/hookable_hash.rb
31
31
  - lib/ducktape.rb
32
+ - lib/ext/def_hookable.rb
32
33
  - lib/version.rb
33
34
  - README.md
34
35
  homepage: https://github.com/SilverPhoenix99/ducktape
@@ -1,69 +0,0 @@
1
- module Ducktape
2
- class HookableArray
3
- include HookableCollection
4
-
5
- def self.[](*args)
6
- new([*args])
7
- end
8
-
9
- def self.try_convert(obj)
10
- return obj if obj.is_a? self
11
- obj = Array.try_convert(obj)
12
- obj ? new(obj) : nil
13
- end
14
-
15
- # Careful when duping arrays. Duping is shallow.
16
- def initialize(*args, &block)
17
- @content = if args.length == 1
18
- arg = args[0]
19
- case
20
- when arg.is_a?(Array) then arg.dup
21
- when arg.is_a?(HookableArray) then arg.instance_variable_get('@content').dup
22
- when arg.is_a?(Enumerable) || arg.respond_to?(:to_a) then arg.to_a
23
- end
24
- end || Array.new(*args, &block)
25
- end
26
-
27
- def to_a() self end
28
- def to_ary() self end
29
-
30
- def ==(other)
31
- other = Array.try_convert(other)
32
- return false unless other || other.count != self.count
33
- enum = other.each
34
- each { |v1| return false unless v1 == enum.next }
35
- true
36
- end
37
-
38
- compile_hooks(
39
- %w'clear
40
- collect!
41
- compact!
42
- concat
43
- delete
44
- delete_at
45
- delete_if
46
- fill
47
- flatten!
48
- insert
49
- keep_if
50
- map!
51
- pop
52
- push
53
- reject!
54
- replace
55
- reverse!
56
- rotate!
57
- select!
58
- shift
59
- shuffle!
60
- slice!
61
- sort!
62
- sort_by!
63
- uniq!
64
- unshift',
65
- '<<' => 'append',
66
- '[]=' => 'store'
67
- )
68
- end
69
- end
@@ -1,58 +0,0 @@
1
- module Ducktape
2
- module HookableCollection
3
- extend Hookable::ClassMethods
4
-
5
- module ClassMethods
6
- def compile_hooks(names_ary, names_hash = {})
7
- #Reversed merge because names_hash has priority.
8
- names_hash = Hash[names_ary.map { |v| [v, v] }].merge!(names_hash)
9
-
10
- names_hash.each do |name, aka|
11
- aka = "on_#{aka}"
12
- def_hook(aka) unless method_defined?(aka)
13
- defined_hooks[name.to_s] = aka
14
- end
15
-
16
- nil
17
- end
18
-
19
- def defined_hooks
20
- @defined_hooks ||= {}
21
- end
22
- end
23
-
24
- def self.included(base)
25
- base.send(:include, Hookable)
26
- base.extend(ClassMethods)
27
- end
28
-
29
- def self.extended(_)
30
- raise 'Cannot extend, only include.'
31
- end
32
-
33
- def_hook 'on_changed'
34
-
35
- def content() @content.dup end
36
- def to_s() "#{self.class}#{@content.to_s}" end
37
- def inspect() "#{self.class}#{@content.inspect}" end
38
- def hash() @content.hash end
39
- def dup() self.class.new(@content) end
40
-
41
- def eq?(other)
42
- equal?(other) || self == other
43
- end
44
-
45
- def method_missing(name, *args, &block)
46
- result = @content.public_send(name, *args, &block)
47
- result = self if result.equal?(@content)
48
-
49
- aka = self.class.defined_hooks[name.to_s]
50
- if aka
51
- call_hooks(aka, self, args, result)
52
- call_hooks('on_changed', self, name, args, result)
53
- end
54
-
55
- result
56
- end
57
- end
58
- end
@@ -1,55 +0,0 @@
1
- module Ducktape
2
- class HookableHash
3
- include HookableCollection
4
-
5
- def self.[](*args)
6
- new(Hash[*args])
7
- end
8
-
9
- def self.try_convert(obj)
10
- return obj if obj.is_a? self
11
- obj = Hash.try_convert(obj)
12
- obj ? new(obj) : nil
13
- end
14
-
15
- # Careful with duping hashes. Duping is shallow.
16
- def initialize(*args, &block)
17
- @content = if args.length == 1
18
- arg = args[0]
19
- case
20
- when arg.is_a?(Hash) then arg.dup
21
- when arg.is_a?(HookableHash) then arg.instance_variable_get('@hash').dup
22
- when arg.respond_to?(:to_hash) then arg.to_hash
23
- end
24
- end || Hash.new(*args, &block)
25
- end
26
-
27
- def to_hash() self end
28
-
29
- def ==(other)
30
- other = Hash.try_convert(other)
31
- return false unless other || other.count != self.count
32
- enum = other.each
33
- each { |v1| return false unless v1 == enum.next }
34
- true
35
- end
36
-
37
- compile_hooks(
38
- %w'clear
39
- default=
40
- default_proc=
41
- delete
42
- delete_if
43
- keep_if
44
- merge!
45
- rehash
46
- reject!
47
- replace
48
- select!
49
- shift
50
- store
51
- update',
52
- '[]=' => 'store'
53
- )
54
- end
55
- end