gorillib-model 0.0.1

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 (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