myrrha 1.0.0 → 1.1.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 CHANGED
@@ -1,5 +1,75 @@
1
+ # 1.1.0 / 2011-07-28
2
+
3
+ ## Enhancements to coerce()
4
+
5
+ * Added coercion rules from Symbol/String to Module/Class
6
+
7
+ coerce("Integer", Class) # => Integer
8
+ coerce(:Integer, Class) # => Integer
9
+ coerce("Myrrha::Version", Module) # => Myrrha::Version
10
+ [... and so on ...]
11
+
12
+ * Added following coercion rules for Booleans
13
+
14
+ coerce("true", TrueClass) # => true
15
+ coerce("false", FalseClass) # => false
16
+
17
+ * Added coercion rule from any Object to String through ruby's String(). Note
18
+ that even with this coercion rule, coerce(nil, String) returns nil as that
19
+ rule has higher priority.
20
+
21
+ * require('time') is automatically issued when trying to coerce a String to
22
+ a Time. Time.parse is obviously needed.
23
+
24
+ * Myrrha::Boolean (Boolean with core extensions) is now a factored domain (see
25
+ below). Therefore, it is now a true Class instance.
26
+
27
+ ## Enhancements to the general coercion mechanism
28
+
29
+ * An optimistic coercion is tried when a rule is encountered whose target
30
+ domain is a super domain of the requested one. Coercion only succeeds if
31
+ the coerced value correctly belongs to the latter domain. Example:
32
+
33
+ rules = Myrrha.coercions do |r|
34
+ r.coercion String, Numeric, lambda{|s,t| Integer(s)}
35
+ end
36
+ rules.coerce("12", Integer) # => 12 in 1.1.0 while it failed in 1.0.0
37
+ rules.coerce("12", Float) # => Myrrha::Error
38
+
39
+ * You can now specify a coercion path, through an array of domains. For
40
+ example (completely contrived, of course):
41
+
42
+ rules = Myrrha.coercions do |r|
43
+ r.coercion String, Symbol, lambda{|s,t| s.to_sym }
44
+ r.coercion Float, String, lambda{|s,t| s.to_s }
45
+ r.coercion Integer, Float, lambda{|s,t| Float(s) }
46
+ r.coercion Integer, Symbol, [Float, String]
47
+ end
48
+ rules.coerce(12, Symbol) # => :"12.0" as Symbol(String(Float(12)))
49
+
50
+ * You can now define domains through specialization by constraint (sbyc) on ruby
51
+ classes, using Myrrha.domain:
52
+
53
+ # Create a positive integer domain, as ... positive integers
54
+ PosInt = Myrrha.domain(Integer){|i| i > 0 }
55
+
56
+ Created domain is a real Class instance, that correctly responds to :===
57
+ and :superclass. The feature is mainly introduced for supporting the following
58
+ kind of coercion scenarios (see README for more about this):
59
+
60
+ rules = Myrrha.coercions do |r|
61
+ r.coercion String, Integer, lambda{|s,t| Integer(s)}
62
+ end
63
+ rules.coerce("12", PosInt) # => 12
64
+ rules.coerce("-12", PosInt) # => ArgumentError, "Invalid value -12 for PosInt"
65
+
66
+ ## Bug fixes
67
+
68
+ * Fixed Coercions#dup when a set of rules has a main target domain. This fixes
69
+ the duplication of ToRubyLiteral rules, among others.
70
+
1
71
  # 1.0.0 / 2011-07-22
2
72
 
3
- * Enhancements
73
+ ## Enhancements
4
74
 
5
75
  * Birthday!
data/Gemfile.lock CHANGED
@@ -1,12 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- myrrha (1.0.0)
4
+ myrrha (1.1.0)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
8
8
  specs:
9
- bluecloth (2.0.11)
9
+ bluecloth (2.1.0)
10
10
  diff-lcs (1.1.2)
11
11
  highline (1.6.2)
12
12
  noe (1.3.0)
@@ -31,7 +31,7 @@ PLATFORMS
31
31
  ruby
32
32
 
33
33
  DEPENDENCIES
34
- bluecloth (~> 2.0.9)
34
+ bluecloth (~> 2.1.0)
35
35
  bundler (~> 1.0)
36
36
  myrrha!
37
37
  noe (~> 1.3.0)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Myrrha
1
+ # Myrrha (v1.1.0)
2
2
 
3
3
  ## Description
4
4
 
@@ -17,15 +17,15 @@ a numeric, a boolean, a date, a time, an URI, and so on.
17
17
  # Bug fixes (tiny) do not even add new default rules to coerce and
18
18
  # to\_ruby\_literal. Minor version can, which could break your code.
19
19
  # Therefore, please always use:
20
- gem "alf", "~> 1.0.0"
20
+ gem "myrrha", "~> 1.0.0"
21
21
 
22
22
  ## Links
23
23
 
24
- * http://rubydoc.info/github/blambeau/myrrha/master/frames (read this file there!)
24
+ * http://www.rubydoc.info/gems/myrrha/1.1.0/file/README.md (read this file there!)
25
25
  * http://github.com/blambeau/myrrha (source code)
26
26
  * http://rubygems.org/gems/myrrha (download)
27
27
 
28
- ## The missing <code>coerce()</code>
28
+ ## The <code>coerce()</code> feature
29
29
 
