ducktape 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,400 @@
1
+ Ducktape
2
+ ========
3
+
4
+ A [truly outrageous](http://youtu.be/dSPb56-_I98) gem for bindable attributes.
5
+
6
+ To install:
7
+
8
+ ```
9
+ gem install ducktape
10
+ ```
11
+
12
+ Bindable attributes
13
+ -------------------
14
+
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
+ Validation also works with any kind of objects. For example, to make an enumerable:
211
+
212
+ ```ruby
213
+ class X
214
+ include Ducktape::Bindable
215
+
216
+ # place attribute will only accept assignment to these three symbols:
217
+ bindable :place, default: :first, validate: [:first, :middle, :last]
218
+ end
219
+ ```
220
+
221
+ 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`.
222
+
223
+ ### Coercion
224
+
225
+ 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.
226
+
227
+ 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.
228
+
229
+ ```ruby
230
+ class X
231
+ include Ducktape::Bindable
232
+
233
+ bindable :my_float,
234
+ validate: Numeric,
235
+ default: 0.0,
236
+ coerce: ->(owner, value) { value = value.to_f; value < 0.0 ? 0.0 : (value > 1.0 ? 1.0 : value) }
237
+ end
238
+
239
+ x = X.new
240
+ x.my_float = 2.0
241
+
242
+ puts x.my_float
243
+ ```
244
+
245
+ The output would be:
246
+ ```ruby
247
+ => 1.0
248
+ ```
249
+
250
+ Note that if validation is defined, then it will only happen after coercion is applied.
251
+
252
+ ### Hookable change notifications
253
+
254
+ You can watch for changes in a BA by using the public instance method `#on_changed(attr_name, &block)`. Here's an example:
255
+
256
+ ```ruby
257
+ def attribute_changed(event, owner, attr_name, new_value, old_value)
258
+ 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}"
259
+ end
260
+
261
+ class X
262
+ include Ducktape::Bindable
263
+
264
+ bindable :name, validate: [String, Symbol]
265
+ bindable :age, validate: Integer
266
+ bindable :points, validate: Integer
267
+
268
+ def initialize(name, age, points)
269
+ self.name = name
270
+ self.age = age
271
+ self.points = points
272
+
273
+ # You can hook for any method available
274
+ %w'name age points'.each { |k, v| on_changed k, &method(:attribute_changed) }
275
+ end
276
+ end
277
+
278
+ # oops, a misspelling...
279
+ x = X.new('Richad', 23, 150)
280
+
281
+ # It's also useful to see changes outside of the class:
282
+ x.on_changed 'name', &->(_, _, _, _, new_value) { puts "Hello #{new_value}!" }
283
+
284
+ x.name = 'Richard'
285
+ ```
286
+
287
+ After calling `#name=`, the output should be something like:
288
+
289
+ ```ruby
290
+ => "Hello Richard!"
291
+ => "X<14e35b4> called the event \"on_changed\" and changed the attribute \"name\" from \"Richad\" to \"Richard\""
292
+ ```
293
+
294
+ The `on_changed` hook has the following arguments:
295
+ * the name of the event (in this case, `'on_changed'`)
296
+ * the caller/owner of the BA (the instance that sent the message),
297
+ * the name of the BA (`name`, `age`, `points`, etc...),
298
+ * the new value,
299
+ * the old value
300
+
301
+ Hooks
302
+ -----
303
+
304
+ Has you might have seen, Ducktape comes with hooks, which is what powers the `on_changed` for bindable attributes.
305
+ You can easily define a hook by using `def_hook`:
306
+
307
+ ```ruby
308
+ def called_load(event, owner)
309
+ puts "#{owner.class}<#{owner.object_id.to_s(16)}> called #{event.inspect}"
310
+ end
311
+
312
+ class X
313
+ include Ducktape::Hookable
314
+
315
+ def_hook :on_loaded #define one or more hooks
316
+
317
+ def load
318
+ call_hooks(:on_loaded)
319
+ end
320
+ end
321
+
322
+ x = X.new
323
+
324
+ x.on_loaded &method(:called_load)
325
+
326
+ #if we didn't create a hook with def_hook we could still use:
327
+ #x.add_hook :on_loaded, &method(:called_load)
328
+
329
+ x.load
330
+ ```
331
+
332
+ The output should be something like:
333
+ ```ruby
334
+ => "X<14e35b4> called \"on_loaded\""
335
+ ```
336
+
337
+ ### Removing hooks
338
+
339
+ To remove all hooks from an object call the `#clear_hooks` method. To select a single hook, pass the name of the hook as a parameter. The next section has an example of this.
340
+
341
+ ### Hookable arrays
342
+
343
+ A Ducktape::HookableArray is a wrapper for arrays that allows you to add hooks to modifiers of the array.
344
+ 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_`.
345
+
346
+ There are two exceptions to the naming convention:
347
+ * `HookableArray#[]=`: pass the hook through the `on_assign` method.
348
+ * `HookableArray#<<`: pass the hook through the `on_append` method.
349
+
350
+ The parameters for all these hooks are very similar to the ones used for bindables:
351
+ * the name of the event (for example, `'on_assign'`)
352
+ * the instance of `HookableArray` that triggered the hook
353
+ * an array of the arguments that were passed to the method that triggered the hook (for example, the index and value of the `[]=` method)
354
+ * and the result of the call to the method
355
+
356
+ Additionally, there is a generic `on_changed` hook, that is called for every modifier. In this case, the parametes are:
357
+ * the name of the event (for example, `'on_assign'`)
358
+ * the instance of `HookableArray` that triggered the hook
359
+ * the name of the method (`"[]="`, `"<<"`, "sort!", etc...) that triggered the hook
360
+ * an array of the arguments that were passed to the method that triggered the hook (for example, the index and value of the `[]=` method)
361
+ * and the result of the call to the method
362
+
363
+ Here is an example that shows how to use the hooks on a HookableArray:
364
+
365
+ ```ruby
366
+ a = Ducktape::HookableArray[1, :x] #same as new()
367
+ a.on_append do |event, owner, args, result|
368
+ puts "#{event.inspect}, #{owner.class}<#{owner.object_id.to_s(16)}>, #{args.inspect}, #{result.inspect}"
369
+ end
370
+
371
+ a.on_changed do |event, owner, name, args, result|
372
+ puts "#{event.inspect}, #{owner.class}<#{owner.object_id.to_s(16)}>, #{name.inspect}, #{args.inspect}, #{result.inspect}"
373
+ end
374
+
375
+ a << 'hi'
376
+ ```
377
+
378
+ The output would be something like:
379
+ ```ruby
380
+ => "\"on_append\", Ducktape::HookableArray<37347c>, [\"hi\"], [1, :x, \"hi\"]"
381
+ => "\"on_changed\", Ducktape::HookableArray<37347c>, \"<<\", [\"hi\"], [1, :x, \"hi\"]"
382
+ ```
383
+
384
+ If you then do:
385
+ ```ruby
386
+ a.clear_hooks('on_append')
387
+
388
+ a << 'bye'
389
+ ```
390
+
391
+ The output will only be for the `on_changed` hook, that wasn't removed:
392
+ ```ruby
393
+ => "\"on_changed\", Ducktape::HookableArray<37347c>, \"<<\", [\"bye\"], [1, :x, \"hi\", \"bye\"]"
394
+ ```
395
+
396
+ Future work
397
+ ===========
398
+ * Hashes passed to BA's should check for element changes (hashes with hooks).
399
+ * Multi-sourced BA's.
400
+ * More complex binding source paths instead of just the member name (e.g.: ruby like 'a.b.c' or xml like 'a/b/c').
@@ -1,4 +1,5 @@
1
1
  module Ducktape
2
+
2
3
  class AttributeNotDefinedError < StandardError
3
4
  def initialize(klass, name)
4
5
  super("attribute #{name.to_s.inspect} not defined for class #{klass}")
@@ -1,53 +1,58 @@
1
1
  module Ducktape
2
2
  class BindableAttributeMetadata
3
3
 
4
- attr_reader :name
5
-
6
- def initialize(name, options = {})
7
- if name.is_a? BindableAttributeMetadata
8
- @name = name.name
9
- @default = options[:default] || name.instance_variable_get(:@default)
10
- @validation = options[:validate] || name.instance_variable_get(:@validation)
11
- @coercion = options[:coerce] || name.instance_variable_get(:@coercion)
12
- else
13
- @name = name
14
- @default = options[:default]
15
- @validation = options[:validate]
16
- @coercion = options[:coerce]
17
- end
18
-
19
- @validation = [*@validation] unless @validation.nil?
20
- end
21
-
22
- def default=(value)
23
- @default = value
24
- end
25
-
26
- def default
27
- @default.is_a?(Proc) ? @default.call : @default
28
- end
29
-
30
- def validation(*options, &block)
31
- options << block
32
- @validation = options
33
- end
34
-
35
- def validate(value)
36
- return true unless @validation
37
- @validation.each do |validation|
38
- return true if (validation.is_a?(Class) and value.is_a?(validation)) or
39
- (validation.is_a?(Proc) and validation.call(value)) or
40
- value == validation
41
- end
42
- false
43
- end
44
-
45
- def coercion(&block)
46
- @coercion = block
47
- end
48
-
49
- def coerce(owner, value)
50
- @coercion ? @coercion.call(owner, value) : value
51
- end
52
- end
4
+ VALID_OPTIONS = [:access, :default, :validate, :coerce].freeze
5
+
6
+ attr_reader :name
7
+
8
+ def initialize(name, options = {})
9
+
10
+ options.each_key { |k| puts "WARNING: invalid option #{k.inspect} for #{name.inspect} attribute. Will be ignored." unless VALID_OPTIONS.member?(k) }
11
+
12
+ if name.is_a? BindableAttributeMetadata
13
+ @name = name.name
14
+ @default = options[:default] || name.instance_variable_get(:@default)
15
+ @validation = options[:validate] || name.instance_variable_get(:@validation)
16
+ @coercion = options[:coerce] || name.instance_variable_get(:@coercion)
17
+ else
18
+ @name = name
19
+ @default = options[:default]
20
+ @validation = options[:validate]
21
+ @coercion = options[:coerce]
22
+ end
23
+
24
+ @validation = [*@validation] unless @validation.nil?
25
+ end
26
+
27
+ def default=(value)
28
+ @default = value
29
+ end
30
+
31
+ def default
32
+ @default.is_a?(Proc) ? @default.call : @default
33
+ end
34
+
35
+ def validation(*options, &block)
36
+ options << block
37
+ @validation = options
38
+ end
39
+
40
+ def validate(value)
41
+ return true unless @validation
42
+ @validation.each do |validation|
43
+ return true if (validation.is_a?(Class) and value.is_a?(validation)) or
44
+ (validation.is_a?(Proc) and validation.call(value)) or
45
+ value == validation
46
+ end
47
+ false
48
+ end
49
+
50
+ def coercion(&block)
51
+ @coercion = block
52
+ end
53
+
54
+ def coerce(owner, value)
55
+ @coercion ? @coercion.call(owner, value) : value
56
+ end
57
+ end
53
58
  end
@@ -25,6 +25,15 @@ module Ducktape
25
25
  self.hooks[event.to_s].delete(block)
26
26
  end
27
27
 
28
+ def clear_hooks(event = nil)
29
+ if event
30
+ self.hooks.delete(event.to_s)
31
+ else
32
+ self.hooks.clear
33
+ end
34
+ nil
35
+ end
36
+
28
37
  protected
29
38
  def hooks
30
39
  @hooks ||= Hash.new { |h,k| h[k.to_s] = [] }
@@ -1,5 +1,54 @@
1
1
  module Ducktape
2
- module HookableArray
2
+ class HookableArray
3
+ include Hookable
3
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
+ return nil if obj.nil?
13
+ new(obj)
14
+ end
15
+
16
+ def initialize(*args, &block)
17
+ @array = if args.length == 1 && (args[0].is_a?(Array) || args[0].is_a?(HookableArray))
18
+ args[0]
19
+ else
20
+ Array.new(*args, &block)
21
+ end
22
+ end
23
+
24
+ def method_missing(name, *args, &block)
25
+ @array.public_send(name, *args, &block)
26
+ end
27
+
28
+ def to_s() @array.to_s end
29
+ def inspect() @array.inspect end
30
+ def to_ary() self end
31
+
32
+ def_hook 'on_changed'
33
+
34
+ compile_hook = ->(name, aka = nil) do
35
+ aka ||= name
36
+ aka = "on_#{aka}"
37
+
38
+ def_hook(aka) unless method_defined?(aka)
39
+
40
+ define_method(name) do |*args, &block|
41
+ result = @array.public_send(__method__, *args, &block)
42
+ call_hooks(aka, self, args, result)
43
+ call_hooks('on_changed', self, name, args, result)
44
+ result
45
+ end
46
+ end
47
+
48
+ %w'clear collect! compact! concat delete delete_at delete_if fill flatten! insert keep_if
49
+ map! pop push reject! replace reverse! rotate! select! shift shuffle! slice! sort!
50
+ sort_by! uniq! unshift'.each { |m| compile_hook.(m) }
51
+
52
+ { '[]=' => 'assign', '<<' => 'append' }.each { |k, v| compile_hook.(k, v) }
4
53
  end
5
54
  end
@@ -0,0 +1,3 @@
1
+ module Ducktape
2
+ VERSION = '0.1.0'
3
+ end
data/lib/ducktape.rb CHANGED
@@ -1,11 +1,12 @@
1
- module Ducktape
2
- ROOT = File.expand_path('../ducktape', __FILE__)
1
+ require 'ducktape/version'
3
2
 
3
+ module Ducktape
4
4
  {
5
5
  :Bindable => 'bindable',
6
6
  :BindableAttribute => 'bindable_attribute',
7
7
  :BindableAttributeMetadata => 'bindable_attribute_metadata',
8
8
  :BindingSource => 'binding_source',
9
- :Hookable => 'hookable'
10
- }.each { |k, v| autoload k, "#{ROOT}/#{v}" }
9
+ :Hookable => 'hookable',
10
+ :HookableArray => 'hookable_array'
11
+ }.each { |k, v| autoload k, "ducktape/#{v}" }
11
12
  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.0.5
4
+ version: 0.1.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-04-30 00:00:00.000000000 Z
13
+ date: 2012-05-01 00:00:00.000000000 Z
14
14
  dependencies: []
15
15
  description: Truly outrageous bindable attributes
16
16
  email:
@@ -20,13 +20,15 @@ executables: []
20
20
  extensions: []
21
21
  extra_rdoc_files: []
22
22
  files:
23
- - lib/ducktape.rb
24
23
  - lib/ducktape/bindable.rb
25
24
  - lib/ducktape/bindable_attribute.rb
26
25
  - lib/ducktape/bindable_attribute_metadata.rb
27
26
  - lib/ducktape/binding_source.rb
28
27
  - lib/ducktape/hookable.rb
29
28
  - lib/ducktape/hookable_array.rb
29
+ - lib/ducktape/version.rb
30
+ - lib/ducktape.rb
31
+ - README.md
30
32
  homepage: https://github.com/SilverPhoenix99/ducktape
31
33
  licenses: []
32
34
  post_install_message: