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