30
30
  Myrrha.coerce(:anything, Domain)
31
31
  coerce(:anything, Domain) # with core extensions
@@ -48,34 +48,71 @@ possible and even straightforward:
48
48
  end
49
49
  # => [12, true, #<Date: 2011-07-20 (...)>]
50
50
 
51
- ### Example
51
+ ### Implemented coercions
52
+
53
+ Implemented coercions are somewhat conservative, and only use a subset of what
54
+ ruby provides here and there. This is to avoid strangeness ala PHP... The
55
+ general philosophy is to provide the natural coercions we apply everyday.
56
+
57
+ The master rules are
58
+
59
+ * <code>coerce(value, Domain)</code> return <code>value</code> if
60
+ <code>belongs_to?(value, Domain)</code> is true (see last section below)
61
+ * <code>coerce(value, Domain)</code> returns <code>Domain.coerce(value)</code>
62
+ if the latter method exists.
63
+ * <code>coerce("any string", Domain)</code> returns <code>Domain.parse(value)</code>
64
+ if the latter method exists.
65
+
66
+ The specific implemented rules are
52
67
 
53
68
  require 'myrrha/with_core_ext'
54
69
  require 'myrrha/coerce'
55
70
 
56
- # it works on numerics
71
+ # NilClass -> _Anything_ returns nil, always
72
+ coerce(nil, Integer) # => nil
73
+
74
+ # Object -> String, via ruby's String()
75
+ coerce("hello", String) # => "hello"
76
+ coerce(:hello, String) # => "hello"
77
+
78
+ # String -> Numeric, through ruby's Integer() and Float()
57
79
  coerce("12", Integer) # => 12
58
80
  coerce("12.0", Float) # => 12.0
59
81
 
60
- # but also on regexp (through Regexp.compile)
82
+ # String -> Numeric is smart enough:
83
+ coerce("12", Numeric) # => 12 (Integer)
84
+ coerce("12.0", Numeric) # => 12.0 (Float)
85
+
86
+ # String -> Regexp, through Regexp.compile
61
87
  coerce("[a-z]+", Regexp) # => /[a-z]+/
62
88
 
63
- # and, yes, on Boolean (sorry Matz!)
89
+ # String -> Symbol, through to_sym
90
+ coerce("hello", Symbol) # => :hello
91
+
92
+ # String -> Boolean (hum, sorry Matz!)
64
93
  coerce("true", Boolean) # => true
65
94
  coerce("false", Boolean) # => false
95
+ coerce("true", TrueClass) # => true
96
+ coerce("false", FalseClass) # => false
66
97
 
67
- # and on date and time (through Date/Time.parse)
98
+ # String -> Date, through Date.parse
68
99
  require 'date'
69
- require 'time'
70
100
  coerce("2011-07-20", Date) # => #<Date: 2011-07-20 (4911525/2,0,2299161)>
101
+
102
+ # String -> Time, through Time.parse (just in time issuing of require('time'))
71
103
  coerce("2011-07-20 10:57", Time) # => 2011-07-20 10:57:00 +0200
72
104
 
73
- # why not on URI?
105
+ # String -> URI, through URI.parse
74
106
  require 'uri'
75
107
  coerce('http://google.com', URI) # => #<URI::HTTP:0x8281ce0 URL:http://google.com>
76
108
 
77
- # on nil, it always returns nil
78
- coerce(nil, Integer) # => nil
109
+ # String -> Class and Module through constant lookup
110
+ coerce("Integer", Class) # => Integer
111
+ coerce("Myrrha::Version", Module) # => Myrrha::Version
112
+
113
+ # Symbol -> Class and Module through constant lookup
114
+ coerce(:Integer, Class) # => Integer
115
+ coerce(:Enumerable, Module) # => Enumerable
79
116
 
80
117
  ### No core extension? no problem!
81
118
 
@@ -133,7 +170,7 @@ might be intrusive. Why not using your own set of coercion rules?
133
170
  MyRules.apply(:hello, Foo)
134
171
  # => #<Foo:0x8b7d254 @arg=:hello>
135
172
 
136
- ## The missing <code>to\_ruby\_literal()</code>
173
+ ## The <code>to\_ruby\_literal()</code> feature
137
174
 
138
175
  Myrrha.to_ruby_literal([:anything])
139
176
  [:anything].to_ruby_literal # with core extensions
@@ -291,7 +328,41 @@ which PRE holds are executed in order, until one succeed (chain of
291
328
  responsibility design pattern). This means that coercions always execute in
292
329
  <code>O(number of rules)</code>.
293
330
 
