Flexibility 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +119 -0
  3. data/lib/flexibility.rb +968 -0
  4. metadata +80 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 33a826c7309e8f6404a2f4df4899676fcaff263c
4
+ data.tar.gz: 5e09c99b2649a9ecebca920903e711b53bfa431a
5
+ SHA512:
6
+ metadata.gz: be07cdecbaaf25a0d76fd40f1f0265cd64691f9a136e5c46a4cfeb72fa1daab11d9de6d0fbf7ea534f7eff5e4bdd5b9cbaf3f02e9d0f78018a1860facaec820b
7
+ data.tar.gz: 185698b88fde7522e585aa2340493183390dbd36191cb48fafd44e2ed8e10ce6e8eaad0ea9dc065e4fd081947ffaf01e5f1d5e24b2b131fef559ffb69231ded8
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ `Flexibility` is a mix-in for ruby classes that allows you to easily
2
+ `#define` methods that can take a mixture of positional
3
+ and keyword arguments.
4
+
5
+ For example, suppose we define
6
+
7
+ ```ruby
8
+ class Banner
9
+ include Flexibility
10
+
11
+ define( :show,
12
+ message: [
13
+ required,
14
+ validate { |s| String === s },
15
+ transform { |s| s.upcase }
16
+ ],
17
+ width: [
18
+ default { @width },
19
+ validate { |n| 0 <= n }
20
+ ],
21
+ symbol: default('*')
22
+ ) do |message,width,symbol,unused_opts|
23
+ width = [ width, message.length + 4 ].max
24
+ puts "#{symbol * width}"
25
+ puts "#{symbol} #{message.ljust(width - 4)} #{symbol}"
26
+ puts "#{symbol * width}"
27
+ end
28
+
29
+ def initialize
30
+ @width = 40
31
+ end
32
+ end
33
+ ```
34
+
35
+ Popping over to IRB, we could use `Banner#show` with keyword arguments,
36
+
37
+ irb> banner = Banner.new
38
+ irb> banner.show( message: "HELLO", width: 10, symbol: '*' )
39
+ **********
40
+ * HELLO *
41
+ **********
42
+ => nil
43
+
44
+ positional arguments
45
+
46
+ irb> banner.show( "HELLO WORLD!", 20, '#' )
47
+ ####################
48
+ # HELLO WORLD! #
49
+ ####################
50
+ => nil
51
+
52
+ or a mix
53
+
54
+ irb> banner.show( "A-HA", symbol: '-', width: 15 )
55
+ ---------------
56
+ - A-HA -
57
+ ---------------
58
+ => nil
59
+
60
+ The keyword arguments are taken from the last argument, if it is a Hash, while
61
+ the preceeding positional arguments are matched up to the keyword in the same
62
+ position in the argument description.
63
+
64
+ `Flexibility` also allows the user to run zero or more callbacks on each
65
+ argument, and includes a number of callback generators to specify a `#default`
66
+ value, mark a given argument as `#required`, `#validate` an argument, or
67
+ `#transform` an argument into a more acceptable form.
68
+
69
+ Continuing our prior example, this means `Banner#show` only requires one
70
+ argument, which it automatically upper-cases:
71
+
72
+ irb> banner.show( "celery?" )
73
+ ****************************************
74
+ * CELERY? *
75
+ ****************************************
76
+
77
+ And it will raise an error if the `message` is missing or not a String, or if
78
+ the `width` argument is negative:
79
+
80
+ irb> banner.show
81
+ !> ArgumentError: Required argument :message not given
82
+ irb> banner.show 8675309
83
+ !> ArgumentError: Invalid value 8675309 given for argument :message
84
+ irb> banner.show "hello", -9
85
+ !> ArgumentError: Invalid value -9 given for argument :width
86
+
87
+ Just as `Flexibility#define` allows the method caller to determine whether to
88
+ pass the method arguments positionally, with keywords, or in a mixture of the
89
+ two, it also allows method authors to determine whether the method receives
90
+ arguments in a Hash or positionally:
91
+
92
+ ```ruby
93
+ class Banner
94
+ opts_desc = { a: [], b: [], c: [], d: [], e: [] }
95
+ define :all_positional, opts_desc do |a,b,c,d,e,opts|
96
+ [ a, b, c, d, e, opts ]
97
+ end
98
+ define :all_keyword, opts_desc do |opts|
99
+ [ opts ]
100
+ end
101
+ define :mixture, opts_desc do |a,b,c,opts|
102
+ [ a, b, c, opts ]
103
+ end
104
+ end
105
+ ```
106
+
107
+ irb> banner.all_positional(1,2,3,4,5)
108
+ => [ 1, 2, 3, 4, 5, {} ]
109
+ irb> banner.all_positional(a:1, b:2, c:3, d:4, e:5, f:6)
110
+ => [ 1, 2, 3, 4, 5, {f:6} ]
111
+ irb> banner.all_keyword(1,2,3,4,5)
112
+ => [ { a:1, b:2, c:3, d:4, e:5 } ]
113
+ irb> banner.all_keyword(a:1, b:2, c:3, d:4, e:5, f:6)
114
+ => [ { a:1, b:2, c:3, d:4, e:5, f:6 } ]
115
+ irb> banner.mixture(1,2,3,4,5)
116
+ => [ 1, 2, 3, { d:4, e:5 } ]
117
+ irb> banner.mixture(a:1, b:2, c:3, d:4, e:5, f:6)
118
+ => [ 1, 2, 3, { d:4, e:5, f:6 } ]
119
+
@@ -0,0 +1,968 @@
1
+ # {include:file:README.md}
2
+ #
3
+ # @author Noah Luck Easterly <noah.easterly@gmail.com>
4
+ module Flexibility
5
+
6
+ # helper for creating UnboundMethods
7
+ #
8
+ # irb> inject = Array.instance_method(:inject)
9
+ # irb> Flexibility.run_unbound_method(inject, %w{ a b c }, "x") { |l,r| "(#{l}#{r})" }
10
+ # => "(((xa)b)c)"
11
+ # irb> inject_r = Flexibility.create_unbound_method( Array ) { |*args,&blk| reverse.inject(*args, &blk) }
12
+ # irb> Flexibility.run_unbound_method(inject_r, %w{ a b c }, "x") { |l,r| "(#{l}#{r})" }
13
+ # => "(((xc)b)a)"
14
+ #
15
+ # in a less civilized time, I might have just monkey-patched this as
16
+ # `UnboundMethod::create`
17
+ #
18
+ # ----
19
+ #
20
+ # @param klass [Class] class to associate the method with
21
+ # @param body [Proc] proc to use for the method body
22
+ # @return [UnboundMethod]
23
+ def self.create_unbound_method(klass, &body)
24
+ name = body.inspect
25
+ klass.class_eval do
26
+ define_method(name, &body)
27
+ um = instance_method(name)
28
+ remove_method(name)
29
+ um
30
+ end
31
+ end
32
+
33
+ # helper to call UnboundMethods with proper number of args,
34
+ # and avoid `ArgumentError: wrong number of arguments`.
35
+ #
36
+ # irb> each = Array.instance_method(:each)
37
+ # irb> each.bind( [ 1, 2, 3] ).call( 4, 5, 6 ) { |x| puts x }
38
+ # !> ArgumentError: wrong number of arguments (3 for 0)
39
+ # irb> Flexibility.run_unbound_method(each, [ 1, 2, 3], 4, 5, 6 ) { |x| puts x }
40
+ # 1
41
+ # 2
42
+ # 3
43
+ # => [1,2,3]
44
+ #
45
+ # in a less civilized time, I might have just monkey-patched this as
46
+ # `UnboundMethod#run`
47
+ #
48
+ # ----
49
+ #
50
+ # @param um [UnboundMethod(*args,blk) => res]
51
+ # UnboundMethod to run
52
+ # @param instance [Object] object to bind `um` to, must be a instance of `um.owner`
53
+ # @param args [Array] arguments to pass to invocation of `um`
54
+ # @param blk [Proc] block to bind to invocation of `um`
55
+ # @return [res]
56
+ def self.run_unbound_method(um, instance, *args, &blk)
57
+ args = args.take(um.arity) if 0 <= um.arity && um.arity < args.length
58
+ um.bind(instance).call(*args,&blk)
59
+ end
60
+
61
+ # @!group Argument Callback Generators
62
+
63
+ # {#default} allows you to specify a default value for an argument.
64
+ #
65
+ # You can pass {#default} either
66
+ #
67
+ # - an argument containing a constant value
68
+ # - a block to be bound to the instance and run as needed
69
+ #
70
+ # With the block form, you also have access to
71
+ #
72
+ # - `self` and the instance variables of the bound instance
73
+ # - the keyword associated with the argument
74
+ # - the hash of options defined thus far
75
+ # - the original argument value (useful if an earlier transformation `nil`'ed it out)
76
+ # - the block bound to the method invocation
77
+ #
78
+ # For example, given the method `dimensions`:
79
+ #
80
+ # ```ruby
81
+ # class Banner
82
+ # include Flexibility
83
+ #
84
+ # define( :dimensions,
85
+ # depth: default( 1 ),
86
+ # width: default { @width },
87
+ # height: default { |_key,opts| opts[:width] } ,
88
+ # duration: default { |&blk| blk[] if blk }
89
+ # ) do |opts|
90
+ # opts
91
+ # end
92
+ #
93
+ # def initialize
94
+ # @width = 40
95
+ # end
96
+ # end
97
+ # ```
98
+ #
99
+ # We can specify (or not) any of the arguments to see the defaults in action
100
+ #
101
+ # irb> banner = Banner.new
102
+ # irb> banner.dimensions
103
+ # => { depth: 1, width: 40, height: 40 }
104
+ # irb> banner.dimensions( depth: 2, width: 10, height: 5, duration: 7 )
105
+ # => { depth: 2, width: 10, height: 5, duration: 7 }
106
+ # irb> banner.dimensions( width: 10 ) { puts "getting duration" ; 12 }
107
+ # getting duration
108
+ # => { depth: 1, width: 10, height: 10, duration: 12 }
109
+ #
110
+ # ----
111
+ #
112
+ # Note that the `yield` keyword inside the block bound to `default` won't be
113
+ # able to access the block bound to the method invocation, as `yield` is
114
+ # lexically scoped (like a local variable).
115
+ #
116
+ # ```ruby
117
+ # module YieldExample
118
+ # def self.create
119
+ # Class.new do
120
+ # include Flexibility
121
+ # define( :run,
122
+ # using_yield: default { yield },
123
+ # using_block: default { |&blk| blk[] }
124
+ # ) { |opts| opts }
125
+ # end.new
126
+ # end
127
+ # end
128
+ # ```
129
+ #
130
+ # irb> YieldExample.create { :class_creation }.run { :method_invocation }
131
+ # => { using_yield: :class_creation, using_block: :method_invocation }
132
+ #
133
+ # ----
134
+ #
135
+ # @param default_val
136
+ # if the returned `UnboundMethod` is called with `nil` as its first parameter,
137
+ # it returns `default_val` (unless {#default} is called with a block)
138
+ # @yield
139
+ # if the returned `UnboundMethod` is called with `nil` as its first parameter,
140
+ # it returns the result of `yield` (unless {#default} is called with an
141
+ # argument).
142
+ #
143
+ # The block bound to {#default} receives the following parameters when called
144
+ # by a method created with {#define}:
145
+ # @yieldparam key [Symbol] the key of the option currently being processed
146
+ # @yieldparam opts [Hash] the options hash thus far
147
+ # @yieldparam initial [Object] the original value passed to the method for this option
148
+ # @yieldparam &blk [Proc] the block passed to the method
149
+ # @yieldparam self [keyword] bound to the same instance that the method is invoked on
150
+ # @raise [ArgumentError]
151
+ # unless called with a block and no args, or called with no block and one arg
152
+ # @return [UnboundMethod(val,key,opts,initial,&blk)]
153
+ # @see #define
154
+ # @!parse def default(default_val=nil) ; end
155
+ def default(*args,&cb)
156
+ if args.length != (cb ? 0 : 1)
157
+ raise(ArgumentError, "Wrong number of arguments to `default` (expects 0 with a block, or 1 without)", caller)
158
+ elsif cb
159
+ um = Flexibility::create_unbound_method(self, &cb)
160
+ Flexibility::create_unbound_method(self) do |*args, &blk|
161
+ val = args.shift
162
+ unless val.nil?
163
+ val
164
+ else
165
+ Flexibility::run_unbound_method(um,self,*args,&blk)
166
+ end
167
+ end
168
+ else
169
+ default = args.first
170
+ Flexibility::create_unbound_method(self) { |*args| val = args.shift; val.nil? ? default : val }
171
+ end
172
+ end
173
+
174
+ # {#required} allows you to throw an exception if an argument is not given.
175
+ #
176
+ # {#required} returns an `UnboundMethod` that simply checks that its first
177
+ # parameter is non-`nil`:
178
+ #
179
+ # - if the parameter is `nil`, it raises an `ArgumentError`
180
+ # - if the parameter is not `nil`, it returns it.
181
+ #
182
+ # For example,
183
+ #
184
+ # ```ruby
185
+ # class Banner
186
+ # include Flexibility
187
+ #
188
+ # define( :area,
189
+ # width: required,
190
+ # height: required
191
+ # ) do |width,height,_|
192
+ # width * height
193
+ # end
194
+ # end
195
+ # ```
196
+ #
197
+ # We can specify (or not) any of the arguments to see the checking in action
198
+ #
199
+ # irb> banner = Banner.new
200
+ # irb> banner.area
201
+ # !> ArgumentError: Required argument :width not given
202
+ # irb> banner.area :width => 5
203
+ # !> ArgumentError: Required argument :height not given
204
+ # irb> banner.area :height => 5
205
+ # !> ArgumentError: Required argument :width not given
206
+ # irb> banner.area :width => 6, :height => 5
207
+ # => 30
208
+ #
209
+ # Note that {#required} specifically checks that the argument is non-nil, not
210
+ # *unspecified*, so explicitly given `nil` arguments will still raise an
211
+ # error:
212
+ #
213
+ # irb> banner.area :width => nil, :height => 5
214
+ # !> ArgumentError: Required argument :width not given
215
+ #
216
+ # ----
217
+ #
218
+ # @return [UnboundMethod(val,key,opts,initial,&blk)]
219
+ # `UnboundMethod` which returns first parameter given if non-`nil`,
220
+ # otherwise raises `ArgumentError`
221
+ # @see #define
222
+ def required
223
+ Flexibility::create_unbound_method(self) do |*args|
224
+ val, key = *args
225
+ if val.nil?
226
+ raise(ArgumentError, "Required argument #{key.inspect} not given", caller)
227
+ end
228
+ val
229
+ end
230
+ end
231
+
232
+ # {#validate} allows you to throw an exception if the given block returns
233
+ # falsy.
234
+ #
235
+ # You pass {#validate} a block which will be invoked each time the
236
+ # returned `UnboundMethod` is called.
237
+ #
238
+ # - if the block returns true, the `UnboundMethod` will return the first parameter
239
+ # - if the block returns false, the `UnboundMethod` will raise an `ArgumentError`
240
+ #
241
+ # Within the block, you have access to
242
+ #
243
+ # - `self` and the instance variables of the bound instance
244
+ # - the keyword associated with the argument
245
+ # - the hash of options defined thus far
246
+ # - the original argument value (useful if an earlier transformation `nil`'ed it out)
247
+ # - the block bound to the method invocation
248
+ #
249
+ # For example, given the method ``:
250
+ #
251
+ # ```ruby
252
+ # class Converter
253
+ # include Flexibility
254
+ #
255
+ # define( :polar_to_cartesian,
256
+ # radius: validate { |r| 0 <= r },
257
+ # theta: validate { |t| 0 <= t && t < Math::PI },
258
+ # phi: validate { |p| 0 <= p && p < 2*Math::PI }
259
+ # ) do |r,t,p,_|
260
+ # { x: r * Math.sin(t) * Math.cos(p),
261
+ # y: r * Math.sin(t) * Math.sin(p),
262
+ # z: r * Math.cos(t)
263
+ # }
264
+ # end
265
+ # end
266
+ # ```
267
+ #
268
+ # irb> conv = Converter.new
269
+ # irb> conv.polar_to_cartesian -1, 0, 0
270
+ # !> ArgumentError: Invalid value -1 given for argument :radius
271
+ # irb> conv.polar_to_cartesian 0, -1, 0
272
+ # !> ArgumentError: Invalid value -1 given for argument :theta
273
+ # irb> conv.polar_to_cartesian 0, 0, -1
274
+ # !> ArgumentError: Invalid value -1 given for argument :phi
275
+ # irb> conv.polar_to_cartesian 0, 0, 0
276
+ # => { x: 0, y: 0, z: 0 }
277
+ #
278
+ #
279
+ # And just to show how you can access instance variables,
280
+ # earlier parameters, and the bound block with {#validate}...
281
+ #
282
+ # ```ruby
283
+ # class Silly
284
+ # include Flexibility
285
+ #
286
+ # def initialize(min,max)
287
+ # @min,@max = min,max
288
+ # end
289
+ #
290
+ # in_range = validate { |x,&blk| @min <= blk[x] && blk[x] <= @max }
291
+ #
292
+ # define( :check,
293
+ # lo: in_range,
294
+ # hi: [
295
+ # in_range,
296
+ # validate { |x,key,opts,&blk| blk[opts[:lo]] <= blk[x] }
297
+ # ],
298
+ # ) { |opts| opts }
299
+ # end
300
+ # ```
301
+ #
302
+ # irb> silly = Silly.new(3,5)
303
+ # irb> silly.check("hi", "salutations") { |s| s.length }
304
+ # !> ArgumentError: Invalid value "hi" given for argument :lo
305
+ # irb> silly.check("hey", "salutations") { |s| s.length }
306
+ # !> ArgumentError: Invalid value "salutations" given for argument :hi
307
+ # irb> silly.check("hello", "hey") { |s| s.length }
308
+ # !> ArgumentError: Invalid value "hey" given for argument :hi
309
+ # irb> silly.check("hey", "hello") { |s| s.length }
310
+ # => { lo: "hey", hi: "hello" }
311
+ #
312
+ # ----
313
+ #
314
+ # Note that the `yield` keyword inside the block bound to {#validate} won't be
315
+ # able to access the block bound to the method invocation, as `yield` is
316
+ # lexically scoped (like a local variable).
317
+ #
318
+ # ```ruby
319
+ # module YieldExample
320
+ # def self.create
321
+ # Class.new do
322
+ # include Flexibility
323
+ # define( :run,
324
+ # using_yield: validate { |val,key| puts [key, yield].inspect ; true },
325
+ # using_block: validate { |val,key,&blk| puts [key, blk[]].inspect ; true }
326
+ # ) { |opts| opts }
327
+ # end.new
328
+ # end
329
+ # end
330
+ # ```
331
+ #
332
+ # irb> YieldExample.create { :class_creation }.run(1,2) { :method_invocation }
333
+ # [:using_yield, :class_creation]
334
+ # [:using_block, :method_invocation]
335
+ # => { using_yield: 1, using_block: 2 }
336
+ #
337
+ # ----
338
+ #
339
+ # @yield
340
+ # The block bound to {#validate} receives the following parameters when
341
+ # called by a method created with {#define}:
342
+ # @yieldparam val [Object] the value of the option currently being processed
343
+ # @yieldparam key [Symbol] the key for the option currently being processed
344
+ # @yieldparam opts [Hash] the options hash thus far
345
+ # @yieldparam initial [Object] the original value passed to the method for this option
346
+ # @yieldparam &blk [Proc] the block passed to the method
347
+ # @yieldparam self [keyword] bound to the same instance that the method is invoked on
348
+ # @yieldreturn [Boolean]
349
+ # indicates whether the returned `UnboundMethod` should
350
+ # return the first parameter or raise an `ArgumentError`.
351
+ # @return [UnboundMethod(val,key,opts,initial,&blk)]
352
+ # `UnboundMethod` which returns first parameter given if block
353
+ # bound to {#validate} returns truthy on arguments/block given ,
354
+ # raises `ArgumentError` otherwise.
355
+ # @see #define
356
+ def validate(&cb)
357
+ um = Flexibility::create_unbound_method(self, &cb)
358
+ Flexibility::create_unbound_method(self) do |*args,&blk|
359
+ val, key, _opts, orig = *args
360
+ unless Flexibility::run_unbound_method(um,self,*args,&blk)
361
+ raise(ArgumentError, "Invalid value #{orig.inspect} given for argument #{key.inspect}", caller)
362
+ end
363
+ val
364
+ end
365
+ end
366
+
367
+ # {#transform} allows you to lift an arbitrary code block into an
368
+ # `UnboundMethod`.
369
+ #
370
+ # You pass {#transform} a block which will be invoked each time the returned
371
+ # `UnboundMethod` is called. Within the block, you have access to
372
+ #
373
+ # - `self` and the instance variables of the bound instance
374
+ # - the keyword associated with the argument
375
+ # - the hash of options defined thus far
376
+ # - the original argument value (useful if an earlier transformation `nil`'ed it out)
377
+ # - the block bound to the method invocation
378
+ #
379
+ # The return value of the `UnboundMethod` will be completely determined by the
380
+ # return value of the block bound to the call of {#transform}.
381
+ #
382
+ # ```ruby
383
+ # require 'date'
384
+ # class Timer
385
+ # include Flexibility
386
+ #
387
+ # to_epoch = transform do |t|
388
+ # case t
389
+ # when String ; DateTime.parse(t).to_time.to_i
390
+ # when DateTime ; t.to_time.to_i
391
+ # else ; t.to_i if t.respond_to? :to_i
392
+ # end
393
+ # end
394
+ #
395
+ # define( :elapsed,
396
+ # start: to_epoch,
397
+ # stop: to_epoch
398
+ # ) do |start, stop, _|
399
+ # stop - start
400
+ # end
401
+ # end
402
+ # ```
403
+ #
404
+ # irb> timer = Timer.new
405
+ # irb> timer.elapsed "1984-06-07", "1989-06-16"
406
+ # => 158544000
407
+ # irb> (timer.elapsed DateTime.now, (DateTime.now + 365)) / 60
408
+ # => 525600
409
+ #
410
+ # And just to show how you can access instance variables,
411
+ # earlier parameters, and the bound block with {#transform}...
412
+ #
413
+ # ```ruby
414
+ # class Silly
415
+ # include Flexibility
416
+ #
417
+ # def initialize base
418
+ # @base = base
419
+ # end
420
+ #
421
+ # define( :tag_with_base,
422
+ # fst: transform { |x,&blk| [x, blk[@base] ] },
423
+ # snd: transform { |x,_,opts| [x, opts[:fst].last] }
424
+ # ) { |opts| opts }
425
+ # end
426
+ # ```
427
+ #
428
+ # irb> silly = Silly.new( "base value" )
429
+ # irb> silly.tag_with_base( fst: 3, snd: "hi" ) { |msg| puts msg ; msg.length }
430
+ # base value
431
+ # => { fst: [ 3, 10 ], snd: [ "hi", 10 ] }
432
+ #
433
+ # ----
434
+ #
435
+ # Note that the `yield` keyword inside the block bound to {#transform} won't be
436
+ # able to access the block bound to the method invocation, as `yield` is
437
+ # lexically scoped (like a local variable).
438
+ #
439
+ # ```ruby
440
+ # module YieldExample
441
+ # def self.create
442
+ # Class.new do
443
+ # include Flexibility
444
+ # define( :run,
445
+ # using_yield: transform { |val| yield(val) },
446
+ # using_block: transform { |val,&blk| blk[val] }
447
+ # ) { |opts| opts }
448
+ # end.new
449
+ # end
450
+ # end
451
+ # ```
452
+ #
453
+ # irb> YieldExample.create { |val| [:class_creation, val] }.run(1,2) { |val| [ :method_invocation, val] }
454
+ # => { using_yield: [:class_creation, 1], using_block: [:method_invocation,2] }
455
+ #
456
+ # ----
457
+ #
458
+ # @yield
459
+ # The block bound to {#transform} receives the following parameters when
460
+ # called by a method created with {#define}:
461
+ # @yieldparam val [Object] the value of the option currently being processed
462
+ # @yieldparam key [Symbol] the key for the option currently being processed
463
+ # @yieldparam opts [Hash] the options hash thus far
464
+ # @yieldparam initial [Object] the original value passed to the method for this option
465
+ # @yieldparam &blk [Proc] the block passed to the method
466
+ # @yieldparam self [keyword] bound to the same instance that the method is invoked on
467
+ # @yieldreturn
468
+ # value for returned `UnboundMethod` to return
469
+ # @return [UnboundMethod(val,key,opts,initial,&blk)]
470
+ # `UnboundMethod` created from block bound to {#transform}
471
+ # @see #define
472
+ def transform(&blk)
473
+ Flexibility::create_unbound_method(self, &blk)
474
+ end
475
+
476
+ # @!endgroup
477
+
478
+ # {#define} lets you define methods that can be called with either
479
+ #
480
+ # - positional arguments
481
+ # - keyword arguments
482
+ # - a mix of positional and keyword arguments
483
+ #
484
+ # It takes a `method_name`, an `Hash` using the argument keywords as keys,
485
+ # and a block defining the method body.
486
+ #
487
+ # For example
488
+ #
489
+ # ```ruby
490
+ # class Example
491
+ # include Flexibility
492
+ #
493
+ # define( :run,
494
+ # a: [],
495
+ # b: [],
496
+ # c: []
497
+ # ) do |opts|
498
+ # opts.each { |k,v| puts "#{k}: #{v.inspect}" }
499
+ # end
500
+ #
501
+ # end
502
+ # ```
503
+ #
504
+ # irb> ex = Example.new
505
+ # irb> ex.run( 1, 2, 3 ) # all positional arguments
506
+ # a: 1
507
+ # b: 2
508
+ # c: 3
509
+ # irb> ex.run( c:1, a:2, b:3, d: 0 ) # all keyword arguments
510
+ # a: 2
511
+ # b: 3
512
+ # c: 1
513
+ # d: 0
514
+ # irb> ex.run( 7, 9, d: 18, c:11 ) # mixed keyword and positional arguments
515
+ # a: 7
516
+ # b: 9
517
+ # c: 11
518
+ # d: 18
519
+ #
520
+ # Positional arguments will override keyword arguments if both are given
521
+ #
522
+ # irb> ex.run( 10, 20, 30, a: 1, b: 2, c: 3 )
523
+ # a: 10
524
+ # b: 20
525
+ # c: 30
526
+ #
527
+ # By default, `nil` or unspecified values won't appear in the options hash
528
+ # given to the method body.
529
+ #
530
+ # irb> ex.run( nil, a: 2, c: 3 )
531
+ # c: 3
532
+ #
533
+ # You can use as many keyword arguments as you like, but calling the method
534
+ # with extra positional arguments will cause the method to raise an exception
535
+ #
536
+ # irb> ex.run( 1, 2, 3, 4 )
537
+ # !> ArgumentError: Got 4 arguments, but only know how to handle 3
538
+ #
539
+ # ----
540
+ #
541
+ # {#define} also lets you decide whether the method body receives the arguments
542
+ #
543
+ # - in a Hash
544
+ # - as a mix of positional arguments and a trailing hash
545
+ #
546
+ # It does this by inspecting the arity of the block that defines the method
547
+ # body. A block that takes `N+1` arguments will be provided with `N`
548
+ # positional arguments. The final argument to the block is always a hash of
549
+ # options.
550
+ #
551
+ # For example:
552
+ #
553
+ # ```ruby
554
+ # class Example
555
+ # include Flexibility
556
+ #
557
+ # define( :run,
558
+ # a: [],
559
+ # b: [],
560
+ # c: []
561
+ # ) do |a,b,opts|
562
+ # puts "a = #{a.inspect}"
563
+ # puts "b = #{b.inspect}"
564
+ # puts "opts = #{opts.inspect}"
565
+ # opts.length
566
+ # end
567
+ # end
568
+ # ```
569
+ #
570
+ # irb> ex.run( 1, 2, 3 )
571
+ # a = 1
572
+ # b = 2
573
+ # opts = {:c=>3}
574
+ # irb> ex.run( a:1, b:2, c:3, d:4 )
575
+ # a = 1
576
+ # b = 2
577
+ # opts = {:c=>3, :d=>4}
578
+ #
579
+ # If the method body takes too many arguments (more than the number of
580
+ # keywords plus one for the options hash), then {#define} will raise an error
581
+ # instead of creating the method, since it lacks keywords to use to refer to
582
+ # those extra arguments
583
+ #
584
+ # irb> Class.new { include Flexibility ; define(:ex) { |a,b,c,opts| } }
585
+ # !> ArgumentError: More positional arguments in method body than specified in expected arguments
586
+ #
587
+ # Currently, it's also an error to give {#define} a method body that uses a
588
+ # splat (`*`) to capture a variable number of arguments:
589
+ #
590
+ # irb> Class.new { include Flexibility ; define(:ex) { |*args,opts| } }
591
+ # !> NotImplementedError: Flexibility doesn't support splats in method definitions yet, sorry!
592
+ #
593
+ # ----
594
+ #
595
+ # {#define} also lets you specify, along with each keyword, a sequence of
596
+ # UnboundMethod callbacks to be run on the argument given for that keyword on
597
+ # each run of the generated method.
598
+ #
599
+ # When run, these callbacks will be passed:
600
+ #
601
+ # - the current value of the given argument
602
+ # - the keyword associated with the given argument
603
+ # - the hash of options generated thus far
604
+ # - the original value of the given argument
605
+ # - any block passed to this invocation of the generated method
606
+ #
607
+ # The callback will also have its value of `self` bound to the same instance
608
+ # running the generated method.
609
+ #
610
+ # ```ruby
611
+ # class IntParser
612
+ # include Flexibility
613
+ #
614
+ # def initialize base
615
+ # @base = base
616
+ # end
617
+ #
618
+ # def parse arg
619
+ # arg.to_i(@base)
620
+ # end
621
+ #
622
+ # define(:parse_both,
623
+ # a: [ instance_method(:parse) ],
624
+ # b: [ instance_method(:parse) ]
625
+ # ) do |opts|
626
+ # opts
627
+ # end
628
+ # end
629
+ # ```
630
+ #
631
+ # irb> p16 = IntParser.new(16)
632
+ # irb> p32 = IntParser.new(32)
633
+ # irb> p16.parse_both *%w{ ff 11 }
634
+ # => { a: 255, b: 17 }
635
+ # irb> p32.parse_both *%w{ ff 11 }
636
+ # => { a: 495, b: 33 }
637
+ #
638
+ # If you pass multiple callbacks, they are executed in sequence, with the
639
+ # result of one callback being fed to the next:
640
+ #
641
+ # ```ruby
642
+ # class IntParser
643
+ # #...
644
+ # def increment num
645
+ # num + 1
646
+ # end
647
+ #
648
+ # def decrement num
649
+ # num - 1
650
+ # end
651
+ #
652
+ # def format arg
653
+ # arg.to_s(@base)
654
+ # end
655
+ #
656
+ # define(:parse_change_and_format_both,
657
+ # a: [ instance_method(:parse), instance_method(:increment), instance_method(:format) ],
658
+ # b: [ instance_method(:parse), instance_method(:decrement), instance_method(:format) ],
659
+ # ) do |opts|
660
+ # opts
661
+ # end
662
+ # end
663
+ # ```
664
+ #
665
+ # irb> p16.parse_change_and_format_both *%w{ ff 11 }
666
+ # => { a: "100", b: "10" }
667
+ # irb> p32.parse_change_and_format_both *%w{ ff 11 }
668
+ # => { a: "fg", b: "10" }
669
+ #
670
+ # Rather than defining one-off instance methods like `IntParser#increment` and
671
+ # `IntParser#decrement`, you can use the {#default}, {#required},
672
+ # {#transform}, and {#validate} methods provided by `Flexibility` to construct
673
+ # `UnboundMethod` callbacks:
674
+ #
675
+ # ```ruby
676
+ # class IntParser
677
+ # #...
678
+ # parse = instance_method(:parse)
679
+ # format = instance_method(:format)
680
+ #
681
+ # parsable = validate do |s|
682
+ # _0 = '0'.ord
683
+ # _9 = _0 + [@base, 10].min - 1
684
+ # _a = 'a'.ord
685
+ # _z = _a + [@base - 10, 26].min - 1
686
+ # _A = 'A'.ord
687
+ # _Z = _A + [@base - 10, 26].min - 1
688
+ # s.chars.all? do |c|
689
+ # n = c.ord
690
+ # [ _0 <= n && n <= _9,
691
+ # _a <= n && n <= _z,
692
+ # _A <= n && n <= _Z,
693
+ # ].any?
694
+ # end
695
+ # end
696
+ #
697
+ # define(:parse_change_and_format_both,
698
+ # a: [ parsable, parse, transform { |i| i + 1 }, format ],
699
+ # b: [ parsable, parse, transform { |i| i - 1 }, format ],
700
+ # ) do |opts|
701
+ # opts
702
+ # end
703
+ # end
704
+ # ```
705
+ #
706
+ # irb> p16.parse_change_and_format_both *%w{ ff 11 }
707
+ # => { a: "100", b: "10" }
708
+ # irb> p16.parse_change_and_format_both *%w{ gg 11 }
709
+ # !> ArgumentError: Invalid value "gg" given for argument :a
710
+ # irb> p32.parse_change_and_format_both *%w{ gg 11 }
711
+ # => { a: "gh", b: "10" }
712
+ #
713
+ # To make it even simpler, you can also use a `Proc`, `Symbol` or
714
+ # anything else that responds to `#to_proc` for a callback as well.
715
+ #
716
+ # ```ruby
717
+ # class Item
718
+ # def initialize foo, bar
719
+ # @foo, @bar = foo, bar
720
+ # end
721
+ # def foo(*args)
722
+ # puts "running foo! with #{args.inspect}"
723
+ # @foo
724
+ # end
725
+ # def bar(*args)
726
+ # puts "running bar! with #{args.inspect}"
727
+ # @bar
728
+ # end
729
+ # def inspect
730
+ # "#<Item @foo=#@foo @bar=#@bar>"
731
+ # end
732
+ # end
733
+ #
734
+ # class Example
735
+ # include Flexibility
736
+ # def initialize tag
737
+ # @tag = tag
738
+ # end
739
+ #
740
+ # define(:run,
741
+ # a: [ :foo, proc { |n,&blk| blk[ @tag, n ] } ],
742
+ # b: [ :bar, proc { |n,&blk| blk[ @tag, n ] } ]
743
+ # ) do |opts|
744
+ # opts
745
+ # end
746
+ # end
747
+ # ```
748
+ #
749
+ # irb> item = Item.new( "left", "right" )
750
+ # irb> ex = Example.new( "popcorn" )
751
+ # irb> ex.run( a: item, b: item ) { |tag, val| puts "running block with tag=#{tag} val=#{val}" ; tag + val }
752
+ # running foo! with [:a, {}, #<Item @foo=left @bar=right>]
753
+ # running block with tag=popcorn val=left
754
+ # running bar! with [:b, {:a=>"popcornleft"}, #<Item @foo=left @bar=right>]
755
+ # running block with tag=popcorn val=right
756
+ # => { a: "popcornleft", b: "popcornright" }
757
+ #
758
+ # Note how, as mentioned earler, we can access the bound block and prior
759
+ # options within the callback.
760
+ #
761
+ # In addition, if you only need a single callback for an argument, you don't
762
+ # have to wrap it in an array:
763
+ #
764
+ # ```ruby
765
+ # class Example
766
+ #
767
+ # def initialize(min)
768
+ # @min = min
769
+ # end
770
+ #
771
+ # define(:run,
772
+ # foo: required,
773
+ # bar: validate { |bar| bar >= @min },
774
+ # baz: default { |_,opts| opts[:bar] },
775
+ # quux: transform { |val,key| val[key] }
776
+ # ) do |opts|
777
+ # opts
778
+ # end
779
+ # end
780
+ # ```
781
+ #
782
+ # irb> ex = Example.new(10)
783
+ # irb> ex.run
784
+ # !> ArgumentError: Required argument :foo not given
785
+ # irb> ex.run 100, 0
786
+ # !> ArgumentError: Invalid value 0 given for argument :bar
787
+ # irb> ex.run 100, 17, quux: { quux: 5 }
788
+ # => { foo: 100, bar: 17, baz: 17, quux: 5 }
789
+ #
790
+ # ----
791
+ #
792
+ # The method body given to {#define} can receive the block bound to the
793
+ # method call at runtime using the standard `&` prefix:
794
+ #
795
+ # ```ruby
796
+ # class AmpersandExample
797
+ # include Flexibility
798
+ #
799
+ # define(:run) do |&blk|
800
+ # (1..4).each(&blk)
801
+ # end
802
+ # end
803
+ # ```
804
+ #
805
+ # irb> AmpersandExample.new.run { |i| puts i }
806
+ # 1
807
+ # 2
808
+ # 3
809
+ # 4
810
+ # => 1..4
811
+ #
812
+ # Note, however, that the `yield` keyword inside the method body won't be able
813
+ # to access the block bound to the method invocation, as `yield` is lexically
814
+ # scoped (like a local variable).
815
+ #
816
+ # ```ruby
817
+ # module YieldExample
818
+ # def self.create
819
+ # Class.new do
820
+ # include Flexibility
821
+ # define( :run ) do |&blk|
822
+ # blk.call :using_block
823
+ # yield :using_yield
824
+ # end
825
+ # end
826
+ # end
827
+ # end
828
+ # ```
829
+ #
830
+ # irb> klass = YieldExample.create { |x| puts "class creation block got #{x}" }
831
+ # irb> instance = klass.new
832
+ # irb> instance.run { |x| puts "method invocation block got #{x}" }
833
+ # method invocation block got using_block
834
+ # class creation block got using_yield
835
+ #
836
+ # ----
837
+ #
838
+ # @param method_name [ Symbol ]
839
+ # the name of the method to create
840
+ # @param expected [ { Symbol => [ UnboundMethod(val,key,opts,initial,&blk) ] } ]
841
+ # an ordered `Hash` of keywords for each argument, associated with an
842
+ # `Array` of `UnboundMethod` callbacks to call on each argument value when
843
+ # the defined method is run.
844
+ #
845
+ # In addition to `UnboundMethod`, qnything that responds to `#to_proc` may
846
+ # be used for a callback, and a single callback can be used in place of an
847
+ # `Array` of one callback.
848
+ # @yield
849
+ # The result of running all the callbacks on each parameter for a given
850
+ # call to the defined method.
851
+ #
852
+ # If the block bound to `#define` takes `N+1` parameters, then the first `N`
853
+ # will be bound to the values of the first `N` keywords. The last
854
+ # parameter given to the block will contain a `Hash` mapping the remaining
855
+ # keywords to their values.
856
+ #
857
+ # @raise [ArgumentError]
858
+ # If the method body takes `N+1` arguments, but fewer than `N` keywords are
859
+ # given in the `expected` parameter, then {#define} does not define the
860
+ # method, and instead raises an error.
861
+ #
862
+ # @raise [NotImplementedError]
863
+ # If the method body uses a splat (`*`) to capture a variable number of arguments,
864
+ # {#define} raises an error, as `Flexibility` has not determined how best to
865
+ # handle that case yet. Sorry. Bother the developer if you want that
866
+ # changed.
867
+ #
868
+ # @see #default
869
+ # @see #required
870
+ # @see #validate
871
+ # @see #transform
872
+ def define method_name, expected={}, &method_body
873
+ if method_body.arity < 0
874
+ raise(NotImplementedError, "Flexibility doesn't support splats in method definitions yet, sorry!", caller)
875
+ elsif method_body.arity > expected.length + 1
876
+ raise(ArgumentError, "More positional arguments in method body than specified in expected arguments", caller)
877
+ end
878
+
879
+ # create an UnboundMethod from method_body so we can
880
+ # 1. set `self`
881
+ # 2. pass it arguments
882
+ # 3. pass it a block
883
+ #
884
+ # `instance_eval` only allows us to do (1), whereas `instance_exec` only
885
+ # allows (1) and (2), and `call` only allows (2) and (3).
886
+ method_um = Flexibility::create_unbound_method(self, &method_body)
887
+
888
+ # similarly, create UnboundMethods from the callbacks
889
+ expected_ums = {}
890
+
891
+ expected.each do |key, cbs|
892
+ # normalize a single callback to a collection
893
+ cbs = [cbs] unless cbs.respond_to? :inject
894
+
895
+ expected_ums[key] = cbs.map.with_index do |cb, index|
896
+ if UnboundMethod === cb
897
+ cb
898
+ elsif cb.respond_to? :to_proc
899
+ Flexibility::create_unbound_method(self, &cb)
900
+ else
901
+ raise(ArgumentError, "Unrecognized expectation #{cb.inspect} for #{key.inspect}, expecting an UnboundMethod or something that responds to #to_proc", caller)
902
+ end
903
+ end
904
+ end
905
+
906
+ # assume all but the last block argument should capture positional
907
+ # arguments
908
+ keys = expected_ums.keys[ 0 ... method_um.arity - 1]
909
+
910
+ # interpret user arguments using #options, then pass them to the method
911
+ # body
912
+ define_method(method_name) do |*given, &blk|
913
+
914
+ # let the caller bundle arguments in a trailing Hash
915
+ trailing_opts = Hash === given.last ? given.pop : {}
916
+ unless expected_ums.length >= given.length
917
+ raise(ArgumentError, "Got #{given.length} arguments, but only know how to handle #{expected_ums.length}", caller)
918
+ end
919
+
920
+ opts = {}
921
+ expected_ums.each.with_index do |(key, ums), i|
922
+ # check positional argument for value first, then default to trailing options
923
+ initial = i < given.length ? given[i] : trailing_opts[key]
924
+
925
+ # run every callback, threading the results through each
926
+ final = ums.inject(initial) do |val, um|
927
+ Flexibility::run_unbound_method(um, self, val, key, opts, initial, &blk)
928
+ end
929
+
930
+ opts[key] = final unless final.nil?
931
+ end
932
+
933
+ # copy remaining options
934
+ (trailing_opts.keys - expected_ums.keys).each do |key|
935
+ opts[key] = trailing_opts[key]
936
+ end
937
+
938
+ Flexibility::run_unbound_method(
939
+ method_um,
940
+ self,
941
+ *keys.map { |key| opts.delete key }.push( opts ).take( method_um.arity ),
942
+ &blk
943
+ )
944
+ end
945
+ end
946
+
947
+ # When included, `Flexibility` adds all its instance methods as private class
948
+ # methods of the including class:
949
+ #
950
+ # irb> c = Class.new
951
+ # irb> before = c.private_methods
952
+ # irb> c.class_eval { include Flexibility }
953
+ # irb> c.private_methods - before
954
+ # => [ :default, :required, :validate, :transform, :define ]
955
+ #
956
+ # ----
957
+ #
958
+ # @param target [Module] the class or module that included Flexibility
959
+ # @see Module#include
960
+ def self.append_features(target)
961
+ class<<target
962
+ Flexibility.instance_methods.each do |name|
963
+ define_method(name, Flexibility.instance_method(name))
964
+ private name
965
+ end
966
+ end
967
+ end
968
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: Flexibility
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Noah Luck Easterly
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: yard
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.8'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.8'
41
+ description: |2
42
+ Flexibility is a mix-in for ruby classes that allows you to easily
43
+ define methods that can take a mixture of positional and keyword
44
+ arguments.
45
+ email: noah.easterly@gmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files:
49
+ - README.md
50
+ files:
51
+ - README.md
52
+ - lib/flexibility.rb
53
+ homepage: https://github.com/rampion/Flexibility
54
+ licenses:
55
+ - Unlicense
56
+ metadata: {}
57
+ post_install_message:
58
+ rdoc_options:
59
+ - "--markup"
60
+ - markdown
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 2.0.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubyforge_project:
75
+ rubygems_version: 2.4.3
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: include Flexibility; accept keywords or positional arguments to methods
79
+ test_files: []
80
+ has_rdoc: yard