myrrha 1.2.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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