294
- ### <code>belongs\_to?</code> and <code>subdomain?</code>
331
+ ### Specifying converters
332
+
333
+ A converter is the third (resp. second) element specified in a coercion rules
334
+ (resp. an upon or fallback rule). A converter is generally a Proc of arity 2,
335
+ which is passed the source value and requested target domain.
336
+
337
+ Myrrha.coercions do |r|
338
+ r.coercion String, Numeric, lambda{|value,requested_domain|
339
+ # this is converter code
340
+ }
341
+ end
342
+ convert("12", Integer)
343
+
344
+ A converter may also be specified as an array of domains. In this case, it is
345
+ assumed that they for a path inside the convertion graph. Consider for example
346
+ the following coercion rules (contrived example)
347
+
348
+ rules = Myrrha.coercions do |r|
349
+ r.coercion String, Symbol, lambda{|s,t| s.to_sym } # 1
350
+ r.coercion Float, String, lambda{|s,t| s.to_s } # 2
351
+ r.coercion Integer, Float, lambda{|s,t| Float(s) } # 3
352
+ r.coercion Integer, Symbol, [Float, String] # 4
353
+ end
354
+
355
+ The last rule specifies a convertion path, through intermediate domains. The
356
+ complete rule specifies that applying the following path will work
357
+
358
+ Integer -> Float -> String -> Symbol
359
+ #3 #2 #1
360
+
361
+ Indeed,
362
+
363
+ rules.coerce(12, Symbol) # => :"12.0"
364
+
365
+ ### Semantics of <code>belongs\_to?</code> and <code>subdomain?</code>
295
366
 
296
367
  The pseudo-code given above relies on two main abstractions. Suppose the user
297
368
  makes a call to <code>coerce(value, requested_domain)</code>:
@@ -305,6 +376,8 @@ makes a call to <code>coerce(value, requested_domain)</code>:
305
376
 
306
377
  * <code>subdomain?(SourceDomain,TargetDomain)</code> is true iif
307
378
  * <code>SourceDomain == TargetDomain</code> yields true
379
+ * TargetDomain respond to <code>:superdomain_of?</code> and answers true on
380
+ SourceDomain
308
381
  * SourceDomain and TargetDomain are both classes and the latter is a super
309
382
  class of the former
310
383
 
@@ -334,4 +407,55 @@ makes a call to <code>coerce(value, requested_domain)</code>:
334
407
  # this is your last change, an Myrrha::Error will be raised if you fail
335
408
  end
336
409
 
337
- end
410
+ end
411
+
412
+ ### Factoring domains through specialization by constraint
413
+
414
+ Specialization by constraint (SByC) is a theory of types for which the following
415
+ rules hold:
416
+
417
+ * A type (aka domain) is a set of values
418
+ * A sub-type is a subset
419
+ * A sub-type can therefore be specified through a predicate on the super domain
420
+
421
+ For example, "positive integers" is a sub type of "integers" where the predicate
422
+ is "value > 0".
423
+
424
+ Myrrha comes with a small feature allowing you to create types 'ala' SByC:
425
+
426
+ PosInt = Myrrha.domain(Integer){|i| i > 0}
427
+ PosInt.name # => "PosInt"
428
+ PosInt.class # => Class
429
+ PosInt.superclass # => Integer
430
+ PosInt.ancestors # => [PosInt, Integer, Numeric, Comparable, Object, Kernel, BasicObject]
431
+ PosInt === 10 # => true
432
+ PosInt === -1 # => false
433
+ PosInt.new(10) # => 10
434
+ PosInt.new(-10) # => ArgumentError, "Invalid value -10 for PosInt"
435
+
436
+ Note that the feature is very limited, and is not intended to provide a truly
437
+ coherent typing framework. For example:
438
+
439
+ 10.is_a?(PosInt) # => false
440
+ 10.kind_of?(PosInt) # => false
441
+
442
+ Instead, Myrrha domains are only provided as an helper to build sound coercions
443
+ rules easily while 1) keeping a Class-based approach to source and target
444
+ domains and 2) having friendly error messages 3) really supporting true
445
+ reasoning on types and value:
446
+
447
+ # Only a rule that converts String to Integer
448
+ rules = Myrrha.coercions do |r|
449
+ r.coercion String, Integer, lambda{|s,t| Integer(s)}
450
+ end
451
+
452
+ # it succeeds on both integers and positive integers
453
+ rules.coerce("12", Integer) # => 12
454
+ rules.coerce("12", PosInt) # => 12
455
+
456
+ # and correctly fails in each case!
457
+ rules.coerce("-12", Integer) # => -12
458
+ rules.coerce("-12", PosInt) # => ArgumentError, "Invalid value -12 for PosInt"
459
+
460
+
461
+
data/examples/coerce.rb CHANGED
@@ -1,26 +1,46 @@
1
1
  require 'myrrha/with_core_ext'
2
2
  require 'myrrha/coerce'
3
3
 
4
- # it works on numerics
4
+ # NilClass -> _Anything_ returns nil, always
5
+ coerce(nil, Integer) # => nil
6
+
7
+ # Object -> String, via ruby's String()
8
+ coerce("hello", String) # => "hello"
9
+ coerce(:hello, String) # => "hello"
10
+
11
+ # String -> Numeric, through ruby's Integer() and Float()
5
12
  coerce("12", Integer) # => 12
6
13
  coerce("12.0", Float) # => 12.0
7
14
 
8
- # but also on regexp (through Regexp.compile)
15
+ # String -> Numeric is smart enough:
16
+ coerce("12", Numeric) # => 12 (Integer)
17
+ coerce("12.0", Numeric) # => 12.0 (Float)
18
+
19
+ # String -> Regexp, through Regexp.compile
9
20
  coerce("[a-z]+", Regexp) # => /[a-z]+/
10
21
 
11
- # and, yes, on Boolean (sorry Matz!)
22
+ # String -> Symbol, through to_sym
23
+ coerce("hello", Symbol) # => :hello
24
+
25
+ # String -> Boolean (hum, sorry Matz!)
12
26
  coerce("true", Boolean) # => true
