myrrha 1.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.
Files changed (49) hide show
  1. data/CHANGELOG.md +5 -0
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +41 -0
  4. data/LICENCE.md +22 -0
  5. data/Manifest.txt +13 -0
  6. data/README.md +337 -0
  7. data/Rakefile +23 -0
  8. data/examples/String#toXXX.rb +40 -0
  9. data/examples/coerce.rb +26 -0
  10. data/examples/coerce_foo.rb +12 -0
  11. data/examples/coerce_foo2.rb +17 -0
  12. data/examples/coerce_foo3.rb +23 -0
  13. data/examples/coerce_intro.rb +10 -0
  14. data/examples/coerce_noext.rb +6 -0
  15. data/examples/examples_helper.rb +17 -0
  16. data/examples/friendly_but_safe_api.rb +73 -0
  17. data/examples/to_ruby_literal.rb +16 -0
  18. data/examples/to_ruby_literal_foo.rb +14 -0
  19. data/examples/to_ruby_literal_foo2.rb +17 -0
  20. data/examples/to_ruby_literal_foo3.rb +21 -0
  21. data/examples/to_ruby_literal_noext.rb +5 -0
  22. data/lib/myrrha/coerce.rb +93 -0
  23. data/lib/myrrha/loader.rb +0 -0
  24. data/lib/myrrha/to_ruby_literal.rb +76 -0
  25. data/lib/myrrha/version.rb +14 -0
  26. data/lib/myrrha/with_core_ext.rb +2 -0
  27. data/lib/myrrha.rb +295 -0
  28. data/myrrha.gemspec +191 -0
  29. data/myrrha.noespec +37 -0
  30. data/spec/coercions/test_append.rb +11 -0
  31. data/spec/coercions/test_belongs_to.rb +29 -0
  32. data/spec/coercions/test_convert.rb +23 -0
  33. data/spec/coercions/test_dup.rb +21 -0
  34. data/spec/coercions/test_subdomain.rb +11 -0
  35. data/spec/shared/a_value.rb +33 -0
  36. data/spec/spec_helper.rb +34 -0
  37. data/spec/test_assumptions.rb +18 -0
  38. data/spec/test_coerce.rb +119 -0
  39. data/spec/test_myrrha.rb +87 -0
  40. data/spec/test_to_ruby_literal.rb +19 -0
  41. data/spec/test_value.rb +6 -0
  42. data/tasks/debug_mail.rake +78 -0
  43. data/tasks/debug_mail.txt +13 -0
  44. data/tasks/examples.rake +13 -0
  45. data/tasks/gem.rake +68 -0
  46. data/tasks/spec_test.rake +79 -0
  47. data/tasks/unit_test.rake +77 -0
  48. data/tasks/yard.rake +51 -0
  49. metadata +195 -0
