myrrha 1.2.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +62 -29
- data/README.md +117 -121
- data/Rakefile +0 -12
- data/examples/sbyc_domain.rb +1 -1
- data/lib/myrrha.rb +11 -364
- data/lib/myrrha/coerce.rb +22 -25
- data/lib/myrrha/coercions.rb +265 -0
- data/lib/myrrha/domain.rb +18 -0
- data/lib/myrrha/domain/coercion_methods.rb +17 -0
- data/lib/myrrha/domain/impl.rb +50 -0
- data/lib/myrrha/domain/sbyc.rb +78 -0
- data/lib/myrrha/domain/value_methods.rb +60 -0
- data/lib/myrrha/errors.rb +12 -0
- data/lib/myrrha/loader.rb +0 -1
- data/lib/myrrha/to_ruby_literal.rb +1 -1
- data/lib/myrrha/version.rb +3 -3
- data/myrrha.noespec +3 -3
- data/spec/coercions/test_append.rb +3 -3
- data/spec/coercions/test_belongs_to.rb +12 -6
- data/spec/coercions/test_convert.rb +5 -5
- data/spec/coercions/test_delegate.rb +35 -0
- data/spec/coercions/test_subdomain.rb +9 -3
- data/spec/domain/impl/test_behavior.rb +41 -0
- data/spec/domain/impl/test_value_methods.rb +101 -0
- data/spec/domain/sbyc/test_behavior.rb +46 -0
- data/spec/domain/test_sbyc.rb +81 -0
- data/spec/myrrha/test_coercions.rb +1 -1
- data/spec/test_myrrha.rb +0 -3
- data/tasks/examples.rake +2 -1
- data/tasks/gem.rake +9 -4
- metadata +83 -43
- data/spec/myrrha/test_domain.rb +0 -116
data/Rakefile
CHANGED
@@ -1,15 +1,3 @@
|
|
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
1
|
# We run tests by default
|
14
2
|
task :default => :test
|
15
3
|
|
data/examples/sbyc_domain.rb
CHANGED
data/lib/myrrha.rb
CHANGED
@@ -1,32 +1,15 @@
|
|
1
|
+
require_relative "myrrha/version"
|
2
|
+
require_relative "myrrha/loader"
|
3
|
+
require_relative 'myrrha/errors'
|
1
4
|
#
|
2
5
|
# Myrrha -- the missing coercion framework for Ruby
|
3
6
|
#
|
4
7
|
module Myrrha
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
#
|
12
|
-
# Creates a domain instance by specialization by constraint
|
13
|
-
#
|
14
|
-
# @param [Class] superdom the superdomain of the created domain
|
15
|
-
# @param [Proc] pred the domain predicate
|
16
|
-
# @return [Class] the created domain
|
17
|
-
#
|
18
|
-
def self.domain(superdom = Object, subdoms=nil, &pred)
|
19
|
-
dom = Class.new(superdom).extend(Domain)
|
20
|
-
dom.instance_eval {
|
21
|
-
@subdomains = subdoms
|
22
|
-
@superdomain = superdom
|
23
|
-
@predicate = pred
|
24
|
-
}
|
25
|
-
dom
|
26
|
-
end
|
27
|
-
|
28
|
-
#
|
29
|
-
# Builds a set of coercions rules.
|
8
|
+
|
9
|
+
require_relative 'myrrha/domain'
|
10
|
+
require_relative 'myrrha/coercions'
|
11
|
+
|
12
|
+
# Builds a set of coercions rules.
|
30
13
|
#
|
31
14
|
# Example:
|
32
15
|
#
|
@@ -41,351 +24,15 @@ module Myrrha
|
|
41
24
|
def self.coercions(&block)
|
42
25
|
Coercions.new(&block)
|
43
26
|
end
|
44
|
-
|
45
|
-
#
|
46
|
-
# Encapsulates class methods of created domains
|
47
|
-
#
|
48
|
-
module Domain
|
49
|
-
|
50
|
-
#
|
51
|
-
# Creates a new instance of this domain
|
52
|
-
#
|
53
|
-
def new(*args)
|
54
|
-
if (args.size == 1) && (superclass === args.first)
|
55
|
-
if self === args.first
|
56
|
-
args.first
|
57
|
-
else
|
58
|
-
raise ArgumentError, "Invalid value #{args.join(' ')} for #{self}"
|
59
|
-
end
|
60
|
-
elsif superclass.respond_to?(:new)
|
61
|
-
new(super(*args))
|
62
|
-
else
|
63
|
-
raise ArgumentError, "Invalid value #{args.join(' ')} for #{self}"
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# (see Class.superclass)
|
68
|
-
def superclass
|
69
|
-
superdomain || super
|
70
|
-
end
|
71
|
-
|
72
|
-
#
|
73
|
-
# Returns the super domain if installed
|
74
|
-
#
|
75
|
-
def superdomain
|
76
|
-
@superdomain
|
77
|
-
end
|
78
|
-
|
79
|
-
#
|
80
|
-
# Returns true if clazz if an explicit sub domain of self or if it's the
|
81
|
-
# case in Ruby.
|
82
|
-
#
|
83
|
-
def superdomain_of?(child)
|
84
|
-
Array(@subdomains).include?(child)
|
85
|
-
end
|
86
|
-
|
87
|
-
#
|
88
|
-
# Checks if `value` belongs to this domain
|
89
|
-
#
|
90
|
-
def ===(value)
|
91
|
-
(superclass === value) && predicate.call(value)
|
92
|
-
end
|
93
|
-
|
94
|
-
#
|
95
|
-
# Returns the specialization by constraint predicate
|
96
|
-
#
|
97
|
-
# @return [Proc] the domain predicate
|
98
|
-
#
|
99
|
-
def predicate
|
100
|
-
@predicate
|
101
|
-
end
|
102
|
-
|
103
|
-
end # module Domain
|
104
|
-
|
105
|
-
# Defines a set of coercion rules
|
106
|
-
#
|
107
|
-
class Coercions
|
108
|
-
|
109
|
-
# @return [Domain] The main target domain, if any
|
110
|
-
attr_accessor :main_target_domain
|
111
|
-
|
112
|
-
#
|
113
|
-
# Creates an empty list of coercion rules
|
114
|
-
#
|
115
|
-
def initialize(&defn)
|
116
|
-
@definitions = []
|
117
|
-
@upons = []
|
118
|
-
@rules = []
|
119
|
-
@fallbacks = []
|
120
|
-
@appender = :<<
|
121
|
-
@main_target_domain = nil
|
122
|
-
extend_rules(:<<, defn) if defn
|
123
|
-
end
|
124
|
-
|
125
|
-
#
|
126
|
-
# Appends the list of rules with new ones.
|
127
|
-
#
|
128
|
-
# New upon, coercion and fallback rules will be put after the already
|
129
|
-
# existing ones, in each case.
|
130
|
-
#
|
131
|
-
# Example:
|
132
|
-
#
|
133
|
-
# rules = Myrrha.coercions do ... end
|
134
|
-
# rules.append do |r|
|
135
|
-
#
|
136
|
-
# # [previous coercion rules would come here]
|
137
|
-
#
|
138
|
-
# # install new rules
|
139
|
-
# r.coercion String, Float, lambda{|v,t| Float(t)}
|
140
|
-
# end
|
141
|
-
#
|
142
|
-
def append(&proc)
|
143
|
-
extend_rules(:<<, proc)
|
144
|
-
end
|
145
|
-
|
146
|
-
#
|
147
|
-
# Prepends the list of rules with new ones.
|
148
|
-
#
|
149
|
-
# New upon, coercion and fallback rules will be put before the already
|
150
|
-
# existing ones, in each case.
|
151
|
-
#
|
152
|
-
# Example:
|
153
|
-
#
|
154
|
-
# rules = Myrrha.coercions do ... end
|
155
|
-
# rules.prepend do |r|
|
156
|
-
#
|
157
|
-
# # install new rules
|
158
|
-
# r.coercion String, Float, lambda{|v,t| Float(t)}
|
159
|
-
#
|
160
|
-
# # [previous coercion rules would come here]
|
161
|
-
#
|
162
|
-
# end
|
163
|
-
#
|
164
|
-
def prepend(&proc)
|
165
|
-
extend_rules(:unshift, proc)
|
166
|
-
end
|
167
|
-
|
168
|
-
#
|
169
|
-
# Adds an upon rule for a source domain.
|
170
|
-
#
|
171
|
-
# Example:
|
172
|
-
#
|
173
|
-
# Myrrha.coercions do |r|
|
174
|
-
#
|
175
|
-
# # Don't even try something else on nil
|
176
|
-
# r.upon(NilClass){|s,t| nil}
|
177
|
-
# [...]
|
178
|
-
#
|
179
|
-
# end
|
180
|
-
#
|
181
|
-
# @param source [Domain] a source domain (mimic Domain)
|
182
|
-
# @param converter [Converter] an optional converter (mimic Converter)
|
183
|
-
# @param convproc [Proc] used when converter is not specified
|
184
|
-
# @return self
|
185
|
-
#
|
186
|
-
def upon(source, converter = nil, &convproc)
|
187
|
-
@upons.send(@appender, [source, nil, converter || convproc])
|
188
|
-
self
|
189
|
-
end
|
190
|
-
|
191
|
-
#
|
192
|
-
# Adds a coercion rule from a source to a target domain.
|
193
|
-
#
|
194
|
-
# The conversion can be provided through `converter` or via a block
|
195
|
-
# directly. See main documentation about recognized converters.
|
196
|
-
#
|
197
|
-
# Example:
|
198
|
-
#
|
199
|
-
# Myrrha.coercions do |r|
|
200
|
-
#
|
201
|
-
# # With an explicit proc
|
202
|
-
# r.coercion String, Integer, lambda{|v,t|
|
203
|
-
# Integer(v)
|
204
|
-
# }
|
205
|
-
#
|
206
|
-
# # With an implicit proc
|
207
|
-
# r.coercion(String, Float) do |v,t|
|
208
|
-
# Float(v)
|
209
|
-
# end
|
210
|
-
#
|
211
|
-
# end
|
212
|
-
#
|
213
|
-
# @param source [Domain] a source domain (mimicing Domain)
|
214
|
-
# @param target [Domain] a target domain (mimicing Domain)
|
215
|
-
# @param converter [Converter] an optional converter (mimic Converter)
|
216
|
-
# @param convproc [Proc] used when converter is not specified
|
217
|
-
# @return self
|
218
|
-
#
|
219
|
-
def coercion(source, target = main_target_domain, converter = nil, &convproc)
|
220
|
-
@rules.send(@appender, [source, target, converter || convproc])
|
221
|
-
self
|
222
|
-
end
|
223
|
-
|
224
|
-
#
|
225
|
-
# Adds a fallback rule for a source domain.
|
226
|
-
#
|
227
|
-
# Example:
|
228
|
-
#
|
229
|
-
# Myrrha.coercions do |r|
|
230
|
-
#
|
231
|
-
# # Add a 'last chance' rule for Strings
|
232
|
-
# r.fallback(String) do |v,t|
|
233
|
-
# # the user wants _v_ to be converted to a value of domain _t_
|
234
|
-
# end
|
235
|
-
#
|
236
|
-
# end
|
237
|
-
#
|
238
|
-
# @param source [Domain] a source domain (mimic Domain)
|
239
|
-
# @param converter [Converter] an optional converter (mimic Converter)
|
240
|
-
# @param convproc [Proc] used when converter is not specified
|
241
|
-
# @return self
|
242
|
-
#
|
243
|
-
def fallback(source, converter = nil, &convproc)
|
244
|
-
@fallbacks.send(@appender, [source, nil, converter || convproc])
|
245
|
-
self
|
246
|
-
end
|
247
|
-
|
248
|
-
#
|
249
|
-
# Coerces `value` to an element of `target_domain`
|
250
|
-
#
|
251
|
-
# This method tries each coercion rule, then each fallback in turn. Rules
|
252
|
-
# for which source and target domain match are executed until one succeeds.
|
253
|
-
# A Myrrha::Error is raised if no rule matches or executes successfuly.
|
254
|
-
#
|
255
|
-
# @param [Object] value any ruby value
|
256
|
-
# @param [Domain] target_domain a target domain to convert to (mimic Domain)
|
257
|
-
# @return self
|
258
|
-
#
|
259
|
-
def coerce(value, target_domain = main_target_domain)
|
260
|
-
return value if belongs_to?(value, target_domain)
|
261
|
-
error = nil
|
262
|
-
each_rule do |from,to,converter|
|
263
|
-
next unless from.nil? or belongs_to?(value, from, target_domain)
|
264
|
-
begin
|
265
|
-
catch(:nextrule) do
|
266
|
-
if to.nil? or subdomain?(to, target_domain)
|
267
|
-
got = convert(value, target_domain, converter)
|
268
|
-
return got
|
269
|
-
elsif subdomain?(target_domain, to)
|
270
|
-
got = convert(value, to, converter)
|
271
|
-
return got if belongs_to?(got, target_domain)
|
272
|
-
end
|
273
|
-
end
|
274
|
-
rescue => ex
|
275
|
-
error = ex.message unless error
|
276
|
-
end
|
277
|
-
end
|
278
|
-
msg = "Unable to coerce `#{value}` to #{target_domain}"
|
279
|
-
msg += " (#{error})" if error
|
280
|
-
raise Error, msg
|
281
|
-
end
|
282
|
-
alias :apply :coerce
|
283
|
-
|
284
|
-
#
|
285
|
-
# Returns true if `value` can be considered as a valid element of the
|
286
|
-
# domain `domain`, false otherwise.
|
287
|
-
#
|
288
|
-
# @param [Object] value any ruby value
|
289
|
-
# @param [Domain] domain a domain (mimic Domain)
|
290
|
-
# @return [Boolean] true if `value` belongs to `domain`, false otherwise
|
291
|
-
#
|
292
|
-
def belongs_to?(value, domain, target_domain = domain)
|
293
|
-
case domain
|
294
|
-
when Proc
|
295
|
-
if domain.arity == 2
|
296
|
-
domain.call(value, target_domain)
|
297
|
-
elsif RUBY_VERSION < "1.9"
|
298
|
-
domain.call(value)
|
299
|
-
elsif domain
|
300
|
-
domain === value
|
301
|
-
end
|
302
|
-
else
|
303
|
-
domain.respond_to?(:===) ?
|
304
|
-
domain === value :
|
305
|
-
false
|
306
|
-
end
|
307
|
-
end
|
308
|
-
|
309
|
-
#
|
310
|
-
# Returns `true` if `child` can be considered a valid sub domain of
|
311
|
-
# `parent`, false otherwise.
|
312
|
-
#
|
313
|
-
# @param [Domain] child a domain (mimic Domain)
|
314
|
-
# @param [Domain] parent another domain (mimic Domain)
|
315
|
-
# @return [Boolean] true if `child` is a subdomain of `parent`, false
|
316
|
-
# otherwise.
|
317
|
-
#
|
318
|
-
def subdomain?(child, parent)
|
319
|
-
if child == parent
|
320
|
-
true
|
321
|
-
elsif parent.respond_to?(:superdomain_of?)
|
322
|
-
parent.superdomain_of?(child)
|
323
|
-
elsif child.respond_to?(:superclass) && child.superclass
|
324
|
-
subdomain?(child.superclass, parent)
|
325
|
-
else
|
326
|
-
false
|
327
|
-
end
|
328
|
-
end
|
329
|
-
|
330
|
-
#
|
331
|
-
# Duplicates this set of rules in such a way that the original will not
|
332
|
-
# be affected by any change made to the copy.
|
333
|
-
#
|
334
|
-
# @return [Coercions] a copy of this set of rules
|
335
|
-
#
|
336
|
-
def dup
|
337
|
-
c = Coercions.new
|
338
|
-
@definitions.each do |defn|
|
339
|
-
c.extend_rules(*defn)
|
340
|
-
end
|
341
|
-
c
|
342
|
-
end
|
343
|
-
|
344
|
-
protected
|
345
|
-
|
346
|
-
# Extends existing rules
|
347
|
-
def extend_rules(appender, block)
|
348
|
-
@definitions << [appender, block]
|
349
|
-
@appender = appender
|
350
|
-
block.call(self)
|
351
|
-
self
|
352
|
-
end
|
353
|
-
|
354
|
-
#
|
355
|
-
# Yields each rule in turn (upons, coercions then fallbacks)
|
356
|
-
#
|
357
|
-
def each_rule(&proc)
|
358
|
-
@upons.each(&proc)
|
359
|
-
@rules.each(&proc)
|
360
|
-
@fallbacks.each(&proc)
|
361
|
-
end
|
362
|
-
|
363
|
-
#
|
364
|
-
# Calls converter on a (value,target_domain) pair.
|
365
|
-
#
|
366
|
-
def convert(value, target_domain, converter)
|
367
|
-
if converter.respond_to?(:call)
|
368
|
-
converter.call(value, target_domain)
|
369
|
-
elsif converter.is_a?(Array)
|
370
|
-
path = converter + [target_domain]
|
371
|
-
path.inject(value){|cur,ndom| coerce(cur, ndom)}
|
372
|
-
else
|
373
|
-
raise ArgumentError, "Unable to use #{converter} for coercing"
|
374
|
-
end
|
375
|
-
end
|
376
|
-
|
377
|
-
end # class Coercions
|
378
|
-
|
27
|
+
|
379
28
|
# Myrrha main options
|
380
29
|
OPTIONS = {
|
381
30
|
:core_ext => false
|
382
31
|
}
|
383
|
-
|
32
|
+
|
384
33
|
# Install core extensions?
|
385
34
|
def self.core_ext?
|
386
35
|
OPTIONS[:core_ext]
|
387
36
|
end
|
388
|
-
|
37
|
+
|
389
38
|
end # module Myrrha
|
390
|
-
require "myrrha/version"
|
391
|
-
require "myrrha/loader"
|
data/lib/myrrha/coerce.rb
CHANGED
@@ -1,22 +1,19 @@
|
|
1
1
|
require 'myrrha'
|
2
2
|
module Myrrha
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
Boolean = Myrrha.domain(Object, [TrueClass, FalseClass]){|x|
|
8
|
-
(x==true) || (x==false)
|
9
|
-
}
|
3
|
+
|
4
|
+
class Boolean < Object
|
5
|
+
extend Myrrha::Domain::SByC.new(Object, [TrueClass, FalseClass], lambda{|x| (x==true) || (x==false)})
|
6
|
+
end
|
10
7
|
|
11
8
|
#
|
12
9
|
# Coerces _s_ to a Boolean
|
13
10
|
#
|
14
11
|
# This method mimics Ruby's Integer(), Float(), etc. for Boolean values.
|
15
12
|
#
|
16
|
-
# @param [Object] s a Boolean or a String
|
13
|
+
# @param [Object] s a Boolean or a String
|
17
14
|
# @return [Boolean] true if `s` is already true of the string 'true',
|
18
15
|
# false if `s` is already false of the string 'false'.
|
19
|
-
# @raise [ArgumentError] if `s` cannot be coerced to a boolean.
|
16
|
+
# @raise [ArgumentError] if `s` cannot be coerced to a boolean.
|
20
17
|
#
|
21
18
|
def self.Boolean(s)
|
22
19
|
if (s==true || s.to_str.strip == "true")
|
@@ -27,28 +24,28 @@ module Myrrha
|
|
27
24
|
raise ArgumentError, "invalid value for Boolean: \"#{s}\""
|
28
25
|
end
|
29
26
|
end
|
30
|
-
|
31
|
-
# Defines basic coercions for Ruby, mostly from String
|
27
|
+
|
28
|
+
# Defines basic coercions for Ruby, mostly from String
|
32
29
|
Coerce = coercions do |g|
|
33
|
-
|
30
|
+
|
34
31
|
# Returns a constant denoted by `s`
|
35
32
|
def g.constant_lookup(s, target_domain)
|
36
|
-
found = (s.split('::') - [""]).inject(Kernel){|cur,n|
|
33
|
+
found = (s.split('::') - [""]).inject(Kernel){|cur,n|
|
37
34
|
cur.const_get(n.to_sym)
|
38
35
|
}
|
39
36
|
belongs_to?(found, target_domain) ? found : throw(:nextrule)
|
40
37
|
end
|
41
|
-
|
38
|
+
|
42
39
|
# NilClass should return immediately
|
43
|
-
g.upon(NilClass) do |s,t|
|
40
|
+
g.upon(NilClass) do |s,t|
|
44
41
|
nil
|
45
42
|
end
|
46
|
-
|
43
|
+
|
47
44
|
# Use t.coerce if it exists
|
48
45
|
g.upon(lambda{|s,t| t.respond_to?(:coerce)}) do |s,t|
|
49
46
|
t.coerce(s)
|
50
47
|
end
|
51
|
-
|
48
|
+
|
52
49
|
# Specific basic rules
|
53
50
|
g.coercion Object, String, lambda{|s,t| String(s) }
|
54
51
|
g.coercion String, Integer, lambda{|s,t| Integer(s) }
|
@@ -62,18 +59,18 @@ module Myrrha
|
|
62
59
|
g.coercion String, Class, lambda{|s,t| g.constant_lookup(s, t) }
|
63
60
|
g.coercion String, Module, lambda{|s,t| g.constant_lookup(s, t) }
|
64
61
|
g.coercion String, Time, lambda{|s,t| require 'time'; Time.parse(s) }
|
65
|
-
|
66
|
-
# By default, we try to invoke :parse on the class
|
67
|
-
g.fallback(String) do |s,t|
|
68
|
-
t.respond_to?(:parse) ? t.parse(s.to_str) : throw(:nextrule)
|
62
|
+
|
63
|
+
# By default, we try to invoke :parse on the class
|
64
|
+
g.fallback(String) do |s,t|
|
65
|
+
t.respond_to?(:parse) ? t.parse(s.to_str) : throw(:nextrule)
|
69
66
|
end
|
70
|
-
|
67
|
+
|
71
68
|
end # Coerce
|
72
69
|
|
73
70
|
def self.coerce(value, domain)
|
74
71
|
Coerce.apply(value, domain)
|
75
72
|
end
|
76
|
-
|
73
|
+
|
77
74
|
end # module Myrrha
|
78
75
|
|
79
76
|
if Myrrha.core_ext?
|
@@ -85,6 +82,6 @@ if Myrrha.core_ext?
|
|
85
82
|
private
|
86
83
|
def coerce(value, domain)
|
87
84
|
Myrrha.coerce(value, domain)
|
88
|
-
end
|
85
|
+
end
|
89
86
|
end
|
90
|
-
end
|
87
|
+
end
|