13
27
  coerce("false", Boolean) # => false
14
28
 
15
- # and on date and time (through Date/Time.parse)
29
+ # String -> Date, through Date.parse
16
30
  require 'date'
17
- require 'time'
18
31
  coerce("2011-07-20", Date) # => #<Date: 2011-07-20 (4911525/2,0,2299161)>
32
+
33
+ # String -> Time, through Time.parse (just in time issuing of require('time'))
19
34
  coerce("2011-07-20 10:57", Time) # => 2011-07-20 10:57:00 +0200
20
35
 
21
- # why not on URI?
36
+ # String -> URI, through URI.parse
22
37
  require 'uri'
23
- coerce('http://google.com', URI) # => #<URI::HTTP:0x8281ce0 URL:http://google.com>
38
+ coerce('http://google.com', URI) # => #<URI::HTTP:0x8281ce0 URL:http://google.com>
39
+
40
+ # String -> Class and Module through constant lookup
41
+ coerce("Integer", Class) # => Integer
42
+ coerce("Myrrha::Version", Module) # => Myrrha::Version
24
43
 
25
- # on nil, it always returns nil
26
- coerce(nil, Integer) # => nil
44
+ # Symbol -> Class and Module through constant lookup
45
+ coerce(:Integer, Class) # => Integer
46
+ coerce(:Enumerable, Module) # => Enumerable
@@ -0,0 +1,38 @@
1
+ require 'myrrha'
2
+
3
+ PosInt = Myrrha.domain(Integer){|i| i>0}
4
+
5
+ ###
6
+
7
+ PosInt.class
8
+ PosInt.superclass
9
+ PosInt.ancestors
10
+ PosInt === 10
11
+ PosInt === -1
12
+ PosInt.new(10)
13
+ begin
14
+ PosInt.new(-10)
15
+ raise "Unexpected case: PosInt.new(-10) succeeds"
16
+ rescue ArgumentError => ex
17
+ puts ex.message
18
+ end
19
+
20
+ ###
21
+
22
+ 10.is_a?(PosInt)
23
+ 10.kind_of?(PosInt)
24
+
25
+ ###
26
+
27
+ rules = Myrrha.coercions do |r|
28
+ r.coercion String, Integer, lambda{|s,t| Integer(s)}
29
+ end
30
+ rules.coerce("12", Integer)
31
+ rules.coerce("12", PosInt)
32
+ rules.coerce("-12", Integer)
33
+ begin
34
+ rules.coerce("-12", PosInt)
35
+ raise "Unexpected case: rules.coerce('-12', PosInt) succeeds"
36
+ rescue Myrrha::Error => ex
37
+ puts ex.message
38
+ end
data/lib/myrrha.rb CHANGED
@@ -8,6 +8,23 @@ module Myrrha
8
8
  #
9
9
  class Error < StandardError; end
10
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
+ @sub_domains = subdoms
22
+ @super_domain = superdom
23
+ @predicate = pred
24
+ }
25
+ dom
26
+ end
27
+
11
28
  #
12
29
  # Builds a set of coercions rules.
13
30
  #
@@ -26,6 +43,58 @@ module Myrrha
26
43
  end
27
44
 
28
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
+ @super_domain
70
+ end
71
+
72
+ #
73
+ # Checks if `value` belongs to this domain
74
+ #
75
+ def ===(value)
76
+ (superclass === value) && @predicate.call(value)
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(@sub_domains).include?(child)
85
+ end
86
+
87
+ #
88
+ # Returns the specialization by constraint predicate
89
+ #
90
+ # @return [Proc] the domain predicate
91
+ #
92
+ def predicate
93
+ @predicate
94
+ end
95
+
96
+ end # module Domain
97
+
29
98
  # Defines a set of coercion rules
30
99
  #
31
100
  class Coercions
@@ -36,11 +105,12 @@ module Myrrha
36
105
  #
37
106
  # Creates an empty list of coercion rules
38
107
  #
39
- def initialize(upons = [], rules = [], fallbacks = [])
108
+ def initialize(upons = [], rules = [], fallbacks = [], main_target_domain = nil)
40
109
  @upons = upons
41
110
  @rules = rules
42
111
  @fallbacks = fallbacks
43
112
  @appender = :<<
113
+ @main_target_domain = main_target_domain
44
114
  yield(self) if block_given?
45
115
  end
46
116
 
@@ -183,11 +253,16 @@ module Myrrha
183
253
  error = nil
184
254
  each_rule do |from,to,converter|
185
255
  next unless from.nil? or belongs_to?(value, from, target_domain)
186
- next unless to.nil? or subdomain?(to, target_domain)
187
256
  begin
188
- catch(:nextrule){
189
- return convert(value, target_domain, converter)
190
- }
257
+ catch(:nextrule) do
258
+ if to.nil? or subdomain?(to, target_domain)
259
+ got = convert(value, target_domain, converter)
260
+ return got
261
+ elsif subdomain?(target_domain, to)
262
+ got = convert(value, to, converter)
263
+ return got if belongs_to?(got, target_domain)
264
+ end
265
+ end
191
266
  rescue => ex
192
267
  error = ex.message unless error
193
268
  end
