gorillib-model 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +12 -0
  3. data/README.md +21 -0
  4. data/Rakefile +15 -0
  5. data/gorillib-model.gemspec +27 -0
  6. data/lib/gorillib/builder.rb +239 -0
  7. data/lib/gorillib/core_ext/datetime.rb +23 -0
  8. data/lib/gorillib/core_ext/exception.rb +153 -0
  9. data/lib/gorillib/core_ext/module.rb +10 -0
  10. data/lib/gorillib/core_ext/object.rb +14 -0
  11. data/lib/gorillib/model/base.rb +273 -0
  12. data/lib/gorillib/model/collection/model_collection.rb +157 -0
  13. data/lib/gorillib/model/collection.rb +200 -0
  14. data/lib/gorillib/model/defaults.rb +115 -0
  15. data/lib/gorillib/model/errors.rb +24 -0
  16. data/lib/gorillib/model/factories.rb +555 -0
  17. data/lib/gorillib/model/field.rb +168 -0
  18. data/lib/gorillib/model/lint.rb +24 -0
  19. data/lib/gorillib/model/named_schema.rb +53 -0
  20. data/lib/gorillib/model/positional_fields.rb +35 -0
  21. data/lib/gorillib/model/schema_magic.rb +163 -0
  22. data/lib/gorillib/model/serialization/csv.rb +60 -0
  23. data/lib/gorillib/model/serialization/json.rb +44 -0
  24. data/lib/gorillib/model/serialization/lines.rb +30 -0
  25. data/lib/gorillib/model/serialization/to_wire.rb +54 -0
  26. data/lib/gorillib/model/serialization/tsv.rb +53 -0
  27. data/lib/gorillib/model/serialization.rb +41 -0
  28. data/lib/gorillib/model/type/extended.rb +83 -0
  29. data/lib/gorillib/model/type/ip_address.rb +153 -0
  30. data/lib/gorillib/model/type/url.rb +11 -0
  31. data/lib/gorillib/model/validate.rb +22 -0
  32. data/lib/gorillib/model/version.rb +5 -0
  33. data/lib/gorillib/model.rb +34 -0
  34. data/spec/builder_spec.rb +193 -0
  35. data/spec/core_ext/datetime_spec.rb +41 -0
  36. data/spec/core_ext/exception.rb +98 -0
  37. data/spec/core_ext/object.rb +45 -0
  38. data/spec/model/collection_spec.rb +290 -0
  39. data/spec/model/defaults_spec.rb +104 -0
  40. data/spec/model/factories_spec.rb +323 -0
  41. data/spec/model/lint_spec.rb +28 -0
  42. data/spec/model/serialization/csv_spec.rb +30 -0
  43. data/spec/model/serialization/tsv_spec.rb +28 -0
  44. data/spec/model/serialization_spec.rb +41 -0
  45. data/spec/model/type/extended_spec.rb +166 -0
  46. data/spec/model/type/ip_address_spec.rb +141 -0
  47. data/spec/model_spec.rb +261 -0
  48. data/spec/spec_helper.rb +15 -0
  49. data/spec/support/capture_output.rb +28 -0
  50. data/spec/support/nuke_constants.rb +9 -0
  51. data/spec/support/shared_context_for_builders.rb +59 -0
  52. data/spec/support/shared_context_for_models.rb +55 -0
  53. data/spec/support/shared_examples_for_factories.rb +71 -0
  54. data/spec/support/shared_examples_for_model_fields.rb +62 -0
  55. data/spec/support/shared_examples_for_models.rb +87 -0
  56. metadata +193 -0
