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