Flexibility 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +119 -0
- data/lib/flexibility.rb +968 -0
- 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
|
+
|
data/lib/flexibility.rb
ADDED
@@ -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
|