@@ -0,0 +1,115 @@
1
+ module Gorillib
2
+ module Model
3
+
4
+ Field.class_eval do
5
+ field :default, :whatever
6
+
7
+ # @return [true, false] true if the field has a default value/proc set
8
+ def has_default?
9
+ attribute_set?(:default)
10
+ end
11
+ end
12
+
13
+ # FieldDefaults allows defaults to be declared for your fields
14
+ #
15
+ # Defaults are declared by passing the :default option to the field
16
+ # class method. If you need the default to be dynamic, pass a lambda, Proc,
17
+ # or any object that responds to #call as the value to the :default option
18
+ # and the result will calculated on initialization. These dynamic defaults
19
+ # can depend on the values of other fields.
20
+ #
21
+ # @example Usage
22
+ # class Person
23
+ # field :first_name, String, :default => "John"
24
+ # field :last_name, String, :default => "Doe"
25
+ # end
26
+ #
27
+ # person = Person.new
28
+ # person.first_name #=> "John"
29
+ # person.last_name #=> "Doe"
30
+ #
31
+ # @example Dynamic Default
32
+ # class Event
33
+ # field :start_date, Date
34
+ # field :end_date, Date, :default => ->{ start_date }
35
+ # end
36
+ #
37
+ # event = Event.receive(:start_date => "2012-01-01")
38
+ # event.end_date.to_s #=> "2012-01-01"
39
+ #
40
+
41
+ # This is called by `read_attribute` if an attribute is unset; you should
42
+ # not call this directly. You might use this to provide defaults, or lazy
43
+ # access, or layered resolution.
44
+ #
45
+ # Once a non-nil default value has been read, it is **fixed on the field**; this method
46
+ # will not be called again, and `attribute_set?(...)` will return `true`.
47
+ #
48
+ # @example values are fixed on read
49
+ # class Defaultable
50
+ # include Gorillib::Model
51
+ # field :timestamp, Integer, default: ->{ Time.now }
52
+ # end
53
+ # dd = Defaultable.new
54
+ # dd.attribute_set?(:timestamp) # => false
55
+ # dd.timestamp # => '2012-01-02 12:34:56 CST'
56
+ # dd.attribute_set?(:timestamp) # => true
57
+ # # The block is *not* re-run -- the time is the same
58
+ # dd.timestamp # => '2012-01-02 12:34:56 CST'
59
+ #
60
+ # @example If the default is a literal nil it is set as normal:
61
+ #
62
+ # Defaultable.field :might_be_nil, String, default: nil
63
+ # dd.attribute_set?(:might_be_nil) # => false
64
+ # dd.might_be_nil # => nil
65
+ # dd.attribute_set?(:might_be_nil) # => true
66
+ #
67
+ # If the default is generated from a block (or anything but a literal nil), no default is set:
68
+ #
69
+ # Defaultable.field :might_be_nil, String, default: ->{ puts 'ran!'; some_other_value ? some_other_value.reverse : nil }
70
+ # Defaultable.field :some_other_value, String
71
+ # dd = Defaultable.new
72
+ # dd.attribute_set?(:might_be_nil) # => false
73
+ # dd.might_be_nil # => nil
74
+ # 'ran!' # block was run
75
+ # dd.might_be_nil # => nil
76
+ # 'ran!' # block was run again
77
+ # dd.some_other_val = 'hello'
78
+ # dd.might_be_nil # => 'olleh'
79
+ # 'ran!' # block was run again, and set a value this time
80
+ # dd.some_other_val = 'goodbye'
81
+ # dd.might_be_nil # => 'olleh'
82
+ # # block was not run again
83
+ #
84
+ # @param [String, Symbol, #to_s] field_name Name of the attribute to unset.
85
+ # @return [Object] The new value
86
+ def read_unset_attribute(field_name)
87
+ field = self.class.fields[field_name] or return nil
88
+ return unless field.has_default?
89
+ val = attribute_default(field)
90
+ return nil if val.nil? && (not field.default.nil?) # don't write nil unless intent is clearly to have default nil
91
+ write_attribute(field.name, val)
92
+ end
93
+
94
+
95
+ protected
96
+
97
+ # the actual default value to assign to the attribute
98
+ def attribute_default(field)
99
+ return unless field.has_default?
100
+ val = field.default
101
+ case
102
+ when val.is_a?(Proc) && (val.arity == 0)
103
+ self.instance_exec(&val)
104
+ when val.is_a?(UnboundMethod) && (val.arity == 0)
105
+ val.bind(self).call
106
+ when val.respond_to?(:call)
107
+ val.call(self, field.name)
108
+ else
109
+ val.try_dup
110
+ end
111
+ end
112
+
113
+ end
114
+
115
+ end
@@ -0,0 +1,24 @@
1
+ module Gorillib
2
+ module Model
3
+
4
+ # All exceptions defined by Gorillib::Model include this module.
5
+ module Error
6
+ end
7
+
8
+ # Exception raised if attempting to assign unknown fields
9
+ class UnknownFieldError < ::NoMethodError
10
+ include Gorillib::Model::Error
11
+ end
12
+
13
+ class ConflictingPositionError < ::ArgumentError
14
+ include Gorillib::Model::Error
15
+ end
16
+
17
+ # Exception raised if deserialized attributes don't have the right shape:
18
+ # for example, a CSV line with too many/too few fields
19
+ class RawDataMismatchError < ::StandardError
20
+ include Gorillib::Model::Error
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,555 @@
1
+ def Gorillib::Factory(*args)
2
+ ::Gorillib::Factory.find(*args)
3
+ end
4
+
5
+ module Gorillib
6
+
7
+ module Factory
8
+ class FactoryMismatchError < TypeMismatchError ; end
9
+
10
+ def self.find(type)
11
+ case
12
+ when factories.include?(type) then return factories[type]
13
+ when type.respond_to?(:receive) then return type
14
+ when type.is_a?(Proc) || type.is_a?(Method) then return Gorillib::Factory::ApplyProcFactory.new(type)
15
+ when type.is_a?(String) then
16
+ return( factories[type] = ActiveSupport::Inflector.constantize(ActiveSupport::Inflector.camelize(type.gsub(/\./, '/'))) )
17
+ else raise ArgumentError, "Don't know which factory makes a #{type}"
18
+ end
19
+ end
20
+
21
+ def self.factory_for(type, options={})
22
+ return find(type) if options.compact.blank?
23
+ klass = factory_klasses[type] or raise "You can only supply options #{options} to a Factory-mapped class"
24
+ klass.new(options)
25
+ end
26
+
27
+ def self.register_factory(factory, typenames)
28
+ typenames.each{|typename| factories[typename] = factory }
29
+ end
30
+
31
+ def self.register_factory_klass(factory_klass, typenames)
32
+ typenames.each{|typename| factory_klasses[typename] = factory_klass }
33
+ end
34
+
35
+ private
36
+ def self.factories() @factories ||= Hash.new end
37
+ def self.factory_klasses() @factory_klasses ||= Hash.new end
38
+ public
39
+
40
+ #
41
+ # A gorillib Factory should answer to the following:
42
+ #
43
+ # * `typename` -- a handle (symbol, lowercased-underscored) naming this type
44
+ # * `native?` -- native objects do not need type-conversion
45
+ # * `blankish?` -- blankish objects are type-converted to a `nil` value
46
+ # * `product` -- the class of objects produced when non-blank
47
+ # * `receive` -- performs the actual conversion
48
+ #
49
+ class BaseFactory
50
+ # [Class] The type of objects produced by this factory
51
+ class_attribute :product
52
+
53
+ def initialize(options={})
54
+ @product = options.delete(:product){ self.class.product }
55
+ define_blankish_method(options.delete(:blankish)) if options.has_key?(:blankish)
56
+ redefine(:convert, options.delete(:convert)) if options.has_key?(:convert)
57
+ warn "Unknown options #{options.keys}" unless options.empty?
58
+ end
59
+
60
+ def self.typename
61
+ @typename ||= ActiveSupport::Inflector.underscore(product.name).to_sym
62
+ end
63
+ def typename ; self.class.typename ; end
64
+
65
+ # A `native` object does not need any transformation; it is accepted directly.
66
+ # By default, an object is native if it `is_a?(product)`
67
+ #
68
+ # @param obj [Object] the object that will be received
69
+ # @return [true, false] true if the item does not need conversion
70
+ def native?(obj)
71
+ obj.is_a?(@product)
72
+ end
73
+ def self.native?(obj) self.new.native?(obj) ; end
74
+
75
+ # A `blankish` object should be converted to `nil`, not a value
76
+ #
77
+ # @param [Object] obj the object to convert and receive
78
+ # @return [true, false] true if the item is equivalent to a nil value
79
+ def blankish?(obj)
80
+ obj.nil? || (obj == "")
81
+ end
82
+ def self.blankish?(obj)
83
+ obj.nil? || (obj == "")
84
+ end
85
+
86
+ # performs the actual conversion
87
+ def receive(*args)
88
+ NoMethodError.abstract_method(self)
89
+ end
90
+
91
+ protected
92
+
93
+ def define_blankish_method(blankish)
94
+ FactoryMismatchError.check_type!(blankish, [Proc, Method, :include?])
95
+ if blankish.respond_to?(:include?)
96
+ then meth = ->(val){ blankish.include?(val) }
97
+ else meth = blankish ; end
98
+ define_singleton_method(:blankish?, meth)
99
+ end
100
+
101
+ def redefine(meth, *args, &block)
102
+ if args.present?
103
+ val = args.first
104
+ case
105
+ when block_given? then raise ArgumentError, "Pass a block or a value, not both"
106
+ when val.is_a?(Proc) || val.is_a?(Method) then block = val
107
+ else block = ->(*){ val.try_dup }
108
+ end
109
+ end
110
+ self.define_singleton_method(meth, &block)
111
+ self
112
+ end
113
+
114
+ # Raises a FactoryMismatchError.
115
+ def mismatched!(obj, message=nil, *args)
116
+ message ||= "item cannot be converted to #{product}"
117
+ FactoryMismatchError.mismatched!(obj, product, message, *args)
118
+ end
119
+
120
+ def self.register_factory!(*typenames)
121
+ typenames = [typename, product] if typenames.empty?
122
+ Gorillib::Factory.register_factory_klass(self, typenames)
123
+ Gorillib::Factory.register_factory( self.new, typenames)
124
+ end
125
+ end
126
+
127
+ class ConvertingFactory < BaseFactory
128
+ def receive(obj)
129
+ return nil if blankish?(obj)
130
+ return obj if native?(obj)
131
+ convert(obj)
132
+ rescue NoMethodError, TypeError, RangeError, ArgumentError => err
133
+ mismatched!(obj, err.message, err.backtrace)
134
+ end
135
+ protected
136
+ # Convert a receivable object to the factory's product type. This method
137
+ # should convert an object to `native?` form or die trying; any variant
138
+ # types (eg nil for an empty string) are handled elsewhere by `receive`.
139
+ #
140
+ # @param [Object] obj the object to convert.
141
+ def convert(obj)
142
+ obj.dup
143
+ end
144
+ end
145
+
146
+ #
147
+ # A NonConvertingFactory accepts objects that are *already* native, and
148
+ # throws a mismatch error for anything else.
149
+ #
150
+ # @example
151
+ # ff = Gorillib::Factory::NonConvertingFactory.new(:product => String, :blankish => ->(obj){ obj.nil? })
152
+ # ff.receive(nil) #=> nil
153
+ # ff.receive("bob") #=> "bob"
154
+ # ff.receive(:bob) #=> Gorillib::Factory::FactoryMismatchError: must be an instance of String, got 3
155
+ #
156
+ class NonConvertingFactory < BaseFactory
157
+ def blankish?(obj) obj.nil? ; end
158
+ def receive(obj)
159
+ return nil if blankish?(obj)
160
+ return obj if native?(obj)
161
+ mismatched!(obj, "must be an instance of #{product},")
162
+ rescue NoMethodError => err
163
+ mismatched!(obj, err.message, err.backtrace)
164
+ end
165
+ end
166
+
167
+ # __________________________________________________________________________
168
+ #
169
+ # Generic Factories
170
+ # __________________________________________________________________________
171
+
172
+ #
173
+ # Factory that accepts whatever given and uses it directly -- no nothin'
174
+ #
175
+ class ::Whatever < BaseFactory
176
+ def initialize(options={})
177
+ options.slice!(:convert, :blankish)
178
+ super(options)
179
+ end
180
+ def native?(obj) true ; end
181
+ def blankish?(obj) false ; end
182
+ def receive(obj) obj ; end
183
+ def self.receive(obj)
184
+ obj
185
+ end
186
+ Gorillib::Factory.register_factory(self, [self, :identical, :whatever])
187
+ end
188
+ IdenticalFactory = ::Whatever unless defined?(IdenticalFactory)
189
+
190
+
191
+ # Manufactures objects from their raw attributes hash
192
+ #
193
+ # The hash must have a value for `:_type`, used to retrieve the actual factory
194
+ #
195
+ class ::GenericModel < BaseFactory
196
+ def blankish?(obj) obj.nil? ; end
197
+ def native?(obj) false ; end
198
+ def receive(attrs, &block)
199
+ Gorillib::Model::Validate.hashlike!(attrs){ "attributes for typed object" }
200
+ klass = Gorillib::Factory(attrs.fetch(:_type){ attrs.fetch("_type") })
201
+ #
202
+ klass.new(attrs, &block)
203
+ end
204
+ def self.receive(obj) allocate.receive(obj) end
205
+ register_factory!(GenericModel, :generic)
206
+ end
207
+
208
+ # __________________________________________________________________________
209
+ #
210
+ # Concrete Factories
211
+ # __________________________________________________________________________
212
+
213
+ class StringFactory < ConvertingFactory
214
+ self.product = String
215
+ def blankish?(obj) obj.nil? end
216
+ def native?(obj) obj.respond_to?(:to_str) end
217
+ def convert(obj) String(obj) end
218
+ register_factory!
219
+ end
220
+
221
+ class BinaryFactory < StringFactory
222
+ def convert(obj)
223
+ super.force_encoding("BINARY")
224
+ end
225
+ register_factory!(:binary)
226
+ end
227
+
228
+ class PathnameFactory < ConvertingFactory
229
+ self.product = ::Pathname
230
+ def convert(obj) Pathname.new(obj) end
231
+ register_factory!
232
+ end
233
+
234
+ class SymbolFactory < ConvertingFactory
235
+ self.product = Symbol
236
+ def convert(obj) obj.to_sym end
237
+ register_factory!
238
+ end
239
+
240
+ class RegexpFactory < ConvertingFactory
241
+ self.product = Regexp
242
+ def convert(obj) Regexp.new(obj) end
243
+ register_factory!
244
+ end
245
+
246
+ #
247
+ # In the following, we use eg `Float(val)` and not `val.to_f` --
248
+ # they round-trip things
249
+ #
250
+ # Float("0x1.999999999999ap-4") # => 0.1
251
+ # "0x1.999999999999ap-4".to_f # => 0
252
+ #
253
+
254
+ FLT_CRUFT_CHARS = ',fFlL'
255
+ FLT_NOT_INT_RE = /[\.eE]/
256
+
257
+ #
258
+ # Converts arg to a Fixnum or Bignum.
259
+ #
260
+ # * Numeric types are converted directly, with floating point numbers being truncated
261
+ # * Strings are interpreted using `Integer()`, so:
262
+ # ** radix indicators (0, 0b, and 0x) are HONORED -- '011' means 9, not 11; '0x22' means 0, not 34
263
+ # ** They must strictly conform to numeric representation or an error is raised (which differs from the behavior of String#to_i)
264
+ # * Non-string values will be converted using to_int, and to_i.
265
+ #
266
+ # @example
267
+ # IntegerFactory.receive(123.999) #=> 123
268
+ # IntegerFactory.receive(Time.new) #=> 1204973019
269
+ #
270
+ # @example IntegerFactory() handles floating-point numbers correctly (as opposed to `Integer()` and GraciousIntegerFactory)
271
+ # IntegerFactory.receive("98.6") #=> 98
272
+ # IntegerFactory.receive("1234.5e3") #=> 1_234_500
273
+ #
274
+ # @example IntegerFactory has love for your hexadecimal, and disturbingly considers 0-prefixed numbers to be octal.
275
+ # IntegerFactory.receive("0x1a") #=> 26
276
+ # IntegerFactory.receive("011") #=> 9
277
+ #
278
+ # @example IntegerFactory() is not as gullible, or generous as GraciousIntegerFactory
279
+ # IntegerFactory.receive("7eleven") #=> (error)
280
+ # IntegerFactory.receive("nonzero") #=> (error)
281
+ # IntegerFactory.receive("123_456L") #=> (error)
282
+ #
283
+ # @note returns Bignum or Fixnum (instances of either are `is_a?(Integer)`)
284
+ class IntegerFactory < ConvertingFactory
285
+ self.product = Integer
286
+ def convert(obj)
287
+ Integer(obj)
288
+ end
289
+ register_factory!(:int, :integer, Integer)
290
+ end
291
+
292
+ #
293
+ # Converts arg to a Fixnum or Bignum.
294
+ #
295
+ # * Numeric types are converted directly, with floating point numbers being truncated
296
+ # * Strings are interpreted using `#to_i`, so:
297
+ # ** radix indicators (0, 0b, and 0x) are IGNORED -- '011' means 11, not 9; '0x22' means 0, not 34
298
+ # ** Strings will be very generously interpreted
299
+ # * Non-string values will be converted using to_i
300
+ #
301
+ # @example
302
+ # GraciousIntegerFactory.receive(123.999) #=> 123
303
+ # GraciousIntegerFactory.receive(Time.new) #=> 1204973019
304
+ #
305
+ # @example GraciousIntegerFactory quietly mangles your floating-pointish strings
306
+ # GraciousIntegerFactory.receive("123.4e-3") #=> 123
307
+ # GraciousIntegerFactory.receive("1e9") #=> 1
308
+ #
309
+ # @example GraciousIntegerFactory does not care for your hexadecimal
310
+ # GraciousIntegerFactory.receive("0x1a") #=> 0
311
+ # GraciousIntegerFactory.receive("011") #=> 11
312
+ #
313
+ # @example GraciousIntegerFactory is generous (perhaps too generous) where IntegerFactory() is not
314
+ # GraciousIntegerFactory.receive("123_456L") #=> 123_456
315
+ # GraciousIntegerFactory.receive("7eleven") #=> 7
316
+ # GraciousIntegerFactory.receive("nonzero") #=> 0
317
+ #
318
+ # @note returns Bignum or Fixnum (instances of either are `is_a?(Integer)`)
319
+ class GraciousIntegerFactory < IntegerFactory
320
+ # See examples/benchmark before 'improving' this method.
321
+ def convert(obj)
322
+ if ::String === obj then
323
+ obj = obj.to_s.tr(::Gorillib::Factory::FLT_CRUFT_CHARS, '') ;
324
+ obj = ::Kernel::Float(obj) if ::Gorillib::Factory::FLT_NOT_INT_RE === obj ;
325
+ end
326
+ ::Kernel::Integer(obj)
327
+ end
328
+ register_factory!(:gracious_int)
329
+ end
330
+
331
+ # Same behavior (and conversion) as IntegerFactory, but specifies its
332
+ # product as `Bignum`.
333
+ #
334
+ # @note returns Bignum or Fixnum (instances of either are `is_a?(Integer)`)
335
+ class BignumFactory < IntegerFactory
336
+ self.product = Bignum
337
+ register_factory!
338
+ end
339
+
340
+ # Returns arg converted to a float.
341
+ # * Numeric types are converted directly
342
+ # * Strings strictly conform to numeric representation or an error is raised (which differs from the behavior of String#to_f)
343
+ # * Strings in radix format (an exact hexadecimal encoding of a number) are properly interpreted.
344
+ # * Octal is not interpreted! This means an IntegerFactory receiving '011' will get 9, a FloatFactory 11.0
345
+ # * Other types are converted using obj.to_f.
346
+ #
347
+ # @example
348
+ # FloatFactory.receive(1) #=> 1.0
349
+ # FloatFactory.receive("123.456") #=> 123.456
350
+ # FloatFactory.receive("0x1.999999999999ap-4" #=> 0.1
351
+ #
352
+ # @example FloatFactory is strict in some cases where GraciousFloatFactory is not
353
+ # FloatFactory.receive("1_23e9f") #=> (error)
354
+ #
355
+ # @example FloatFactory() is not as gullible as GraciousFloatFactory
356
+ # FloatFactory.receive("7eleven") #=> (error)
357
+ # FloatFactory.receive("nonzero") #=> (error)
358
+ #
359
+ class FloatFactory < ConvertingFactory
360
+ self.product = Float
361
+ def convert(obj) Float(obj) ; end
362
+ register_factory!
363
+ end
364
+
365
+ # Returns arg converted to a float.
366
+ # * Numeric types are converted directly
367
+ # * Strings can have ',' (which are removed) or end in `/LlFf/` (pig format);
368
+ # they should other conform to numeric representation or an error is raised.
369
+ # (this differs from the behavior of String#to_f)
370
+ # * Strings in radix format (an exact hexadecimal encoding of a number) are properly interpreted.
371
+ # * Octal is not interpreted! This means an IntegerFactory receiving '011' will get 9, a FloatFactory 11.0
372
+ # * Other types are converted using obj.to_f.
373
+ #
374
+ # @example
375
+ # GraciousFloatFactory.receive(1) #=> 1.0
376
+ # GraciousFloatFactory.receive("123.456") #=> 123.456
377
+ # GraciousFloatFactory.receive("0x1.999999999999ap-4" #=> 0.1
378
+ # GraciousFloatFactory.receive("1_234.5") #=> 1234.5
379
+ #
380
+ # @example GraciousFloatFactory is generous in some cases where FloatFactory is not
381
+ # GraciousFloatFactory.receive("1234.5f") #=> 1234.5
382
+ # GraciousFloatFactory.receive("1,234.5") #=> 1234.5
383
+ # GraciousFloatFactory.receive("1234L") #=> 1234.0
384
+ #
385
+ # @example GraciousFloatFactory is not as gullible as #to_f
386
+ # GraciousFloatFactory.receive("7eleven") #=> (error)
387
+ # GraciousFloatFactory.receive("nonzero") #=> (error)
388
+ #
389
+ class GraciousFloatFactory < FloatFactory
390
+ self.product = Float
391
+ def convert(obj)
392
+ if String === obj then obj = obj.to_s.tr(FLT_CRUFT_CHARS,'') ; end
393
+ super(obj)
394
+ end
395
+ register_factory!(:gracious_float)
396
+ end
397
+
398
+ class ComplexFactory < ConvertingFactory
399
+ self.product = Complex
400
+ def convert(obj)
401
+ if obj.respond_to?(:to_ary)
402
+ x_y = obj.to_ary
403
+ mismatched!(obj, "expected tuple to be a pair") unless (x_y.length == 2)
404
+ Complex(* x_y)
405
+ else
406
+ Complex(obj)
407
+ end
408
+ end
409
+ register_factory!
410
+ end
411
+ class RationalFactory < ConvertingFactory
412
+ self.product = Rational
413
+ def convert(obj)
414
+ if obj.respond_to?(:to_ary)
415
+ x_y = obj.to_ary
416
+ mismatched!(obj, "expected tuple to be a pair") unless (x_y.length == 2)
417
+ Rational(* x_y)
418
+ else
419
+ Rational(obj)
420
+ end
421
+ end
422
+ register_factory!
423
+ end
424
+
425
+ class TimeFactory < ConvertingFactory
426
+ self.product = Time
427
+ FLAT_TIME_RE = /\A\d{14}Z?\z/ unless defined?(Gorillib::Factory::TimeFactory::FLAT_TIME_RE)
428
+ def native?(obj) super(obj) && obj.utc_offset == 0 ; end
429
+ def convert(obj)
430
+ case obj
431
+ when FLAT_TIME_RE then product.utc(obj[0..3].to_i, obj[4..5].to_i, obj[6..7].to_i, obj[8..9].to_i, obj[10..11].to_i, obj[12..13].to_i)
432
+ when Time then obj.getutc
433
+ when Date then product.utc(obj.year, obj.month, obj.day)
434
+ when String then product.parse(obj).utc
435
+ when Numeric then product.at(obj)
436
+ else mismatched!(obj)
437
+ end
438
+ rescue ArgumentError => err
439
+ raise if err.is_a?(TypeMismatchError)
440
+ warn "Cannot parse time #{obj}: #{err}"
441
+ return nil
442
+ end
443
+ register_factory!
444
+ end
445
+
446
+ # __________________________________________________________________________
447
+
448
+ class ClassFactory < NonConvertingFactory ; self.product = Class ; register_factory! ; end
449
+ class ModuleFactory < NonConvertingFactory ; self.product = Module ; register_factory! ; end
450
+ class TrueFactory < NonConvertingFactory ; self.product = TrueClass ; register_factory!(:true, TrueClass) ; end
451
+ class FalseFactory < NonConvertingFactory ; self.product = FalseClass ; register_factory!(:false, FalseClass) ; end
452
+
453
+ class ExceptionFactory < NonConvertingFactory ; self.product = Exception ; register_factory!(:exception, Exception) ; end
454
+
455
+ class NilFactory < NonConvertingFactory
456
+ self.product = NilClass
457
+ def blankish?(obj) false ; end
458
+ register_factory!(:nil, NilClass)
459
+ end
460
+
461
+ class BooleanFactory < ConvertingFactory
462
+ def self.typename() :boolean ; end
463
+ self.product = [TrueClass, FalseClass]
464
+ def blankish?(obj) obj.nil? ; end
465
+ def native?(obj) obj.equal?(true) || obj.equal?(false) ; end
466
+ def convert(obj) (obj.to_s == "false") ? false : true ; end
467
+ register_factory! :boolean
468
+ end
469
+
470
+ #
471
+ #
472
+ #
473
+
474
+ class EnumerableFactory < ConvertingFactory
475
+ # [#receive] factory for converting items
476
+ attr_reader :items_factory
477
+
478
+ def initialize(options={})
479
+ @items_factory = Gorillib::Factory( options.delete(:items){ :identical } )
480
+ redefine(:empty_product, options.delete(:empty_product)) if options.has_key?(:empty_product)
481
+ super(options)
482
+ end
483
+
484
+ def blankish?(obj) obj.nil? ; end
485
+ def native?(obj) false ; end
486
+
487
+ def empty_product
488
+ @product.new
489
+ end
490
+
491
+ def convert(obj)
492
+ clxn = empty_product
493
+ obj.each do |val|
494
+ clxn << items_factory.receive(val)
495
+ end
496
+ clxn
497
+ end
498
+ end
499
+
500
+ class ArrayFactory < EnumerableFactory
501
+ self.product = Array
502
+ register_factory!
503
+ end
504
+
505
+ class HashFactory < EnumerableFactory
506
+ # [#receive] factory for converting keys
507
+ attr_reader :keys_factory
508
+ self.product = Hash
509
+
510
+ def initialize(options={})
511
+ @keys_factory = Gorillib::Factory( options.delete(:keys){ Gorillib::Factory(:identical) } )
512
+ super(options)
513
+ end
514
+
515
+ def convert(obj)
516
+ hsh = empty_product
517
+ obj.each_pair do |key, val|
518
+ hsh[keys_factory.receive(key)] = items_factory.receive(val)
519
+ end
520
+ hsh
521
+ end
522
+ register_factory!
523
+ end
524
+
525
+ class RangeFactory < NonConvertingFactory
526
+ self.product = Range
527
+ def blankish?(obj) obj.nil? || obj == [] ; end
528
+ register_factory!
529
+ end
530
+
531
+ # __________________________________________________________________________
532
+
533
+ class ApplyProcFactory < ConvertingFactory
534
+ attr_reader :callable
535
+
536
+ def initialize(callable=nil, options={}, &block)
537
+ if block_given?
538
+ raise ArgumentError, "Pass a block or a value, not both" unless callable.nil?
539
+ callable = block
540
+ end
541
+ @callable = callable
542
+ super(options)
543
+ end
544
+ def blankish?(obj) obj.nil? ; end
545
+ def native?(val) false ; end
546
+ def convert(obj)
547
+ callable.call(obj)
548
+ end
549
+ register_factory!(:proc)
550
+ end
551
+
552
+
553
+ end
554
+
555
+ end