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.
- data/.gitignore +4 -0
- data/Gemfile +12 -0
- data/README.md +21 -0
- data/Rakefile +15 -0
- data/gorillib-model.gemspec +27 -0
- data/lib/gorillib/builder.rb +239 -0
- data/lib/gorillib/core_ext/datetime.rb +23 -0
- data/lib/gorillib/core_ext/exception.rb +153 -0
- data/lib/gorillib/core_ext/module.rb +10 -0
- data/lib/gorillib/core_ext/object.rb +14 -0
- data/lib/gorillib/model/base.rb +273 -0
- data/lib/gorillib/model/collection/model_collection.rb +157 -0
- data/lib/gorillib/model/collection.rb +200 -0
- data/lib/gorillib/model/defaults.rb +115 -0
- data/lib/gorillib/model/errors.rb +24 -0
- data/lib/gorillib/model/factories.rb +555 -0
- data/lib/gorillib/model/field.rb +168 -0
- data/lib/gorillib/model/lint.rb +24 -0
- data/lib/gorillib/model/named_schema.rb +53 -0
- data/lib/gorillib/model/positional_fields.rb +35 -0
- data/lib/gorillib/model/schema_magic.rb +163 -0
- data/lib/gorillib/model/serialization/csv.rb +60 -0
- data/lib/gorillib/model/serialization/json.rb +44 -0
- data/lib/gorillib/model/serialization/lines.rb +30 -0
- data/lib/gorillib/model/serialization/to_wire.rb +54 -0
- data/lib/gorillib/model/serialization/tsv.rb +53 -0
- data/lib/gorillib/model/serialization.rb +41 -0
- data/lib/gorillib/model/type/extended.rb +83 -0
- data/lib/gorillib/model/type/ip_address.rb +153 -0
- data/lib/gorillib/model/type/url.rb +11 -0
- data/lib/gorillib/model/validate.rb +22 -0
- data/lib/gorillib/model/version.rb +5 -0
- data/lib/gorillib/model.rb +34 -0
- data/spec/builder_spec.rb +193 -0
- data/spec/core_ext/datetime_spec.rb +41 -0
- data/spec/core_ext/exception.rb +98 -0
- data/spec/core_ext/object.rb +45 -0
- data/spec/model/collection_spec.rb +290 -0
- data/spec/model/defaults_spec.rb +104 -0
- data/spec/model/factories_spec.rb +323 -0
- data/spec/model/lint_spec.rb +28 -0
- data/spec/model/serialization/csv_spec.rb +30 -0
- data/spec/model/serialization/tsv_spec.rb +28 -0
- data/spec/model/serialization_spec.rb +41 -0
- data/spec/model/type/extended_spec.rb +166 -0
- data/spec/model/type/ip_address_spec.rb +141 -0
- data/spec/model_spec.rb +261 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/capture_output.rb +28 -0
- data/spec/support/nuke_constants.rb +9 -0
- data/spec/support/shared_context_for_builders.rb +59 -0
- data/spec/support/shared_context_for_models.rb +55 -0
- data/spec/support/shared_examples_for_factories.rb +71 -0
- data/spec/support/shared_examples_for_model_fields.rb +62 -0
- data/spec/support/shared_examples_for_models.rb +87 -0
- 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
|