@@ -233,10 +308,15 @@ module Myrrha
233
308
  # otherwise.
234
309
  #
235
310
  def subdomain?(child, parent)
236
- return true if child == parent
237
- (child.respond_to?(:superclass) && child.superclass) ?
238
- subdomain?(child.superclass, parent) :
311
+ if child == parent
312
+ true
313
+ elsif parent.respond_to?(:superdomain_of?)
314
+ parent.superdomain_of?(child)
315
+ elsif child.respond_to?(:superclass) && child.superclass
316
+ subdomain?(child.superclass, parent)
317
+ else
239
318
  false
319
+ end
240
320
  end
241
321
 
242
322
  #
@@ -246,7 +326,7 @@ module Myrrha
246
326
  # @return [Coercions] a copy of this set of rules
247
327
  #
248
328
  def dup
249
- Coercions.new(@upons.dup, @rules.dup, @fallbacks.dup)
329
+ Coercions.new(@upons.dup, @rules.dup, @fallbacks.dup, main_target_domain)
250
330
  end
251
331
 
252
332
  private
@@ -273,6 +353,9 @@ module Myrrha
273
353
  def convert(value, target_domain, converter)
274
354
  if converter.respond_to?(:call)
275
355
  converter.call(value, target_domain)
356
+ elsif converter.is_a?(Array)
357
+ path = converter + [target_domain]
358
+ path.inject(value){|cur,ndom| coerce(cur, ndom)}
276
359
  else
277
360
  raise ArgumentError, "Unable to use #{converter} for coercing"
278
361
  end
data/lib/myrrha/coerce.rb CHANGED
@@ -2,29 +2,12 @@ require 'myrrha'
2
2
  module Myrrha
3
3
 
4
4
  #
5
- # Defines the missing Boolean type.
5
+ # Defines the missing Boolean as a Myrrha's domain
6
6
  #
7
- # This module mimics a Ruby missing Boolean type.
8
- #
9
- module Boolean
7
+ Boolean = Myrrha.domain(Object, [TrueClass, FalseClass]){|x|
8
+ (x==true) || (x==false)
9
+ }
10
10
 
11
- #
12
- # Returns Object, as the superclass of Boolean
13
- #
14
- # @return [Class] Object
15
- #
16
- def self.superclass; Object; end
17
-
18
- #
19
- # Returns true if `val` is <code>true</code> or <code>false</code>, false
20
- # otherwise.
21
- #
22
- def self.===(val)
23
- (val == true) || (val == false)
24
- end
25
-
26
- end # module Boolean
27
-
28
11
  #
29
12
  # Coerces _s_ to a Boolean
30
13
  #
@@ -48,6 +31,14 @@ module Myrrha
48
31
  # Defines basic coercions for Ruby, mostly from String
49
32
  Coerce = coercions do |g|
50
33
 
34
+ # Returns a constant denoted by `s`
35
+ def g.constant_lookup(s, target_domain)
36
+ found = (s.split('::') - [""]).inject(Kernel){|cur,n|
37
+ cur.const_get(n.to_sym)
38
+ }
39
+ belongs_to?(found, target_domain) ? found : throw(:nextrule)
40
+ end
41
+
51
42
  # NilClass should return immediately
52
43
  g.upon(NilClass) do |s,t|
53
44
  nil
@@ -59,12 +50,18 @@ module Myrrha
59
50
  end
60
51
 
61
52
  # Specific basic rules
62
- g.coercion String, Integer, lambda{|s,t| Integer(s) }
63
- g.coercion String, Float, lambda{|s,t| Float(s) }
64
- g.coercion String, Boolean, lambda{|s,t| Boolean(s) }
65
- g.coercion Integer, Float, lambda{|s,t| Float(s) }
66
- g.coercion String, Symbol, lambda{|s,t| s.to_sym }
67
- g.coercion String, Regexp, lambda{|s,t| Regexp.compile(s) }
53
+ g.coercion Object, String, lambda{|s,t| String(s) }
54
+ g.coercion String, Integer, lambda{|s,t| Integer(s) }
55
+ g.coercion String, Float, lambda{|s,t| Float(s) }
56
+ g.coercion String, Boolean, lambda{|s,t| Boolean(s) }
57
+ g.coercion Integer, Float, lambda{|s,t| Float(s) }
58
+ g.coercion String, Symbol, lambda{|s,t| s.to_sym }
59
+ g.coercion String, Regexp, lambda{|s,t| Regexp.compile(s) }
60
+ g.coercion Symbol, Class, lambda{|s,t| g.constant_lookup(s.to_s, t) }
61
+ g.coercion Symbol, Module, lambda{|s,t| g.constant_lookup(s.to_s, t) }
62
+ g.coercion String, Class, lambda{|s,t| g.constant_lookup(s, t) }
63
+ g.coercion String, Module, lambda{|s,t| g.constant_lookup(s, t) }
64
+ g.coercion String, Time, lambda{|s,t| require 'time'; Time.parse(s) }
68
65
 
69
66
  # By default, we try to invoke :parse on the class
70
67
  g.fallback(String) do |s,t|
@@ -2,7 +2,7 @@ module Myrrha
2
2
  module Version
3
3
 
4
4
  MAJOR = 1
5
- MINOR = 0
5
+ MINOR = 1
6
6
  TINY = 0