@@ -0,0 +1,16 @@
1
+ require 'date'
2
+ require 'myrrha/with_core_ext'
3
+ require 'myrrha/to_ruby_literal'
4
+
5
+ require 'date'
6
+ require 'myrrha/with_core_ext'
7
+ require 'myrrha/to_ruby_literal'
8
+
9
+ 1.to_ruby_literal # => "1"
10
+ Date.today.to_ruby_literal # => "Marshal.load('...')"
11
+ ["hello", Date.today].to_ruby_literal # => "['hello', Marshal.load('...')]"
12
+
13
+ (1..10).to_ruby_literal # => "1..10"
14
+
15
+ today = Date.today
16
+ (today..today+1).to_ruby_literal # => "Marshal.load('...')"
@@ -0,0 +1,14 @@
1
+ require "myrrha/to_ruby_literal"
2
+
3
+ class Foo
4
+ attr_reader :arg
5
+ def initialize(arg)
6
+ @arg = arg
7
+ end
8
+ def to_ruby_literal
9
+ "Foo.new(#{arg.inspect})"
10
+ end
11
+ end
12
+
13
+ Myrrha.to_ruby_literal(Foo.new(:hello))
14
+ # => "Foo.new(:hello)"
@@ -0,0 +1,17 @@
1
+ require "myrrha/to_ruby_literal"
2
+
3
+ class Foo
4
+ attr_reader :arg
5
+ def initialize(arg)
6
+ @arg = arg
7
+ end
8
+ end
9
+
10
+ Myrrha::ToRubyLiteral.append do |r|
11
+ r.coercion(Foo) do |foo, _|
12
+ "Foo.new(#{foo.arg.inspect})"
13
+ end
14
+ end
15
+
16
+ Myrrha.to_ruby_literal(Foo.new(:hello))
17
+ # => "Foo.new(:hello)"
@@ -0,0 +1,21 @@
1
+ require "myrrha/to_ruby_literal"
2
+
3
+ class Foo
4
+ attr_reader :arg
5
+ def initialize(arg)
6
+ @arg = arg
7
+ end
8
+ end
9
+
10
+ MyRules = Myrrha::ToRubyLiteral.dup.append do |r|
11
+ r.coercion(Foo) do |foo, _|
12
+ "Foo.new(#{foo.arg.inspect})"
13
+ end
14
+ end
15
+
16
+ # Myrrha.to_ruby_literal is actually a shortcut for:
17
+ Myrrha::ToRubyLiteral.apply(Foo.new(:hello))
18
+ # => "Marshal.load('...')"
19
+
20
+ MyRules.apply(Foo.new(:hello))
21
+ # => "Foo.new(:hello)"
@@ -0,0 +1,5 @@
1
+ require 'date'
2
+ require 'myrrha/to_ruby_literal'
3
+
4
+ Myrrha.to_ruby_literal(1) # => 1
5
+ Myrrha.to_ruby_literal(Date.today) # => Marshal.load("...")
@@ -0,0 +1,93 @@
1
+ require 'myrrha'
2
+ module Myrrha
3
+
4
+ #
5
+ # Defines the missing Boolean type.
6
+ #
7
+ # This module mimics a Ruby missing Boolean type.
8
+ #
9
+ module Boolean
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
+ #
29
+ # Coerces _s_ to a Boolean
30
+ #
31
+ # This method mimics Ruby's Integer(), Float(), etc. for Boolean values.
32
+ #
33
+ # @param [Object] s a Boolean or a String
34
+ # @return [Boolean] true if `s` is already true of the string 'true',
35
+ # false if `s` is already false of the string 'false'.
36
+ # @raise [ArgumentError] if `s` cannot be coerced to a boolean.
37
+ #
38
+ def self.Boolean(s)
39
+ if (s==true || s.to_str.strip == "true")
40
+ true
41
+ elsif (s==false || s.to_str.strip == "false")
42
+ false
43
+ else
44
+ raise ArgumentError, "invalid value for Boolean: \"#{s}\""
45
+ end
46
+ end
47
+
48
+ # Defines basic coercions for Ruby, mostly from String
49
+ Coerce = coercions do |g|
50
+
51
+ # NilClass should return immediately
52
+ g.upon(NilClass) do |s,t|
53
+ nil
54
+ end
55
+
56
+ # Use t.coerce if it exists
57
+ g.upon(lambda{|s,t| t.respond_to?(:coerce)}) do |s,t|
58
+ t.coerce(s)
59
+ end
60
+
61
+ # 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) }
68
+
69
+ # By default, we try to invoke :parse on the class
70
+ g.fallback(String) do |s,t|
71
+ t.respond_to?(:parse) ? t.parse(s.to_str) : throw(:nextrule)
72
+ end
73
+
74
+ end # Coerce
75
+
76
+ def self.coerce(value, domain)
77
+ Coerce.apply(value, domain)
78
+ end
79
+
80
+ end # module Myrrha
81
+
82
+ if Myrrha.core_ext?
83
+ Boolean = Myrrha::Boolean
84
+ def Boolean(s)
85
+ Myrrha::Boolean(s)
86
+ end
87
+ class Object
88
+ private
89
+ def coerce(value, domain)
90
+ Myrrha.coerce(value, domain)
91
+ end
92
+ end
93
+ end
File without changes
@@ -0,0 +1,76 @@
1
+ require 'myrrha'
2
+ module Myrrha
3
+
4
+ # These are all classes for which using inspect is safe for to_ruby_literal
5
+ TO_RUBY_THROUGH_INSPECT = [ NilClass, TrueClass, FalseClass,
6
+ Fixnum, Bignum, Float,
7
+ String, Symbol, Class, Module, Regexp ]
8
+
9
+ # Defines basic coercions for implementing to_ruby_literal
10
+ ToRubyLiteral = coercions do |r|
11
+ r.main_target_domain = :to_ruby_literal
12
+
13
+ r.upon(Object) do |s,t|
14
+ s.to_ruby_literal{ throw :nextrule }
15
+ end
16
+
17
+ # On safe .inspect
18
+ safe = lambda{|x| TO_RUBY_THROUGH_INSPECT.include?(x.class)}
19
+ r.coercion(safe) do |s,t|
20
+ s.inspect
21
+ end
22
+
23
+ # Best-effort on Range or let it be marshalled
24
+ r.coercion(Range) do |s,t|
25
+ (TO_RUBY_THROUGH_INSPECT.include?(s.first.class) &&
26
+ TO_RUBY_THROUGH_INSPECT.include?(s.last.class)) ?
27
+ s.inspect : throw(:nextrule)
28
+ end
29
+
30
+ # Be friendly on array
31
+ r.coercion(Array) do |s,t|
32
+ "[" + s.collect{|v| r.apply(v)}.join(', ') + "]"
33
+ end
34
+
35
+ # As well as on Hash
36
+ r.coercion(Hash) do |s,t|
37
+ "{" + s.collect{|k,v|
38
+ r.apply(k) + " => " + r.apply(v)
39
+ }.join(', ') + "}"
40
+ end
41
+
42
+ # Use Marshal by default
43
+ r.fallback(Object) do |s,t|
44
+ "Marshal.load(#{Marshal.dump(s).inspect})"
45
+ end
46
+
47
+ end
48
+
49
+ #
50
+ # Converts `value` to a ruby literal
51
+ #
52
+ # @param [Object] value any ruby value
53
+ # @return [String] a representation `s` of `value` such that
54
+ # <code>Kernel.eval(s) == value</code> is true
55
+ #
56
+ def self.to_ruby_literal(value = self)
57
+ block_given? ?
58
+ yield :
59
+ ToRubyLiteral.apply(value)
60
+ end
61
+
62
+ end # module Myrrha
63
+
64
+ class Object
65
+
66
+ #
67
+ # Converts self to a ruby literal
68
+ #
69
+ # @return [String] a representation `s` of self such that
70
+ # <code>Kernel.eval(s) == value</code> is true
71
+ #
72
+ def to_ruby_literal
73
+ block_given? ? yield : Myrrha.to_ruby_literal(self)
74
+ end
75
+
76
+ end if Myrrha.core_ext?
@@ -0,0 +1,14 @@
1
+ module Myrrha
2
+ module Version
3
+
4
+ MAJOR = 1
5
+ MINOR = 0
6
+ TINY = 0
7
+
8
+ def self.to_s
9
+ [ MAJOR, MINOR, TINY ].join('.')
10
+ end
11
+
12
+ end
13
+ VERSION = Version.to_s
14
+ end
@@ -0,0 +1,2 @@
1
+ require 'myrrha'
2
+ Myrrha::OPTIONS[:core_ext] = true
data/lib/myrrha.rb ADDED
@@ -0,0 +1,295 @@
1
+ #
2
+ # Myrrha -- the missing coercion framework for Ruby
3
+ #
4
+ module Myrrha
5
+
6
+ #
7
+ # Raised when a coercion fails
8
+ #
9
+ class Error < StandardError; end
10
+
11
+ #
12
+ # Builds a set of coercions rules.
13
+ #
14
+ # Example:
15
+ #
16
+ # rules = Myrrha.coercions do |c|
17
+ # c.coercion String, Integer, lambda{|s,t| Integer(s)}
18
+ # #
19
+ # # [...]
20
+ # #
21
+ # c.fallback String, lambda{|s,t| ... }
22
+ # end
23
+ #
24
+ def self.coercions(&block)
25
+ Coercions.new(&block)
26
+ end
27
+
28
+ #
29
+ # Defines a set of coercion rules
30
+ #
31
+ class Coercions
32
+
33
+ # @return [Domain] The main target domain, if any
34
+ attr_accessor :main_target_domain
35
+
36
+ #
37
+ # Creates an empty list of coercion rules
38
+ #
39
+ def initialize(upons = [], rules = [], fallbacks = [])
40
+ @upons = upons
41
+ @rules = rules
42
+ @fallbacks = fallbacks
43
+ @appender = :<<
44
+ yield(self) if block_given?
45
+ end
46
+
47
+ #
48
+ # Appends the list of rules with new ones.
49
+ #
50
+ # New upon, coercion and fallback rules will be put after the already
51
+ # existing ones, in each case.
52
+ #
53
+ # Example:
54
+ #
55
+ # rules = Myrrha.coercions do ... end
56
+ # rules.append do |r|
57
+ #
58
+ # # [previous coercion rules would come here]
59
+ #
60
+ # # install new rules
61
+ # r.coercion String, Float, lambda{|v,t| Float(t)}
62
+ # end
63
+ #
64
+ def append(&proc)
65
+ extend_rules(:<<, proc)
66
+ end
67
+
68
+ #
69
+ # Prepends the list of rules with new ones.
70
+ #
71
+ # New upon, coercion and fallback rules will be put before the already
72
+ # existing ones, in each case.
73
+ #
74
+ # Example:
75
+ #
76
+ # rules = Myrrha.coercions do ... end
77
+ # rules.prepend do |r|
78
+ #
79
+ # # install new rules
80
+ # r.coercion String, Float, lambda{|v,t| Float(t)}
81
+ #
82
+ # # [previous coercion rules would come here]
83
+ #
84
+ # end
85
+ #
86
+ def prepend(&proc)
87
+ extend_rules(:unshift, proc)
88
+ end
89
+
90
+ #
91
+ # Adds an upon rule for a source domain.
92
+ #
93
+ # Example:
94
+ #
95
+ # Myrrha.coercions do |r|
96
+ #
97
+ # # Don't even try something else on nil
98
+ # r.upon(NilClass){|s,t| nil}
99
+ # [...]
100
+ #
101
+ # end
102
+ #
103
+ # @param source [Domain] a source domain (mimic Domain)
104
+ # @param converter [Converter] an optional converter (mimic Converter)
105
+ # @param convproc [Proc] used when converter is not specified
106
+ # @return self
107
+ #
108
+ def upon(source, converter = nil, &convproc)
109
+ @upons.send(@appender, [source, nil, converter || convproc])
110
+ self
111
+ end
112
+
113
+ #
114
+ # Adds a coercion rule from a source to a target domain.
115
+ #
116
+ # The conversion can be provided through `converter` or via a block
117
+ # directly. See main documentation about recognized converters.
118
+ #
119
+ # Example:
120
+ #
121
+ # Myrrha.coercions do |r|
122
+ #
123
+ # # With an explicit proc
124
+ # r.coercion String, Integer, lambda{|v,t|
125
+ # Integer(v)
126
+ # }
127
+ #
128
+ # # With an implicit proc
129
+ # r.coercion(String, Float) do |v,t|
130
+ # Float(v)
131
+ # end
132
+ #
133
+ # end
134
+ #
135
+ # @param source [Domain] a source domain (mimicing Domain)
136
+ # @param target [Domain] a target domain (mimicing Domain)
137
+ # @param converter [Converter] an optional converter (mimic Converter)
138
+ # @param convproc [Proc] used when converter is not specified
139
+ # @return self
140
+ #
141
+ def coercion(source, target = main_target_domain, converter = nil, &convproc)
142
+ @rules.send(@appender, [source, target, converter || convproc])
143
+ self
144
+ end
145
+
146
+ #
147
+ # Adds a fallback rule for a source domain.
148
+ #
149
+ # Example:
150
+ #
151
+ # Myrrha.coercions do |r|
152
+ #
153
+ # # Add a 'last chance' rule for Strings
154
+ # r.fallback(String) do |v,t|
155
+ # # the user wants _v_ to be converted to a value of domain _t_
156
+ # end
157
+ #
158
+ # end
159
+ #
160
+ # @param source [Domain] a source domain (mimic Domain)
161
+ # @param converter [Converter] an optional converter (mimic Converter)
162
+ # @param convproc [Proc] used when converter is not specified
163
+ # @return self
164
+ #
165
+ def fallback(source, converter = nil, &convproc)
166
+ @fallbacks.send(@appender, [source, nil, converter || convproc])
167
+ self
168
+ end
169
+
170
+ #
171
+ # Coerces `value` to an element of `target_domain`
172
+ #
173
+ # This method tries each coercion rule, then each fallback in turn. Rules
174
+ # for which source and target domain match are executed until one succeeds.
175
+ # A Myrrha::Error is raised if no rule matches or executes successfuly.
176
+ #
177
+ # @param [Object] value any ruby value
178
+ # @param [Domain] target_domain a target domain to convert to (mimic Domain)
179
+ # @return self
180
+ #
181
+ def coerce(value, target_domain = main_target_domain)
182
+ return value if belongs_to?(value, target_domain)
183
+ error = nil
184
+ each_rule do |from,to,converter|
185
+ next unless from.nil? or belongs_to?(value, from, target_domain)
186
+ next unless to.nil? or subdomain?(to, target_domain)
187
+ begin
188
+ catch(:nextrule){
189
+ return convert(value, target_domain, converter)
190
+ }
191
+ rescue => ex
192
+ error = ex.message unless error
193
+ end
194
+ end
195
+ msg = "Unable to coerce `#{value}` to #{target_domain}"
196
+ msg += " (#{error})" if error
197
+ raise Error, msg
198
+ end
199
+ alias :apply :coerce
200
+
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
+ case domain
211
+ when Proc
212
+ if domain.arity == 2
213
+ domain.call(value, target_domain)
214
+ elsif RUBY_VERSION < "1.9"
215
+ domain.call(value)
216
+ elsif domain
217
+ domain === value
218
+ end
219
+ else
220
+ domain.respond_to?(:===) ?
221
+ domain === value :
222
+ false
223
+ end
224
+ end
225
+
226
+ #
227
+ # Returns `true` if `child` can be considered a valid sub domain of
228
+ # `parent`, false otherwise.
229
+ #
230
+ # @param [Domain] child a domain (mimic Domain)
231
+ # @param [Domain] parent another domain (mimic Domain)
232
+ # @return [Boolean] true if `child` is a subdomain of `parent`, false
233
+ # otherwise.
234
+ #
235
+ def subdomain?(child, parent)
236
+ return true if child == parent
237
+ (child.respond_to?(:superclass) && child.superclass) ?
238
+ subdomain?(child.superclass, parent) :
239
+ false
240
+ end
241
+
242
+ #
243
+ # Duplicates this set of rules in such a way that the original will not
244
+ # be affected by any change made to the copy.
245
+ #
246
+ # @return [Coercions] a copy of this set of rules
247
+ #
248
+ def dup
249
+ Coercions.new(@upons.dup, @rules.dup, @fallbacks.dup)
250
+ end
251
+
252
+ private
253
+
254
+ # Extends existing rules
255
+ def extend_rules(appender, block)
256
+ @appender = appender
257
+ block.call(self)
258
+ self
259
+ end
260
+
261
+ #
262
+ # Yields each rule in turn (upons, coercions then fallbacks)
263
+ #
264
+ def each_rule(&proc)
265
+ @upons.each(&proc)
266
+ @rules.each(&proc)
267
+ @fallbacks.each(&proc)
268
+ end
269
+
270
+ #
271
+ # Calls converter on a (value,target_domain) pair.
272
+ #
273
+ def convert(value, target_domain, converter)
274
+ if converter.respond_to?(:call)
275
+ converter.call(value, target_domain)
276
+ else
277
+ raise ArgumentError, "Unable to use #{converter} for coercing"
278
+ end
279
+ end
280
+
281
+ end # class Coercions
282
+
283
+ # Myrrha main options
284
+ OPTIONS = {
285
+ :core_ext => false
286
+ }
287
+
288
+ # Install core extensions?
289
+ def self.core_ext?
290
+ OPTIONS[:core_ext]
291
+ end
292
+
293
+ end # module Myrrha
294
+ require "myrrha/version"
295
+ require "myrrha/loader"