Flexibility 1.0.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.
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