myrrha 1.2.2 → 2.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 +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
@@ -0,0 +1,265 @@
|
|
1
|
+
module Myrrha
|
2
|
+
# Defines a set of coercion rules
|
3
|
+
#
|
4
|
+
class Coercions
|
5
|
+
|
6
|
+
# @return [Domain] The main target domain, if any
|
7
|
+
attr_accessor :main_target_domain
|
8
|
+
|
9
|
+
# Creates an empty list of coercion rules
|
10
|
+
def initialize(&defn)
|
11
|
+
@definitions = []
|
12
|
+
@upons = []
|
13
|
+
@rules = []
|
14
|
+
@fallbacks = []
|
15
|
+
@appender = :<<
|
16
|
+
@main_target_domain = nil
|
17
|
+
extend_rules(:<<, defn) if defn
|
18
|
+
end
|
19
|
+
|
20
|
+
# Appends the list of rules with new ones.
|
21
|
+
#
|
22
|
+
# New upon, coercion and fallback rules will be put after the already
|
23
|
+
# existing ones, in each case.
|
24
|
+
#
|
25
|
+
# Example:
|
26
|
+
#
|
27
|
+
# rules = Myrrha.coercions do ... end
|
28
|
+
# rules.append do |r|
|
29
|
+
#
|
30
|
+
# # [previous coercion rules would come here]
|
31
|
+
#
|
32
|
+
# # install new rules
|
33
|
+
# r.coercion String, Float, lambda{|v,t| Float(t)}
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
def append(&proc)
|
37
|
+
extend_rules(:<<, proc)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Prepends the list of rules with new ones.
|
41
|
+
#
|
42
|
+
# New upon, coercion and fallback rules will be put before the already
|
43
|
+
# existing ones, in each case.
|
44
|
+
#
|
45
|
+
# Example:
|
46
|
+
#
|
47
|
+
# rules = Myrrha.coercions do ... end
|
48
|
+
# rules.prepend do |r|
|
49
|
+
#
|
50
|
+
# # install new rules
|
51
|
+
# r.coercion String, Float, lambda{|v,t| Float(t)}
|
52
|
+
#
|
53
|
+
# # [previous coercion rules would come here]
|
54
|
+
#
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
def prepend(&proc)
|
58
|
+
extend_rules(:unshift, proc)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Adds an upon rule for a source domain.
|
62
|
+
#
|
63
|
+
# Example:
|
64
|
+
#
|
65
|
+
# Myrrha.coercions do |r|
|
66
|
+
#
|
67
|
+
# # Don't even try something else on nil
|
68
|
+
# r.upon(NilClass){|s,t| nil}
|
69
|
+
# [...]
|
70
|
+
#
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# @param source [Domain] a source domain (mimic Domain)
|
74
|
+
# @param converter [Converter] an optional converter (mimic Converter)
|
75
|
+
# @param convproc [Proc] used when converter is not specified
|
76
|
+
# @return self
|
77
|
+
#
|
78
|
+
def upon(source, converter = nil, &convproc)
|
79
|
+
@upons.send(@appender, [source, nil, converter || convproc])
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
# Adds an upon rule that works by delegation if the value responds to `method`.
|
84
|
+
#
|
85
|
+
# Example:
|
86
|
+
#
|
87
|
+
# Myrrha.coercions do |r|
|
88
|
+
# r.delegate(:to_foo)
|
89
|
+
#
|
90
|
+
# # is a shortcut for
|
91
|
+
# r.upon(lambda{|v,_| v.respond_to?(:to_foo)}){|v,_| v.to_foo}
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
def delegate(method, &convproc)
|
95
|
+
convproc ||= lambda{|v,t| v.send(method) }
|
96
|
+
upon(lambda{|v,t| v.respond_to?(method) }, convproc)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Adds a coercion rule from a source to a target domain.
|
100
|
+
#
|
101
|
+
# The conversion can be provided through `converter` or via a block
|
102
|
+
# directly. See main documentation about recognized converters.
|
103
|
+
#
|
104
|
+
# Example:
|
105
|
+
#
|
106
|
+
# Myrrha.coercions do |r|
|
107
|
+
#
|
108
|
+
# # With an explicit proc
|
109
|
+
# r.coercion String, Integer, lambda{|v,t|
|
110
|
+
# Integer(v)
|
111
|
+
# }
|
112
|
+
#
|
113
|
+
# # With an implicit proc
|
114
|
+
# r.coercion(String, Float) do |v,t|
|
115
|
+
# Float(v)
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# @param source [Domain] a source domain (mimicing Domain)
|
121
|
+
# @param target [Domain] a target domain (mimicing Domain)
|
122
|
+
# @param converter [Converter] an optional converter (mimic Converter)
|
123
|
+
# @param convproc [Proc] used when converter is not specified
|
124
|
+
# @return self
|
125
|
+
#
|
126
|
+
def coercion(source, target = main_target_domain, converter = nil, &convproc)
|
127
|
+
@rules.send(@appender, [source, target, converter || convproc])
|
128
|
+
self
|
129
|
+
end
|
130
|
+
|
131
|
+
# Adds a fallback rule for a source domain.
|
132
|
+
#
|
133
|
+
# Example:
|
134
|
+
#
|
135
|
+
# Myrrha.coercions do |r|
|
136
|
+
#
|
137
|
+
# # Add a 'last chance' rule for Strings
|
138
|
+
# r.fallback(String) do |v,t|
|
139
|
+
# # the user wants _v_ to be converted to a value of domain _t_
|
140
|
+
# end
|
141
|
+
#
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# @param source [Domain] a source domain (mimic Domain)
|
145
|
+
# @param converter [Converter] an optional converter (mimic Converter)
|
146
|
+
# @param convproc [Proc] used when converter is not specified
|
147
|
+
# @return self
|
148
|
+
#
|
149
|
+
def fallback(source, converter = nil, &convproc)
|
150
|
+
@fallbacks.send(@appender, [source, nil, converter || convproc])
|
151
|
+
self
|
152
|
+
end
|
153
|
+
|
154
|
+
# Coerces `value` to an element of `target_domain`
|
155
|
+
#
|
156
|
+
# This method tries each coercion rule, then each fallback in turn. Rules
|
157
|
+
# for which source and target domain match are executed until one succeeds.
|
158
|
+
# A Myrrha::Error is raised if no rule matches or executes successfuly.
|
159
|
+
#
|
160
|
+
# @param [Object] value any ruby value
|
161
|
+
# @param [Domain] target_domain a target domain to convert to (mimic Domain)
|
162
|
+
# @return self
|
163
|
+
#
|
164
|
+
def coerce(value, target_domain = main_target_domain)
|
165
|
+
return value if belongs_to?(value, target_domain)
|
166
|
+
error = nil
|
167
|
+
each_rule do |from,to,converter|
|
168
|
+
next unless from.nil? or belongs_to?(value, from, target_domain)
|
169
|
+
begin
|
170
|
+
catch(:nextrule) do
|
171
|
+
if to.nil? or subdomain?(to, target_domain)
|
172
|
+
got = convert(value, target_domain, converter)
|
173
|
+
return got
|
174
|
+
elsif subdomain?(target_domain, to)
|
175
|
+
got = convert(value, to, converter)
|
176
|
+
return got if belongs_to?(got, target_domain)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
rescue => ex
|
180
|
+
error = ex unless error
|
181
|
+
end
|
182
|
+
end
|
183
|
+
raise Error.new("Unable to coerce `#{value}` to #{target_domain}", error)
|
184
|
+
end
|
185
|
+
alias :apply :coerce
|
186
|
+
|
187
|
+
# Duplicates this set of rules in such a way that the original will not
|
188
|
+
# be affected by any change made to the copy.
|
189
|
+
#
|
190
|
+
# @return [Coercions] a copy of this set of rules
|
191
|
+
#
|
192
|
+
def dup
|
193
|
+
c = Coercions.new
|
194
|
+
@definitions.each do |defn|
|
195
|
+
c.extend_rules(*defn)
|
196
|
+
end
|
197
|
+
c
|
198
|
+
end
|
199
|
+
|
200
|
+
protected
|
201
|
+
|
202
|
+
# Returns true if `value` can be considered as a valid element of the
|
203
|
+
# domain `domain`, false otherwise.
|
204
|
+
#
|
205
|
+
# @param [Object] value any ruby value
|
206
|
+
# @param [Domain] domain a domain (mimic Domain)
|
207
|
+
# @return [Boolean] true if `value` belongs to `domain`, false otherwise
|
208
|
+
#
|
209
|
+
def belongs_to?(value, domain, target_domain = domain)
|
210
|
+
if domain.is_a?(Proc) and domain.arity==2
|
211
|
+
domain.call(value, target_domain)
|
212
|
+
else
|
213
|
+
domain.respond_to?(:===) && (domain === value)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Returns `true` if `child` can be considered a valid sub domain of
|
218
|
+
# `parent`, false otherwise.
|
219
|
+
#
|
220
|
+
# @param [Domain] child a domain (mimic Domain)
|
221
|
+
# @param [Domain] parent another domain (mimic Domain)
|
222
|
+
# @return [Boolean] true if `child` is a subdomain of `parent`, false
|
223
|
+
# otherwise.
|
224
|
+
#
|
225
|
+
def subdomain?(child, parent)
|
226
|
+
if child == parent
|
227
|
+
true
|
228
|
+
elsif parent.respond_to?(:superdomain_of?)
|
229
|
+
parent.superdomain_of?(child)
|
230
|
+
elsif child.respond_to?(:superclass) && child.superclass
|
231
|
+
subdomain?(child.superclass, parent)
|
232
|
+
else
|
233
|
+
false
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Extends existing rules
|
238
|
+
def extend_rules(appender, block)
|
239
|
+
@definitions << [appender, block]
|
240
|
+
@appender = appender
|
241
|
+
block.call(self)
|
242
|
+
self
|
243
|
+
end
|
244
|
+
|
245
|
+
# Yields each rule in turn (upons, coercions then fallbacks)
|
246
|
+
def each_rule(&proc)
|
247
|
+
@upons.each(&proc)
|
248
|
+
@rules.each(&proc)
|
249
|
+
@fallbacks.each(&proc)
|
250
|
+
end
|
251
|
+
|
252
|
+
# Calls converter on a (value,target_domain) pair.
|
253
|
+
def convert(value, target_domain, converter)
|
254
|
+
if converter.respond_to?(:call)
|
255
|
+
converter.call(value, target_domain)
|
256
|
+
elsif converter.is_a?(Array)
|
257
|
+
path = converter + [target_domain]
|
258
|
+
path.inject(value){|cur,ndom| coerce(cur, ndom)}
|
259
|
+
else
|
260
|
+
raise ArgumentError, "Unable to use #{converter} for coercing"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
end # class Coercions
|
265
|
+
end # module Myrrha
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Myrrha
|
2
|
+
module Domain
|
3
|
+
|
4
|
+
# Creates a domain instance by specialization by constraint
|
5
|
+
#
|
6
|
+
# @param [Class] superdom the superdomain of the created domain
|
7
|
+
# @param [Proc] pred the domain predicate
|
8
|
+
# @return [Class] the created domain
|
9
|
+
def self.sbyc(superdom = Object, subdoms = [], &pred)
|
10
|
+
Class.new(superdom).extend SByC.new(superdom, subdoms, pred)
|
11
|
+
end
|
12
|
+
|
13
|
+
end # module Domain
|
14
|
+
end # module Myrrha
|
15
|
+
require_relative 'domain/value_methods'
|
16
|
+
require_relative 'domain/coercion_methods'
|
17
|
+
require_relative 'domain/impl'
|
18
|
+
require_relative 'domain/sbyc'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Myrrha
|
2
|
+
module Domain
|
3
|
+
module CoercionMethods
|
4
|
+
|
5
|
+
def coercions(&bl)
|
6
|
+
@coercions ||= Coercions.new{|c| c.main_target_domain = self}
|
7
|
+
@coercions.append(&bl) if bl
|
8
|
+
@coercions
|
9
|
+
end
|
10
|
+
|
11
|
+
def coerce(arg)
|
12
|
+
coercions.coerce(arg, self)
|
13
|
+
end
|
14
|
+
|
15
|
+
end # module CoercionMethods
|
16
|
+
end # module Domain
|
17
|
+
end # module Myrrha
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Myrrha
|
2
|
+
module Domain
|
3
|
+
class Impl < Module
|
4
|
+
|
5
|
+
def initialize(component_names)
|
6
|
+
@component_names = component_names.freeze
|
7
|
+
define_initialize
|
8
|
+
define_component_readers
|
9
|
+
include_value_methods
|
10
|
+
end
|
11
|
+
|
12
|
+
def included(clazz)
|
13
|
+
define_component_names_on(clazz)
|
14
|
+
define_coercion_methods_on(clazz)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def define_initialize
|
21
|
+
component_names = @component_names
|
22
|
+
define_method(:initialize){|*args|
|
23
|
+
component_names.zip(args).each do |n,arg|
|
24
|
+
instance_variable_set(:"@#{n}", arg)
|
25
|
+
end
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def define_component_readers
|
30
|
+
@component_names.each do |n|
|
31
|
+
define_method(n){ instance_variable_get(:"@#{n}") }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def include_value_methods
|
36
|
+
module_eval{ include ValueMethods }
|
37
|
+
end
|
38
|
+
|
39
|
+
def define_component_names_on(clazz)
|
40
|
+
component_names = @component_names
|
41
|
+
clazz.define_singleton_method(:component_names){ component_names }
|
42
|
+
end
|
43
|
+
|
44
|
+
def define_coercion_methods_on(clazz)
|
45
|
+
clazz.extend(CoercionMethods)
|
46
|
+
end
|
47
|
+
|
48
|
+
end # class Impl
|
49
|
+
end # module Domain
|
50
|
+
end # module Myrrha
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Myrrha
|
2
|
+
module Domain
|
3
|
+
class SByC < Module
|
4
|
+
|
5
|
+
def initialize(super_domain, sub_domains, predicate)
|
6
|
+
@super_domain = super_domain
|
7
|
+
@sub_domains = sub_domains
|
8
|
+
@predicate = predicate
|
9
|
+
define
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def define
|
15
|
+
define_super_domain_method
|
16
|
+
define_sub_domains_method
|
17
|
+
define_predicate_method
|
18
|
+
include_type_methods
|
19
|
+
include_coercion_methods
|
20
|
+
end
|
21
|
+
|
22
|
+
def define_super_domain_method
|
23
|
+
super_domain = @super_domain
|
24
|
+
define_method(:super_domain){ super_domain }
|
25
|
+
end
|
26
|
+
|
27
|
+
def define_sub_domains_method
|
28
|
+
sub_domains = @sub_domains
|
29
|
+
define_method(:sub_domains){ sub_domains }
|
30
|
+
end
|
31
|
+
|
32
|
+
def define_predicate_method
|
33
|
+
predicate = @predicate
|
34
|
+
define_method(:predicate){ predicate }
|
35
|
+
end
|
36
|
+
|
37
|
+
def include_type_methods
|
38
|
+
module_eval{ include TypeMethods }
|
39
|
+
end
|
40
|
+
|
41
|
+
def include_coercion_methods
|
42
|
+
module_eval{ include CoercionMethods }
|
43
|
+
end
|
44
|
+
|
45
|
+
module TypeMethods
|
46
|
+
|
47
|
+
# Creates a new instance of this domain
|
48
|
+
def new(*args)
|
49
|
+
if (args.size == 1) && (superclass===args.first)
|
50
|
+
raise ArgumentError, "Invalid value #{args.join(' ')} for #{self}" unless self===args.first
|
51
|
+
args.first
|
52
|
+
elsif superclass.respond_to?(:new)
|
53
|
+
new(super(*args))
|
54
|
+
else
|
55
|
+
raise ArgumentError, "Invalid value #{args.join(' ')} for #{self}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# (see Class.superclass)
|
60
|
+
def superclass
|
61
|
+
super_domain || super
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns true if clazz if an explicit sub domain of self or if it's the case in Ruby.
|
65
|
+
def superdomain_of?(child)
|
66
|
+
sub_domains.include?(child)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Checks if `value` belongs to this domain
|
70
|
+
def ===(value)
|
71
|
+
(superclass === value) && predicate.call(value)
|
72
|
+
end
|
73
|
+
|
74
|
+
end # module TypeMethods
|
75
|
+
|
76
|
+
end # module SByC
|
77
|
+
end # module Domain
|
78
|
+
end # module Myrrha
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Myrrha
|
2
|
+
module Domain
|
3
|
+
module ValueMethods
|
4
|
+
|
5
|
+
# Parts of this module have been extracted from Virtus, MIT Copyright (c) 2011-2012
|
6
|
+
# Piotr Solnica.
|
7
|
+
|
8
|
+
# Returns a hash code for the value
|
9
|
+
#
|
10
|
+
# @return Integer
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
def hash
|
14
|
+
component_names.map{|key| send(key).hash }.reduce(self.class.hash, :^)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Compare the object with other object for equality
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# object.eql?(other) # => true or false
|
21
|
+
#
|
22
|
+
# @param [Object] other
|
23
|
+
# the other object to compare with
|
24
|
+
#
|
25
|
+
# @return [Boolean]
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
def eql?(other)
|
29
|
+
instance_of?(other.class) and cmp?(__method__, other)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Compare the object with other object for equivalency
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# object == other # => true or false
|
36
|
+
#
|
37
|
+
# @param [Object] other
|
38
|
+
# the other object to compare with
|
39
|
+
#
|
40
|
+
# @return [Boolean]
|
41
|
+
#
|
42
|
+
# @api public
|
43
|
+
def ==(other)
|
44
|
+
return false unless self.class <=> other.class
|
45
|
+
cmp?(__method__, other)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def cmp?(comparator, other)
|
51
|
+
component_names.all?{|key| send(key).send(comparator, other.send(key)) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def component_names
|
55
|
+
self.class.component_names
|
56
|
+
end
|
57
|
+
|
58
|
+
end # module ValueMethods
|
59
|
+
end # module Domain
|
60
|
+
end # module Myrrha
|