7
7
 
8
8
  def self.to_s
data/myrrha.gemspec CHANGED
@@ -127,7 +127,7 @@ Gem::Specification.new do |s|
127
127
  s.add_development_dependency("bundler", "~> 1.0")
128
128
  s.add_development_dependency("rspec", "~> 2.6.0")
129
129
  s.add_development_dependency("yard", "~> 0.7.2")
130
- s.add_development_dependency("bluecloth", "~> 2.0.9")
130
+ s.add_development_dependency("bluecloth", "~> 2.1.0")
131
131
  s.add_development_dependency("wlang", "~> 0.10.1")
132
132
  s.add_development_dependency("noe", "~> 1.3.0")
133
133
 
data/myrrha.noespec CHANGED
@@ -9,7 +9,7 @@ variables:
9
9
  upper:
10
10
  Myrrha
11
11
  version:
12
- 1.0.0
12
+ 1.1.0
13
13
  summary: |-
14
14
  Myrrha provides the coercion framework which is missing to Ruby, IMHO.
15
15
  description: |-
@@ -29,7 +29,7 @@ variables:
29
29
  - {name: bundler, version: "~> 1.0", groups: [development]}
30
30
  - {name: rspec, version: "~> 2.6.0", groups: [development]}
31
31
  - {name: yard, version: "~> 0.7.2", groups: [development]}
32
- - {name: bluecloth, version: "~> 2.0.9", groups: [development]}
32
+ - {name: bluecloth, version: "~> 2.1.0", groups: [development]}
33
33
  - {name: wlang, version: "~> 0.10.1", groups: [development]}
34
34
  - {name: noe, version: "~> 1.3.0", groups: [development]}
35
35
  rake_tasks:
@@ -16,6 +16,13 @@ module Myrrha
16
16
  dupped.coerce("12", Float).should eql(12.0)
17
17
  lambda{ rules.coerce("12", Float) }.should raise_error(Myrrha::Error)
18
18
  end
19
+
20
+ it "should not forget main_target_domain" do
21
+ rules = Coercions.new do |r|
22
+ r.main_target_domain = Integer
23
+ end
24
+ rules.dup.main_target_domain.should eql(Integer)
25
+ end
19
26
 
20
27
  end
21
28
  end
@@ -1,11 +1,12 @@
1
1
  require 'spec_helper'
2
2
  module Myrrha
3
3
  describe "Coercions#subdomain?" do
4
- let(:graph){ Coercions.new }
4
+ let(:r){ Coercions.new }
5
5
 
6
6
  specify {
7
- graph.subdomain?(Symbol, Object).should be_true
7
+ r.subdomain?(Symbol, Object).should be_true
8
+ r.subdomain?(Class, Module).should be_true
8
9
  }
9
-
10
+
10
11
  end
11
12
  end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+ describe "Myrrha.coercions" do
3
+
4
+ it "should support using user-defined domains" do
5
+ name = Myrrha.domain{|s| s.is_a?(Symbol)}
6
+ rules = Myrrha.coercions do |r|
7
+ r.coercion String, name, lambda{|s,t| s.to_sym}
8
+ r.coercion name, String, lambda{|s,t| s.to_s}
9
+ end
10
+ rules.coerce("hello", name).should eq(:hello)
11
+ rules.coerce(:hello, String).should eq("hello")
12
+ end
13
+
14
+ end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+ module Myrrha
3
+ describe "#domain" do
4
+
5
+ specify "the basic contract" do
6
+ subject = Myrrha.domain{|s| s == 12}
7
+ subject.should be_a(Class)
8
+ subject.superclass.should eq(Object)
9
+ (subject === 12).should be_true
10
+ (subject === 13).should be_false
11
+ end
12
+
13
+ specify "with a ruby superclass" do
14
+ subject = Myrrha.domain(Integer){|i| i > 0}
15
+ subject.should be_a(Class)
16
+ subject.superclass.should eq(Integer)
17
+ (subject === 12).should be_true
18
+ (subject === 0).should be_false
19
+ end
20
+
21
+ describe "A factored sub domain of Integer" do
22
+ PosInt = Myrrha.domain(Integer){|i| i > 0}
23
+ specify("#name") {
24
+ PosInt.name.should eq("Myrrha::PosInt")
25
+ }
26
+ specify("#new") {
27
+ PosInt.new(12).should eq(12)
28
+ lambda {
29
+ PosInt.new(0)
30
+ }.should raise_error(ArgumentError)
31
+ }
32
+ specify("#superclass"){
33
+ PosInt.superclass.should eql(Integer)
34
+ }
35
+ specify("#superdomain_of?"){
36
+ PosInt.superdomain_of?(Object).should be_false
37
+ PosInt.superdomain_of?(Integer).should be_false
38
+ }
39
+ it "should be usable in a case" do
40
+ [-12, 12].collect{|i|
41
+ case i
42
+ when PosInt
43
+ :posint
44
+ when Integer
45
+ :integer
46
+ end
47
+ }.should eq([:integer, :posint])
48
+ end
49
+ end
50
+
51
+ describe "A factored sub domain of a user-defined Class" do
52
+ class Color
53
+ attr_reader :r
54
+ attr_reader :g
55
+ attr_reader :b
56
+ def initialize(r,g,b)
57
+ raise ArgumentError unless [r,g,b].all?{|i| i.is_a?(Integer)}
58
+ @r, @g, @b = r, g, b
59
+ end
60
+ end
61
+ RedToZero = Myrrha.domain(Color){|c| c.r == 0}
62
+ specify("#===") {
63
+ (RedToZero === Color.new(0,1,1)).should be_true
64
+ (RedToZero === Color.new(1,1,1)).should be_false
65
+ }
66
+ specify("#new") {
67
+ RedToZero.new(Color.new(0,1,1)).should be_a(Color)
68
+ RedToZero.new(0, 1, 1).should be_a(Color)
69
+ lambda{
70
+ RedToZero.new(Color.new(1,1,1))
71
+ }.should raise_error(ArgumentError)
72
+ lambda{
73
+ RedToZero.new(1, 1, 1)
74
+ }.should raise_error(ArgumentError)
75
+ }
76
+ end
77
+
78
+ end
79
+ end
data/spec/spec_helper.rb CHANGED
@@ -3,7 +3,6 @@ require 'myrrha/with_core_ext'
3
3
  require 'myrrha/coerce'
