ducktape 0.0.5 → 0.1.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 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: