myrrha 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.
- data/CHANGELOG.md +5 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +41 -0
- data/LICENCE.md +22 -0
- data/Manifest.txt +13 -0
- data/README.md +337 -0
- data/Rakefile +23 -0
- data/examples/String#toXXX.rb +40 -0
- data/examples/coerce.rb +26 -0
- data/examples/coerce_foo.rb +12 -0
- data/examples/coerce_foo2.rb +17 -0
- data/examples/coerce_foo3.rb +23 -0
- data/examples/coerce_intro.rb +10 -0
- data/examples/coerce_noext.rb +6 -0
- data/examples/examples_helper.rb +17 -0
- data/examples/friendly_but_safe_api.rb +73 -0
- data/examples/to_ruby_literal.rb +16 -0
- data/examples/to_ruby_literal_foo.rb +14 -0
- data/examples/to_ruby_literal_foo2.rb +17 -0
- data/examples/to_ruby_literal_foo3.rb +21 -0
- data/examples/to_ruby_literal_noext.rb +5 -0
- data/lib/myrrha/coerce.rb +93 -0
- data/lib/myrrha/loader.rb +0 -0
- data/lib/myrrha/to_ruby_literal.rb +76 -0
- data/lib/myrrha/version.rb +14 -0
- data/lib/myrrha/with_core_ext.rb +2 -0
- data/lib/myrrha.rb +295 -0
- data/myrrha.gemspec +191 -0
- data/myrrha.noespec +37 -0
- data/spec/coercions/test_append.rb +11 -0
- data/spec/coercions/test_belongs_to.rb +29 -0
- data/spec/coercions/test_convert.rb +23 -0
- data/spec/coercions/test_dup.rb +21 -0
- data/spec/coercions/test_subdomain.rb +11 -0
- data/spec/shared/a_value.rb +33 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/test_assumptions.rb +18 -0
- data/spec/test_coerce.rb +119 -0
- data/spec/test_myrrha.rb +87 -0
- data/spec/test_to_ruby_literal.rb +19 -0
- data/spec/test_value.rb +6 -0
- data/tasks/debug_mail.rake +78 -0
- data/tasks/debug_mail.txt +13 -0
- data/tasks/examples.rake +13 -0
- data/tasks/gem.rake +68 -0
- data/tasks/spec_test.rake +79 -0
- data/tasks/unit_test.rake +77 -0
- data/tasks/yard.rake +51 -0
- metadata +195 -0
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
myrrha (1.0.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
bluecloth (2.0.11)
|
10
|
+
diff-lcs (1.1.2)
|
11
|
+
highline (1.6.2)
|
12
|
+
noe (1.3.0)
|
13
|
+
highline (~> 1.6.0)
|
14
|
+
quickl (~> 0.2.0)
|
15
|
+
wlang (~> 0.10.1)
|
16
|
+
quickl (0.2.2)
|
17
|
+
rake (0.9.2)
|
18
|
+
rspec (2.6.0)
|
19
|
+
rspec-core (~> 2.6.0)
|
20
|
+
rspec-expectations (~> 2.6.0)
|
21
|
+
rspec-mocks (~> 2.6.0)
|
22
|
+
rspec-core (2.6.4)
|
23
|
+
rspec-expectations (2.6.0)
|
24
|
+
diff-lcs (~> 1.1.2)
|
25
|
+
rspec-mocks (2.6.0)
|
26
|
+
wlang (0.10.2)
|
27
|
+
yard (0.7.2)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
java
|
31
|
+
ruby
|
32
|
+
|
33
|
+
DEPENDENCIES
|
34
|
+
bluecloth (~> 2.0.9)
|
35
|
+
bundler (~> 1.0)
|
36
|
+
myrrha!
|
37
|
+
noe (~> 1.3.0)
|
38
|
+
rake (~> 0.9.2)
|
39
|
+
rspec (~> 2.6.0)
|
40
|
+
wlang (~> 0.10.1)
|
41
|
+
yard (~> 0.7.2)
|
data/LICENCE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# The MIT Licence
|
2
|
+
|
3
|
+
Copyright (c) 2011 - Bernard Lambeau
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest.txt
ADDED
data/README.md
ADDED
@@ -0,0 +1,337 @@
|
|
1
|
+
# Myrrha
|
2
|
+
|
3
|
+
## Description
|
4
|
+
|
5
|
+
Myrrha provides the coercion framework which is missing to Ruby, IMHO. Coercions
|
6
|
+
are simply defined as a set of rules for converting values from source to target
|
7
|
+
domains (in an abstract sense). As a typical and useful example, it comes bundled
|
8
|
+
with a coerce() method providing a unique entry point for converting a string to
|
9
|
+
a numeric, a boolean, a date, a time, an URI, and so on.
|
10
|
+
|
11
|
+
### Install
|
12
|
+
|
13
|
+
% [sudo] gem install myrrha
|
14
|
+
|
15
|
+
### Bundler & Require
|
16
|
+
|
17
|
+
# Bug fixes (tiny) do not even add new default rules to coerce and
|
18
|
+
# to\_ruby\_literal. Minor version can, which could break your code.
|
19
|
+
# Therefore, please always use:
|
20
|
+
gem "alf", "~> 1.0.0"
|
21
|
+
|
22
|
+
## Links
|
23
|
+
|
24
|
+
* http://rubydoc.info/github/blambeau/myrrha/master/frames (read this file there!)
|
25
|
+
* http://github.com/blambeau/myrrha (source code)
|
26
|
+
* http://rubygems.org/gems/myrrha (download)
|
27
|
+
|
28
|
+
## The missing <code>coerce()</code>
|
29
|
+
|
30
|
+
Myrrha.coerce(:anything, Domain)
|
31
|
+
coerce(:anything, Domain) # with core extensions
|
32
|
+
|
33
|
+
### What for?
|
34
|
+
|
35
|
+
Having a single entry point for coercing values from one data-type (typically
|
36
|
+
a String) to another one is very useful. Unfortunately, Ruby does not provide
|
37
|
+
such a unique entry point... Thanks to Myrrah, the following scenario is
|
38
|
+
possible and even straightforward:
|
39
|
+
|
40
|
+
require 'myrrha/with_core_ext'
|
41
|
+
require 'myrrha/coerce'
|
42
|
+
require 'date'
|
43
|
+
|
44
|
+
values = ["12", "true", "2011-07-20"]
|
45
|
+
types = [Integer, Boolean, Date]
|
46
|
+
values.zip(types).collect do |value,domain|
|
47
|
+
coerce(value, domain)
|
48
|
+
end
|
49
|
+
# => [12, true, #<Date: 2011-07-20 (...)>]
|
50
|
+
|
51
|
+
### Example
|
52
|
+
|
53
|
+
require 'myrrha/with_core_ext'
|
54
|
+
require 'myrrha/coerce'
|
55
|
+
|
56
|
+
# it works on numerics
|
57
|
+
coerce("12", Integer) # => 12
|
58
|
+
coerce("12.0", Float) # => 12.0
|
59
|
+
|
60
|
+
# but also on regexp (through Regexp.compile)
|
61
|
+
coerce("[a-z]+", Regexp) # => /[a-z]+/
|
62
|
+
|
63
|
+
# and, yes, on Boolean (sorry Matz!)
|
64
|
+
coerce("true", Boolean) # => true
|
65
|
+
coerce("false", Boolean) # => false
|
66
|
+
|
67
|
+
# and on date and time (through Date/Time.parse)
|
68
|
+
require 'date'
|
69
|
+
require 'time'
|
70
|
+
coerce("2011-07-20", Date) # => #<Date: 2011-07-20 (4911525/2,0,2299161)>
|
71
|
+
coerce("2011-07-20 10:57", Time) # => 2011-07-20 10:57:00 +0200
|
72
|
+
|
73
|
+
# why not on URI?
|
74
|
+
require 'uri'
|
75
|
+
coerce('http://google.com', URI) # => #<URI::HTTP:0x8281ce0 URL:http://google.com>
|
76
|
+
|
77
|
+
# on nil, it always returns nil
|
78
|
+
coerce(nil, Integer) # => nil
|
79
|
+
|
80
|
+
### No core extension? no problem!
|
81
|
+
|
82
|
+
require 'myrrha/coerce'
|
83
|
+
|
84
|
+
Myrrha.coerce("12", Integer) # => 12
|
85
|
+
Myrrha.coerce("12.0", Float) # => 12.0
|
86
|
+
|
87
|
+
Myrrha.coerce("true", Myrrha::Boolean) # => true
|
88
|
+
# [... and so on ...]
|
89
|
+
|
90
|
+
### Adding your own coercions
|
91
|
+
|
92
|
+
The easiest way to add additional coercions is to implement a <code>coerce</code>
|
93
|
+
method on you class; it will be used in priority.
|
94
|
+
|
95
|
+
class Foo
|
96
|
+
def initialize(arg)
|
97
|
+
@arg = arg
|
98
|
+
end
|
99
|
+
def self.coerce(arg)
|
100
|
+
Foo.new(arg)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
Myrrha.coerce(:hello, Foo)
|
105
|
+
# => #<Foo:0x869eee0 @arg=:hello>
|
106
|
+
|
107
|
+
If <code>Foo</code> is not your code and you don't want to make core extensions
|
108
|
+
by adding a <code>coerce</code> class method, you can simply add new rules to
|
109
|
+
Myrrha itself:
|
110
|
+
|
111
|
+
Myrrha::Coerce.append do |r|
|
112
|
+
r.coercion(Symbol, Foo) do |value, _|
|
113
|
+
Foo.new(value)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
Myrrha.coerce(:hello, Foo)
|
118
|
+
# => #<Foo:0x8866f84 @arg=:hello>
|
119
|
+
|
120
|
+
Now, doing so, the new coercion rule will be shared with all Myrrha users, which
|
121
|
+
might be intrusive. Why not using your own set of coercion rules?
|
122
|
+
|
123
|
+
MyRules = Myrrha::Coerce.dup.append do |r|
|
124
|
+
r.coercion(Symbol, Foo) do |value, _|
|
125
|
+
Foo.new(value)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Myrrha.coerce is actually a shortcut for:
|
130
|
+
Myrrha::Coerce.apply(:hello, Foo)
|
131
|
+
# => Myrrha::Error: Unable to coerce `hello` to Foo
|
132
|
+
|
133
|
+
MyRules.apply(:hello, Foo)
|
134
|
+
# => #<Foo:0x8b7d254 @arg=:hello>
|
135
|
+
|
136
|
+
## The missing <code>to\_ruby\_literal()</code>
|
137
|
+
|
138
|
+
Myrrha.to_ruby_literal([:anything])
|
139
|
+
[:anything].to_ruby_literal # with core extensions
|
140
|
+
|
141
|
+
### What for?
|
142
|
+
|
143
|
+
<code>Object#to\_ruby\_literal</code> has a very simple specification. Given an
|
144
|
+
object o that can be considered as a true _value_, the result of
|
145
|
+
<code>o.to\_ruby\_literal</code> must be such that the following invariant
|
146
|
+
holds:
|
147
|
+
|
148
|
+
Kernel.eval(o.to_ruby_literal) == o
|
149
|
+
|
150
|
+
That is, parsing & evaluating the literal yields the same value. When generating
|
151
|
+
(human-readable) ruby code, having a unique entry point that respects the
|
152
|
+
specification is very useful.
|
153
|
+
|
154
|
+
For almost all ruby classes, but not all, using o.inspect respects the
|
155
|
+
invariant. For example, the following is true:
|
156
|
+
|
157
|
+
Kernel.eval("hello".inspect) == "hello" # => true
|
158
|
+
Kernel.eval([1, 2, 3].inspect) == [1, 2, 3] # => true
|
159
|
+
Kernel.eval({:key => :value}.inspect) == {:key => :value} # => true
|
160
|
+
# => true
|
161
|
+
|
162
|
+
Unfortunately, this is not always the case:
|
163
|
+
|
164
|
+
Kernel.eval(Date.today.inspect) == Date.today
|
165
|
+
# => false
|
166
|
+
# => because Date.today.inspect yields "#<Date: 2011-07-20 ...", which is a comment
|
167
|
+
|
168
|
+
### Example
|
169
|
+
|
170
|
+
Myrrha implements a very simple set of rules for implementing
|
171
|
+
<code>Object#to\_ruby\_literal</code> that works:
|
172
|
+
|
173
|
+
require 'date'
|
174
|
+
require 'myrrha/with_core_ext'
|
175
|
+
require 'myrrha/to_ruby_literal'
|
176
|
+
|
177
|
+
1.to_ruby_literal # => "1"
|
178
|
+
Date.today.to_ruby_literal # => "Marshal.load('...')"
|
179
|
+
["hello", Date.today].to_ruby_literal # => "['hello', Marshal.load('...')]"
|
180
|
+
|
181
|
+
Myrrha implements a best-effort strategy to return a human readable string. It
|
182
|
+
simply fallbacks to <code>Marshal.load(...)</code> when the strategy fails:
|
183
|
+
|
184
|
+
(1..10).to_ruby_literal # => "1..10"
|
185
|
+
|
186
|
+
today = Date.today
|
187
|
+
(today..today+1).to_ruby_literal # => "Marshal.load('...')"
|
188
|
+
|
189
|
+
### No core extension? no problem!
|
190
|
+
|
191
|
+
require 'date'
|
192
|
+
require 'myrrha/to_ruby_literal'
|
193
|
+
|
194
|
+
Myrrha.to_ruby_literal(1) # => 1
|
195
|
+
Myrrha.to_ruby_literal(Date.today) # => Marshal.load("...")
|
196
|
+
# [... and so on ...]
|
197
|
+
|
198
|
+
### Adding your own rules
|
199
|
+
|
200
|
+
The easiest way is simply to override <code>to\_ruby\_literal</code> in your
|
201
|
+
class
|
202
|
+
|
203
|
+
class Foo
|
204
|
+
attr_reader :arg
|
205
|
+
def initialize(arg)
|
206
|
+
@arg = arg
|
207
|
+
end
|
208
|
+
def to_ruby_literal
|
209
|
+
"Foo.new(#{arg.inspect})"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
Myrrha.to_ruby_literal(Foo.new(:hello))
|
214
|
+
# => "Foo.new(:hello)"
|
215
|
+
|
216
|
+
As with coerce, contributing your own rule to Myrrha is possible:
|
217
|
+
|
218
|
+
Myrrha::ToRubyLiteral.append do |r|
|
219
|
+
r.coercion(Foo) do |foo, _|
|
220
|
+
"Foo.new(#{foo.arg.inspect})"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
Myrrha.to_ruby_literal(Foo.new(:hello))
|
225
|
+
# => "Foo.new(:hello)"
|
226
|
+
|
227
|
+
And building your own set of rules is possible as well:
|
228
|
+
|
229
|
+
MyRules = Myrrha::ToRubyLiteral.dup.append do |r|
|
230
|
+
r.coercion(Foo) do |foo, _|
|
231
|
+
"Foo.new(#{foo.arg.inspect})"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Myrrha.to_ruby_literal is actually a shortcut for:
|
236
|
+
Myrrha::ToRubyLiteral.apply(Foo.new(:hello))
|
237
|
+
# => "Marshal.load('...')"
|
238
|
+
|
239
|
+
MyRules.apply(Foo.new(:hello))
|
240
|
+
# => "Foo.new(:hello)"
|
241
|
+
|
242
|
+
### Limitation
|
243
|
+
|
244
|
+
As the feature fallbacks to marshaling, everything which is marshalable will
|
245
|
+
work. As usual, <code>to\_ruby\_literal(Proc)</code> won't work.
|
246
|
+
|
247
|
+
## The general coercion framework
|
248
|
+
|
249
|
+
A set of coercion rules can simply be created from scratch as follows:
|
250
|
+
|
251
|
+
Rules = Myrrha.coercions do |r|
|
252
|
+
|
253
|
+
# `upon` rules are tried in priority if PRE holds
|
254
|
+
r.upon(SourceDomain) do |value, requested_domain|
|
255
|
+
|
256
|
+
# PRE: - user wants to coerce `value` to a requested_domain
|
257
|
+
# - belongs_to?(value, SourceDomain)
|
258
|
+
|
259
|
+
# implement the coercion or throw(:newrule)
|
260
|
+
returned_value = something(value)
|
261
|
+
|
262
|
+
# POST: belongs_to?(returned_value, requested_domain)
|
263
|
+
|
264
|
+
end
|
265
|
+
|
266
|
+
# `coercion` rules are then tried in order if PRE holds
|
267
|
+
r.coercion(SourceDomain, TargetDomain) do |value, requested_domain|
|
268
|
+
|
269
|
+
# PRE: - user wants to coerce `value` to a requested_domain
|
270
|
+
# - belongs_to?(value, SourceDomain)
|
271
|
+
# - subdomain?(TargetDomain, requested_domain)
|
272
|
+
|
273
|
+
# implement the coercion or throw(:newrule)
|
274
|
+
returned_value = something(value)
|
275
|
+
|
276
|
+
# POST: returned_value belongs to requested_domain
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
# fallback rules are tried if everything else has failed
|
281
|
+
r.fallback(SourceDomain) do |value, requested_domain|
|
282
|
+
|
283
|
+
# exactly the same as upon rules
|
284
|
+
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
When the user invokes <code>Rules.apply(value, domain)</code> all rules for
|
290
|
+
which PRE holds are executed in order, until one succeed (chain of
|
291
|
+
responsibility design pattern). This means that coercions always execute in
|
292
|
+
<code>O(number of rules)</code>.
|
293
|
+
|
294
|
+
### <code>belongs\_to?</code> and <code>subdomain?</code>
|
295
|
+
|
296
|
+
The pseudo-code given above relies on two main abstractions. Suppose the user
|
297
|
+
makes a call to <code>coerce(value, requested_domain)</code>:
|
298
|
+
|
299
|
+
* <code>belongs\_to?(value, SourceDomain)</code> is true iif
|
300
|
+
* <code>SourceDomain</code> is a <code>Proc</code> of arity 2, and
|
301
|
+
<code>SourceDomain.call(value, requested_domain)</code> yields true
|
302
|
+
* <code>SourceDomain</code> is a <code>Proc</code> of arity 1, and
|
303
|
+
<code>SourceDomain.call(value)</code> yields true
|
304
|
+
* <code>SourceDomain === value</code> yields true
|
305
|
+
|
306
|
+
* <code>subdomain?(SourceDomain,TargetDomain)</code> is true iif
|
307
|
+
* <code>SourceDomain == TargetDomain</code> yields true
|
308
|
+
* SourceDomain and TargetDomain are both classes and the latter is a super
|
309
|
+
class of the former
|
310
|
+
|
311
|
+
### Advanced rule examples
|
312
|
+
|
313
|
+
Rules = Myrrha.coercions do |r|
|
314
|
+
|
315
|
+
# A 'catch-all' upon rule, always fired
|
316
|
+
catch_all = lambda{|v,rd| true}
|
317
|
+
r.upon(catch_all) do |value, requested_domain|
|
318
|
+
if you_can_coerce?(value)
|
319
|
+
# then do it!
|
320
|
+
else
|
321
|
+
throw(:next_rule)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# Delegate every call to the requested domain if it responds to compile
|
326
|
+
compilable = lambda{|v,rd| rd.respond_to?(:compile)}
|
327
|
+
r.upon(compilable) do |value, requested_domain|
|
328
|
+
requested_domain.compile(value)
|
329
|
+
end
|
330
|
+
|
331
|
+
# A fallback strategy if everything else fails
|
332
|
+
r.fallback(Object) do |value, requested_domain|
|
333
|
+
# always fired after everything else
|
334
|
+
# this is your last change, an Myrrha::Error will be raised if you fail
|
335
|
+
end
|
336
|
+
|
337
|
+
end
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
begin
|
2
|
+
gem "bundler", "~> 1.0"
|
3
|
+
require "bundler/setup"
|
4
|
+
rescue LoadError => ex
|
5
|
+
puts ex.message
|
6
|
+
abort "Bundler failed to load, (did you run 'gem install bundler' ?)"
|
7
|
+
end
|
8
|
+
|
9
|
+
# Dynamically load the gem spec
|
10
|
+
$gemspec_file = File.expand_path('../myrrha.gemspec', __FILE__)
|
11
|
+
$gemspec = Kernel.eval(File.read($gemspec_file))
|
12
|
+
|
13
|
+
# We run tests by default
|
14
|
+
task :default => :test
|
15
|
+
|
16
|
+
#
|
17
|
+
# Install all tasks found in tasks folder
|
18
|
+
#
|
19
|
+
# See .rake files there for complete documentation.
|
20
|
+
#
|
21
|
+
Dir["tasks/*.rake"].each do |taskfile|
|
22
|
+
instance_eval File.read(taskfile), taskfile
|
23
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.expand_path('../examples_helper', __FILE__)
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
# In many cases, arguments coming from the user (in console mode, in web,
|
5
|
+
# and so on) are Strings, that must be converted to other types: numerics,
|
6
|
+
# booleans, and so on.
|
7
|
+
#
|
8
|
+
# The String class could be extended with to_Integer, to_Float, to_Date,
|
9
|
+
# to_Time and so on, but this is not a good idea... for obvious reasons.
|
10
|
+
#
|
11
|
+
# With Myrrha, building a set of rules for coercing strings is easy:
|
12
|
+
|
13
|
+
rules = Myrrha.coercions do |r|
|
14
|
+
r.coercion String, Integer, lambda{|s,t| Integer(s) }
|
15
|
+
r.coercion String, Float, lambda{|s,t| Float(s) }
|
16
|
+
r.coercion String, Date, lambda{|s,t| Date.parse(s) }
|
17
|
+
# ... add your own rules here ...
|
18
|
+
end
|
19
|
+
|
20
|
+
# Integers are recognized correctly
|
21
|
+
rules.coerce("12", Integer).should be_a(Integer)
|
22
|
+
rules.coerce("12", Integer).should eq(12)
|
23
|
+
|
24
|
+
# And so are Floats
|
25
|
+
rules.coerce("12.0", Float).should be_a(Float)
|
26
|
+
rules.coerce("12.0", Float).should eq(12.0)
|
27
|
+
|
28
|
+
#
|
29
|
+
# Interestingly, coercion to Numeric works as well. Because
|
30
|
+
# of the order in which the rules are defined, Integer are
|
31
|
+
# return in priority!
|
32
|
+
#
|
33
|
+
rules.coerce("12", Numeric).should be_a(Integer)
|
34
|
+
rules.coerce("12.0", Numeric).should be_a(Float)
|
35
|
+
|
36
|
+
# And dates will work as well
|
37
|
+
rules.coerce("2010-07-20", Date).should be_a(Date)
|
38
|
+
rules.coerce("2010-07-20", Date).should eq(Date.parse("2010-07-20"))
|
39
|
+
|
40
|
+
# Going further? See the set of rules in Myrrha::Ruby
|
data/examples/coerce.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'myrrha/with_core_ext'
|
2
|
+
require 'myrrha/coerce'
|
3
|
+
|
4
|
+
# it works on numerics
|
5
|
+
coerce("12", Integer) # => 12
|
6
|
+
coerce("12.0", Float) # => 12.0
|
7
|
+
|
8
|
+
# but also on regexp (through Regexp.compile)
|
9
|
+
coerce("[a-z]+", Regexp) # => /[a-z]+/
|
10
|
+
|
11
|
+
# and, yes, on Boolean (sorry Matz!)
|
12
|
+
coerce("true", Boolean) # => true
|
13
|
+
coerce("false", Boolean) # => false
|
14
|
+
|
15
|
+
# and on date and time (through Date/Time.parse)
|
16
|
+
require 'date'
|
17
|
+
require 'time'
|
18
|
+
coerce("2011-07-20", Date) # => #<Date: 2011-07-20 (4911525/2,0,2299161)>
|
19
|
+
coerce("2011-07-20 10:57", Time) # => 2011-07-20 10:57:00 +0200
|
20
|
+
|
21
|
+
# why not on URI?
|
22
|
+
require 'uri'
|
23
|
+
coerce('http://google.com', URI) # => #<URI::HTTP:0x8281ce0 URL:http://google.com>
|
24
|
+
|
25
|
+
# on nil, it always returns nil
|
26
|
+
coerce(nil, Integer) # => nil
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'myrrha/coerce'
|
2
|
+
|
3
|
+
class Foo
|
4
|
+
def initialize(arg)
|
5
|
+
@arg = arg
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
Myrrha::Coerce.append do |r|
|
10
|
+
r.coercion(Symbol, Foo) do |value, _|
|
11
|
+
Foo.new(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Myrrha.coerce(:hello, Foo)
|
16
|
+
# => #<Foo:0x8866f84 @arg=:hello>
|
17
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'myrrha/coerce'
|
2
|
+
|
3
|
+
class Foo
|
4
|
+
def initialize(arg)
|
5
|
+
@arg = arg
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
MyRules = Myrrha::Coerce.dup.append do |r|
|
10
|
+
r.coercion(Symbol, Foo) do |value, _|
|
11
|
+
Foo.new(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
begin
|
16
|
+
Myrrha::Coerce.apply(:hello, Foo)
|
17
|
+
raise "Unexpected"
|
18
|
+
rescue Myrrha::Error
|
19
|
+
# => Myrrha::Error: Unable to coerce `hello` to Foo
|
20
|
+
end
|
21
|
+
|
22
|
+
MyRules.apply(:hello, Foo)
|
23
|
+
# => #<Foo:0x8b7d254 @arg=:hello>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'myrrha/with_core_ext'
|
2
|
+
require 'myrrha/coerce'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
values = ["12", "true", "2011-07-20"]
|
6
|
+
types = [Integer, Boolean, Date]
|
7
|
+
values.zip(types).collect do |value,domain|
|
8
|
+
coerce(value, domain)
|
9
|
+
end
|
10
|
+
# => [12, true, #<Date: 2011-07-20 (...)>]
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
require "myrrha"
|
3
|
+
class Object
|
4
|
+
|
5
|
+
def eq(x)
|
6
|
+
lambda{|y| raise("Expected #{x} but was #{y} ") unless x == y}
|
7
|
+
end
|
8
|
+
|
9
|
+
def be_a(clazz)
|
10
|
+
lambda{|y| raise("Expected #{y.inspect} to be a #{clazz}") unless y.is_a?(clazz)}
|
11
|
+
end
|
12
|
+
|
13
|
+
def should(matcher)
|
14
|
+
matcher.call(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require File.expand_path('../examples_helper', __FILE__)
|
2
|
+
|
3
|
+
#
|
4
|
+
# Sometimes building friendly public APIs requires some type flexibility
|
5
|
+
# at the public interface, that is coercion.
|
6
|
+
#
|
7
|
+
# In the example below, the API class defines a public interface. The Foo
|
8
|
+
# class is a pseudo-internal class: it is not intended to be instantiated
|
9
|
+
# by users, but can be manipulated once obtained (typically reused as an
|
10
|
+
# argument for a later call to the API). An array of symbol is a good kind
|
11
|
+
# of Foo literals. Therefore, the API should allow both Foo instance and
|
12
|
+
# arrays of symbols in a 'flexible but safe' way.
|
13
|
+
#
|
14
|
+
class API
|
15
|
+
|
16
|
+
# Foo is an internal class, that users should not instantiate directly.
|
17
|
+
# Instances of Foo can are sometimes returned by internals, are reused
|
18
|
+
# later as API arguments (see below).
|
19
|
+
class Foo
|
20
|
+
|
21
|
+
attr_reader :elements
|
22
|
+
|
23
|
+
def initialize(elements)
|
24
|
+
@elements = elements
|
25
|
+
end
|
26
|
+
|
27
|
+
def reverse
|
28
|
+
Foo.new(elements.reverse)
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"Foo(#{elements.inspect})"
|
33
|
+
end
|
34
|
+
alias :to_s :inspect
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
# We define a simple rule for coercing arrays of symvols
|
39
|
+
# as Foo instances
|
40
|
+
Coercions = Myrrha.coercions do |r|
|
41
|
+
FriendlyFoo = lambda{|v| v.is_a?(Array) and v.all?{|s| s.is_a?(Symbol)}}
|
42
|
+
r.coercion FriendlyFoo, Foo, lambda{|v,t| Foo.new(v)}
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Reverses a Foo.
|
47
|
+
#
|
48
|
+
# This method may be used with a Foo instance. It also accepts an array of
|
49
|
+
# symbols as a friendly Foo literal.
|
50
|
+
#
|
51
|
+
def self.reverse(foo)
|
52
|
+
Coercions.coerce(foo, Foo).reverse
|
53
|
+
end
|
54
|
+
|
55
|
+
end # class API
|
56
|
+
|
57
|
+
# An initial call with an array of symbols work
|
58
|
+
puts(x = API.reverse([:a, :b]))
|
59
|
+
|
60
|
+
# And so is a call with a Foo instance
|
61
|
+
puts(y = API.reverse(x))
|
62
|
+
|
63
|
+
# An invalid call will simply fail
|
64
|
+
begin
|
65
|
+
API.reverse("hello")
|
66
|
+
true.should eq(false)
|
67
|
+
rescue Myrrha::Error => ex
|
68
|
+
puts ex.message
|
69
|
+
end
|
70
|
+
|
71
|
+
# this is for testing purposes
|
72
|
+
API.reverse([:a, :b]).should be_a(API::Foo)
|
73
|
+
API.reverse(API.reverse([:a, :b])).should be_a(API::Foo)
|