4
4
  require 'myrrha/to_ruby_literal'
5
5
  require 'date'
6
- require 'time'
7
6
  require 'shared/a_value'
8
7
 
9
8
  unless defined?(SAFE_VALUES)
data/spec/test_coerce.rb CHANGED
@@ -8,6 +8,18 @@ describe "::Ruby's coercion " do
8
8
  end
9
9
  end
10
10
 
11
+ describe "to String" do
12
+ specify "from Integer" do
13
+ coerce(12, String).should eql("12")
14
+ end
15
+ specify "from String" do
16
+ coerce("12", String).should eql("12")
17
+ end
18
+ specify "from NilClass" do
19
+ coerce(nil, String).should be_nil
20
+ end
21
+ end
22
+
11
23
  describe "to Integer" do
12
24
  specify "from Integer" do
13
25
  coerce(12, Integer).should eql(12)
@@ -62,6 +74,20 @@ describe "::Ruby's coercion " do
62
74
  lambda{coerce("abc", Boolean)}.should raise_error(Myrrha::Error)
63
75
  end
64
76
  end
77
+
78
+ describe "to TrueClass" do
79
+ specify "from String" do
80
+ coerce("true", TrueClass).should eql(true)
81
+ lambda{ coerce("false", TrueClass) }.should raise_error(Myrrha::Error)
82
+ end
83
+ end
84
+
85
+ describe "to FalseClass" do
86
+ specify "from String" do
87
+ coerce("false", FalseClass).should eql(false)
88
+ lambda{ coerce("true", FalseClass) }.should raise_error(Myrrha::Error)
89
+ end
90
+ end
65
91
 
66
92
  describe "to Date" do
67
93
  let(:expected){ Date.parse("2011-07-20") }
@@ -102,6 +128,38 @@ describe "::Ruby's coercion " do
102
128
  coerce("http://www.google.com/", URI).should eql(URI.parse("http://www.google.com/"))
103
129
  end
104
130
  end
131
+
132
+ describe "to Module" do
133
+ specify "from String" do
134
+ coerce("Kernel", Module).should eql(Kernel)
135
+ coerce("::Kernel", Module).should eql(Kernel)
136
+ coerce("Myrrha::Version", Module).should eql(Myrrha::Version)
137
+ coerce("::Myrrha::Version", Module).should eql(Myrrha::Version)
138
+ coerce("Myrrha::Coercions", Module).should eql(Myrrha::Coercions)
139
+ end
140
+ specify "from Symbol" do
141
+ coerce(:Kernel, Module).should eql(Kernel)
142
+ end
143
+ it "should raise error if not a module" do
144
+ lambda{
145
+ coerce("Myrrha::VERSION", Module)
146
+ }.should raise_error(Myrrha::Error)
147
+ end
148
+ end
149
+
150
+ describe "to Class" do
151
+ specify "from String" do
152
+ coerce("Myrrha::Coercions", Class).should eql(Myrrha::Coercions)
153
+ end
154
+ specify "from Symbol" do
155
+ coerce(:Integer, Class).should eql(Integer)
156
+ end
157
+ it "should raise error if not a module" do
158
+ lambda{
159
+ coerce("Myrrha::Version", Class)
160
+ }.should raise_error(Myrrha::Error)
161
+ end
162
+ end
105
163
 
106
164
  specify "to a class that respond to coerce" do
107
165
  class Coerceable
data/spec/test_myrrha.rb CHANGED
@@ -84,4 +84,41 @@ describe Myrrha do
84
84
  rules.coerce("hello", Symbol).should eq(:HELLO)
85
85
  end
86
86
 
