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 +4 -471
- data/lib/ducktape/bindable.rb +11 -9
- data/lib/ducktape/bindable_attribute.rb +64 -54
- data/lib/ducktape/bindable_attribute_metadata.rb +4 -4
- data/lib/ducktape/ext/array.rb +32 -0
- data/lib/ducktape/ext/hash.rb +19 -0
- data/lib/ducktape/ext/string.rb +33 -0
- data/lib/ducktape/hookable.rb +46 -21
- data/lib/ducktape.rb +7 -11
- data/lib/ext/def_hookable.rb +113 -0
- data/lib/version.rb +5 -1
- metadata +6 -5
- data/lib/ducktape/hookable_array.rb +0 -69
- data/lib/ducktape/hookable_collection.rb +0 -58
- data/lib/ducktape/hookable_hash.rb +0 -55
data/README.md
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
|
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
|
-
|
13
|
-
-------------------
|
11
|
+
Additional documentation can be found in the [Wiki](https://github.com/SilverPhoenix99/ducktape/wiki).
|
14
12
|
|
15
|
-
|
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>
|
data/lib/ducktape/bindable.rb
CHANGED
@@ -41,38 +41,40 @@ module Ducktape
|
|
41
41
|
raise 'Cannot extend, only include.'
|
42
42
|
end
|
43
43
|
|
44
|
-
def unbind_source(
|
45
|
-
get_bindable_attr(
|
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
|
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
|
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
|
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
|
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
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
20
|
+
#attr_reader :targets # Hash{ BindableAttribute => BindingSource }
|
21
|
+
|
22
|
+
#def_hook :on_changed
|
22
23
|
|
23
24
|
def initialize(owner, name)
|
24
|
-
@owner, @name, @
|
25
|
-
reset_value
|
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
|
-
|
37
|
-
|
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
|
41
|
-
|
42
|
-
|
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
|
-
|
56
|
+
def to_s
|
57
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} @name=#{name}>"
|
51
58
|
end
|
52
59
|
|
53
|
-
|
60
|
+
private #----------------------------------------------------------------
|
61
|
+
|
62
|
+
attr_reader :targets
|
54
63
|
|
55
64
|
def set_value(value, exclusions = Set.new)
|
56
|
-
return if exclusions.
|
65
|
+
return if exclusions.include? self
|
57
66
|
exclusions << self
|
58
67
|
|
59
68
|
if value.is_a? BindingSource
|
60
|
-
|
61
|
-
|
62
|
-
#
|
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
|
-
|
80
|
-
|
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
|
-
|
85
|
+
call_hooks('on_changed', owner, attribute: name.dup, value: @value, old_value: old_value)
|
86
|
+
end
|
85
87
|
|
86
|
-
|
87
|
-
|
88
|
-
BindingSource::PROPAGATE_TO_SOURCE.member? @source.mode
|
88
|
+
propagate_value(exclusions)
|
89
|
+
@value
|
89
90
|
end
|
90
91
|
|
91
|
-
def
|
92
|
-
|
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
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
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
|
-
|
111
|
-
|
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.
|
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)
|
45
|
-
( v.
|
46
|
-
( v.is_a?(Regexp)
|
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
|
data/lib/ducktape/hookable.rb
CHANGED
@@ -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.
|
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.
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
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.
|
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-
|
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
|