myrrha 1.0.0

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