ducktape 0.2.1 → 0.3.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/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