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