87
+ it "should used superdomain rules in an optimistic strategy" do
88
+ rules = Myrrha.coercions do |c|
89
+ c.coercion String, Numeric, lambda{|s,t| Integer(s)}
90
+ end
91
+ rules.coerce("12", Integer).should eql(12)
92
+ lambda{ rules.coerce("12", Float) }.should raise_error(Myrrha::Error)
93
+ end
94
+
95
+ describe "path convertions" do
96
+ let(:rules){
97
+ Myrrha.coercions do |c|
98
+ c.coercion Integer, String, lambda{|s,t| s.to_s}
99
+ c.coercion String, Float, lambda{|s,t| Float(s)}
100
+ c.coercion Integer, Float, [String]
101
+ c.coercion Float, String, lambda{|s,t| s.to_s}
102
+ c.coercion String, Symbol, lambda{|s,t| s.to_sym}
103
+ c.coercion Integer, Symbol, [Float, String]
104
+ end
105
+ }
106
+ it "should work with a simple and single" do
107
+ rules.coerce(12, Float).should eql(12.0)
108
+ end
109
+ it "should work with a complex and multiple path" do
110
+ rules.coerce(12, Symbol).should eql(:"12.0")
111
+ end
112
+ end
113
+
114
+ specify "path convertions (from CHANGELOG)" do
115
+ rules = Myrrha.coercions do |r|
116
+ r.coercion String, Symbol, lambda{|s,t| s.to_sym }
117
+ r.coercion Float, String, lambda{|s,t| s.to_s }
118
+ r.coercion Integer, Float, lambda{|s,t| Float(s) }
119
+ r.coercion Integer, Symbol, [Float, String]
120
+ end
121
+ rules.coerce(12, Symbol).should eql(:"12.0")
122
+ end
123
+
87
124
  end
data/tasks/examples.rake CHANGED
@@ -6,6 +6,7 @@ task :examples do
6
6
  print '.'
7
7
  else
8
8
  print '*'
9
+ raise "Example #{file} failed."
9
10
  end
10
11
  end
11
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: myrrha
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-07-22 00:00:00.000000000Z
12
+ date: 2011-07-28 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
16
- requirement: &77746150 !ruby/object:Gem::Requirement
16
+ requirement: &85576310 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.9.2
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *77746150
24
+ version_requirements: *85576310
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: bundler
27
- requirement: &77745820 !ruby/object:Gem::Requirement
27
+ requirement: &85575520 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '1.0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *77745820
35
+ version_requirements: *85575520
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rspec
38
- requirement: &77745530 !ruby/object:Gem::Requirement
38
+ requirement: &85574670 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 2.6.0
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *77745530
46
+ version_requirements: *85574670
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: yard
49
- requirement: &77745240 !ruby/object:Gem::Requirement
49
+ requirement: &85573700 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,21 +54,21 @@ dependencies:
54
54
  version: 0.7.2
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *77745240
57
+ version_requirements: *85573700
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: bluecloth
60
- requirement: &77744950 !ruby/object:Gem::Requirement
60
+ requirement: &85572820 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
64
64
  - !ruby/object:Gem::Version
65
- version: 2.0.9
65
+ version: 2.1.0
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *77744950
68
+ version_requirements: *85572820
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: wlang
71
- requirement: &77744660 !ruby/object:Gem::Requirement
71
+ requirement: &85550940 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ~>
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: 0.10.1
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *77744660
79
+ version_requirements: *85550940
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: noe
82
- requirement: &77744360 !ruby/object:Gem::Requirement
82
+ requirement: &85550220 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ~>
@@ -87,7 +87,7 @@ dependencies:
87
87
  version: 1.3.0
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *77744360
90
+ version_requirements: *85550220
91
91
  description: ! "Myrrha provides the coercion framework which is missing to Ruby, IMHO.
92
92
  Coercions\nare simply defined as a set of rules for converting values from source
93
93
  to target\ndomains (in an abstract sense). As a typical and useful example, it comes
@@ -113,6 +113,7 @@ files:
113
113
  - examples/String#toXXX.rb
114
114
  - examples/examples_helper.rb
115
115
  - examples/coerce_foo2.rb
116
+ - examples/sbyc_domain.rb
116
117
  - examples/to_ruby_literal_foo3.rb
117
118
  - examples/to_ruby_literal_noext.rb
118
119
  - examples/coerce_foo3.rb
@@ -123,6 +124,8 @@ files:
123
124
  - lib/myrrha/version.rb
124
125
  - lib/myrrha.rb
125
126
  - spec/spec_helper.rb
127
+ - spec/myrrha/test_coercions.rb
128
+ - spec/myrrha/test_domain.rb
126
129
  - spec/test_myrrha.rb
127
130
  - spec/test_to_ruby_literal.rb
128
131
  - spec/test_value.rb
@@ -164,7 +167,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
164
167
  version: '0'
165
168
  segments:
166
169
  - 0
167
- hash: -508754947
170
+ hash: -900430961
168
171
  required_rubygems_version: !ruby/object:Gem::Requirement
169
172
  none: false
170
173
  requirements:
@@ -173,15 +176,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
173
176
  version: '0'
174
177
  segments:
175
178
  - 0
176
- hash: -508754947
179
+ hash: -900430961
177
180
  requirements: []
178
181
  rubyforge_project:
179
- rubygems_version: 1.8.5
182
+ rubygems_version: 1.8.6
180
183
  signing_key:
181
184
  specification_version: 3
182
185
  summary: Myrrha provides the coercion framework which is missing to Ruby, IMHO.
183
186
  test_files:
184
187
  - spec/spec_helper.rb
188
+ - spec/myrrha/test_coercions.rb
189
+ - spec/myrrha/test_domain.rb
185
190
  - spec/test_myrrha.rb
186
191
  - spec/test_to_ruby_literal.rb
187
192
  - spec/test_value.rb