gorillib 0.4.1pre → 0.4.2pre

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 (89) hide show
  1. data/.gitignore +13 -10
  2. data/.rspec +1 -1
  3. data/.yardopts +1 -0
  4. data/CHANGELOG.md +47 -0
  5. data/Gemfile +22 -19
  6. data/Guardfile +23 -9
  7. data/README.md +12 -12
  8. data/Rakefile +29 -40
  9. data/VERSION +1 -1
  10. data/examples/benchmark/factories_benchmark.rb +87 -0
  11. data/examples/builder/ironfan.rb +1 -19
  12. data/examples/hash/slicing_methods.rb +101 -0
  13. data/gorillib.gemspec +36 -35
  14. data/lib/gorillib/array/deep_compact.rb +4 -3
  15. data/lib/gorillib/array/simple_statistics.rb +76 -0
  16. data/lib/gorillib/base.rb +0 -1
  17. data/lib/gorillib/builder.rb +15 -30
  18. data/lib/gorillib/collection.rb +159 -57
  19. data/lib/gorillib/collection/model_collection.rb +136 -43
  20. data/lib/gorillib/datetime/parse.rb +4 -2
  21. data/lib/gorillib/{array → deprecated/array}/average.rb +0 -0
  22. data/lib/gorillib/{array → deprecated/array}/random.rb +2 -1
  23. data/lib/gorillib/{array → deprecated/array}/sorted_median.rb +0 -0
  24. data/lib/gorillib/{array → deprecated/array}/sorted_percentile.rb +0 -0
  25. data/lib/gorillib/deprecated/array/sorted_sample.rb +13 -0
  26. data/lib/gorillib/{metaprogramming → deprecated/metaprogramming}/aliasing.rb +0 -0
  27. data/lib/gorillib/enumerable/sum.rb +3 -3
  28. data/lib/gorillib/exception/raisers.rb +92 -22
  29. data/lib/gorillib/factories.rb +550 -0
  30. data/lib/gorillib/hash/mash.rb +15 -58
  31. data/lib/gorillib/hashlike/deep_compact.rb +2 -2
  32. data/lib/gorillib/hashlike/slice.rb +55 -40
  33. data/lib/gorillib/model.rb +5 -3
  34. data/lib/gorillib/model/base.rb +33 -119
  35. data/lib/gorillib/model/defaults.rb +58 -14
  36. data/lib/gorillib/model/errors.rb +10 -0
  37. data/lib/gorillib/model/factories.rb +1 -367
  38. data/lib/gorillib/model/field.rb +40 -18
  39. data/lib/gorillib/model/fixup.rb +16 -0
  40. data/lib/gorillib/model/positional_fields.rb +35 -0
  41. data/lib/gorillib/model/schema_magic.rb +162 -0
  42. data/lib/gorillib/model/serialization.rb +1 -2
  43. data/lib/gorillib/model/serialization/csv.rb +59 -0
  44. data/lib/gorillib/pathname.rb +19 -8
  45. data/lib/gorillib/some.rb +2 -0
  46. data/lib/gorillib/string/constantize.rb +17 -10
  47. data/lib/gorillib/string/inflector.rb +11 -7
  48. data/lib/gorillib/type/boolean.rb +40 -0
  49. data/lib/gorillib/type/extended.rb +76 -40
  50. data/lib/gorillib/type/url.rb +6 -4
  51. data/lib/gorillib/utils/console.rb +1 -18
  52. data/lib/gorillib/utils/edge_cases.rb +18 -0
  53. data/spec/examples/builder/ironfan_spec.rb +5 -10
  54. data/spec/gorillib/array/compact_blank_spec.rb +36 -21
  55. data/spec/gorillib/array/simple_statistics_spec.rb +143 -0
  56. data/spec/gorillib/builder_spec.rb +16 -20
  57. data/spec/gorillib/collection_spec.rb +131 -35
  58. data/spec/gorillib/exception/raisers_spec.rb +39 -0
  59. data/spec/gorillib/hash/deep_compact_spec.rb +3 -3
  60. data/spec/gorillib/model/{record/defaults_spec.rb → defaults_spec.rb} +5 -1
  61. data/spec/gorillib/model/factories_spec.rb +335 -0
  62. data/spec/gorillib/model/{record/overlay_spec.rb → overlay_spec.rb} +0 -0
  63. data/spec/gorillib/model/serialization_spec.rb +2 -2
  64. data/spec/gorillib/model_spec.rb +19 -18
  65. data/spec/gorillib/pathname_spec.rb +7 -7
  66. data/spec/gorillib/string/truncate_spec.rb +3 -13
  67. data/spec/gorillib/type/extended_spec.rb +50 -2
  68. data/spec/gorillib/utils/capture_output_spec.rb +1 -1
  69. data/spec/spec_helper.rb +10 -7
  70. data/spec/support/factory_test_helpers.rb +76 -0
  71. data/spec/support/gorillib_test_helpers.rb +36 -24
  72. data/spec/support/model_test_helpers.rb +39 -2
  73. metadata +86 -51
  74. data/lib/alt/kernel/call_stack.rb +0 -56
  75. data/lib/gorillib/array/sorted_sample.rb +0 -12
  76. data/lib/gorillib/builder/field.rb +0 -5
  77. data/lib/gorillib/collection/has_collection.rb +0 -31
  78. data/lib/gorillib/collection/list_collection.rb +0 -58
  79. data/lib/gorillib/exception/confidence.rb +0 -17
  80. data/lib/gorillib/io/system_helpers.rb +0 -30
  81. data/lib/gorillib/model/record_schema.rb +0 -9
  82. data/lib/gorillib/utils/stub_module.rb +0 -33
  83. data/spec/array/average_spec.rb +0 -24
  84. data/spec/array/sorted_median_spec.rb +0 -18
  85. data/spec/array/sorted_percentile_spec.rb +0 -24
  86. data/spec/array/sorted_sample_spec.rb +0 -28
  87. data/spec/gorillib/metaprogramming/aliasing_spec.rb +0 -180
  88. data/spec/gorillib/model/record/factories_spec.rb +0 -335
  89. data/spec/support/kcode_test_helper.rb +0 -16
@@ -16,8 +16,10 @@ class Time
16
16
  when (dt.to_s =~ /\A\d{14}\z/) then parse(dt.to_s+'Z', true)
17
17
  else parse(dt.to_s, true).utc
18
18
  end
19
- rescue StandardError => e
20
- Log.debug e
19
+ rescue StandardError => err
20
+ Log.debug "Can't parse a #{self} from #{dt.inspect}"
21
+ Log.debug err
22
+ return nil
21
23
  end
22
24
  end unless method_defined?(:parse_safely)
23
25
  end
@@ -3,6 +3,7 @@ class Array
3
3
  # Choose a random element from the array
4
4
  #
5
5
  def random_element
6
- self[rand(length)]
6
+ warn "Deprecated; use built-in #sample instead"
7
+ sample
7
8
  end unless method_defined?(:random_element)
8
9
  end
@@ -0,0 +1,13 @@
1
+ require 'gorillib/array/simple_statistics'
2
+
3
+ class Array
4
+
5
+ #
6
+ # DEPRECATED -- use #uniq_nths(num)
7
+ # (#sample is already a method on Array, so this name is confusing)
8
+ #
9
+ def sorted_sample(num)
10
+ sorted_nths(num)
11
+ end
12
+
13
+ end
@@ -23,9 +23,9 @@ unless Enumerable.method_defined?(:sum)
23
23
  if block_given?
24
24
  map(&block).sum(identity)
25
25
  else
26
- inject { |sum, element| sum + element } || identity
26
+ inject{|sum, element| sum + element } || identity
27
27
  end
28
- end unless method_defined?(:sum)
28
+ end
29
29
  end
30
30
 
31
31
  class Range #:nodoc:
@@ -35,6 +35,6 @@ unless Enumerable.method_defined?(:sum)
35
35
  return super if block_given? || !(first.instance_of?(Integer) && last.instance_of?(Integer))
36
36
  actual_last = exclude_end? ? (last - 1) : last
37
37
  (actual_last - first + 1) * (actual_last + first) / 2
38
- end unless method_defined?(:sum)
38
+ end
39
39
  end
40
40
  end
@@ -1,25 +1,43 @@
1
1
  Exception.class_eval do
2
- # @return [Array] __FILE__, __LINE__, description
3
- def self.caller_parts
4
- caller_line = caller[1]
5
- mg = %r{\A([^:]+):(\d+):in \`([^\']+)\'\z}.match(caller_line) or return [caller_line, 1, 'unknown']
2
+ # @return [Array] file, line, method_name
3
+ def self.caller_parts(depth=1)
4
+ caller_line = caller(depth).first
5
+ mg = %r{\A([^:]+):(\d+):in \`([^\']+)\'\z}.match(caller_line) or return [caller_line, 1, '(unknown)']
6
6
  [mg[1], mg[2].to_i, mg[3]]
7
+ rescue
8
+ warn "problem in #{self}.caller_parts"
9
+ return [__FILE__, __LINE__, '(unknown)']
7
10
  end
8
11
 
9
12
  #
10
- # @note !! Be sure to rescue the call to this method; few things suck worse than debugging your rescue blocks/
13
+ # Add context to the backtrace of exceptions ocurring downstream from caller.
14
+ # This is expecially useful in metaprogramming. Follow the implementation in
15
+ # the example.
16
+ #
17
+ # @note !! Be sure to rescue the call to this method; few things suck worse
18
+ # than debugging your rescue blocks.
19
+ #
20
+ # @example
21
+ # define_method(:cromulate) do |level|
22
+ # begin
23
+ # adjust_cromulance(cromulator, level)
24
+ # rescue StandardError => err ; err.polish("setting cromulance #{level} for #{cromulator}") rescue nil ; raise ; end
25
+ # end
26
+ #
11
27
  def polish(extra_info)
12
28
  filename, _, method_name = self.class.caller_parts
13
29
  method_name.gsub!(/rescue in /, '')
14
- most_recent_line = backtrace.detect{|line| line.include?(filename) && line.include?(method_name) }
15
- most_recent_line.sub!(/'$/, " for #{extra_info}'"[0..300])
30
+ most_recent_line = backtrace.detect{|line|
31
+ line.include?(filename) && line.include?(method_name) && line.end_with?("'") }
32
+ most_recent_line.sub!(/'$/, "' for [#{extra_info.to_s[0..300]}]")
16
33
  end
17
34
 
18
35
  end
19
36
 
20
37
  ArgumentError.class_eval do
21
- # Raise an error just like Ruby's native message if the array of arguments
22
- # doesn't match the expected length or range of lengths.
38
+ # Raise an error if there are a different number of arguments than expected.
39
+ # The message will have the same format used by Ruby internal methods.
40
+ # @see #arity_at_least!
23
41
  #
24
42
  # @example want `getset(:foo)` to be different from `getset(:foo, nil)`
25
43
  # def getset(key, *args)
@@ -35,14 +53,16 @@ ArgumentError.class_eval do
35
53
  # @param [Array] args splat args as handed to the caller
36
54
  # @param [#include?] val expected range/list/set of lengths
37
55
  # @raise ArgumentError when there are
38
- def self.check_arity!(args, val)
56
+ def self.check_arity!(args, val, &block)
39
57
  allowed_arity = val.is_a?(Integer) ? (val..val) : val
40
58
  return true if allowed_arity.include?(args.length)
41
- raise self.new("wrong number of arguments (#{args.length} for #{val})")
59
+ info = " #{block.call}" rescue nil if block_given?
60
+ raise self.new("wrong number of arguments (#{args.length} for #{val})#{info}")
42
61
  end
43
62
 
44
- # Raise an error just like Ruby's native message if there are fewer arguments
45
- # than expected
63
+ # Raise an error if there are fewer arguments than expected. The message will
64
+ # have the same format used by Ruby internal methods.
65
+ # @see #check_arity!
46
66
  #
47
67
  # @example want to use splat args, requiring at least one
48
68
  # def assemble_path(*pathsegs)
@@ -57,22 +77,72 @@ ArgumentError.class_eval do
57
77
  end
58
78
  end
59
79
 
60
- NoMethodError.class_eval do
61
- MESSAGE = "undefined method `%s' for %s:%s"
80
+ class TypeMismatchError < ArgumentError ; end
62
81
 
63
- def self.undefined_method(obj)
64
- file, line, meth = caller_parts
65
- self.new(MESSAGE % [meth, obj, obj.class])
82
+ class ArgumentError
83
+ #
84
+ # @param [Array[Symbol,Class,Module]] types
85
+ #
86
+ # @example simple
87
+ # TypeMismatchError.mismatched!(:foo)
88
+ # #=> "TypeMismatchError: :foo has mismatched type
89
+ #
90
+ # @example Can supply the types or duck-types that are expected:
91
+ # TypeMismatchError.mismatched!(:foo, [:to_str, Integer])
92
+ # #=> "TypeMismatchError: :foo has mismatched type; expected #to_str or Integer"
93
+ #
94
+ def self.mismatched!(obj, types=[], msg=nil, *args)
95
+ types = Array(types)
96
+ message = (obj.inspect rescue '(uninspectable object)')
97
+ message << " has mismatched type"
98
+ message << ': ' << msg if msg
99
+ unless types.empty?
100
+ message << '; expected ' << types.map{|type| type.is_a?(Symbol) ? "##{type}" : type.to_s }.join(" or ")
101
+ end
102
+ raise self, message, *args
66
103
  end
67
104
 
68
- def self.unimplemented_method(obj)
105
+ #
106
+ # @param obj [Object] Object to check
107
+ # @param types [Array[Symbol,Class,Module]] Types or methods to compare
108
+ #
109
+ # @example simple
110
+ # TypeMismatchError.mismatched!(:foo)
111
+ # #=> "TypeMismatchError: :foo has mismatched type
112
+ #
113
+ # @example Can supply the types or duck-types that are expected:
114
+ # TypeMismatchError.mismatched!(:foo, [:to_str, Integer])
115
+ # #=> "TypeMismatchError: :foo has mismatched type; expected #to_str or Integer"
116
+ #
117
+ def self.check_type!(obj, types, *args)
118
+ types = Array(types)
119
+ return true if types.any? do |type|
120
+ case type
121
+ when Module then obj.is_a?(type)
122
+ when Symbol then obj.respond_to?(type)
123
+ else raise StandardError, "Can't check type #{type} -- this is an error in the call to the type-checker, not in the object the type-checker is checking"
124
+ end
125
+ end
126
+ self.mismatched!(obj, types, *args)
127
+ end
128
+
129
+ end
130
+
131
+ #
132
+ class AbstractMethodError < NoMethodError ; end
133
+
134
+ NoMethodError.class_eval do
135
+ MESSAGE_FMT = "undefined method `%s' for %s:%s"
136
+
137
+ # Raise an error with the same format used by Ruby internal methods
138
+ def self.undefined_method!(obj)
69
139
  file, line, meth = caller_parts
70
- self.new("#{MESSAGE} -- not implemented yet" % [meth, obj, obj.class])
140
+ raise self.new(MESSAGE_FMT % [meth, obj, obj.class])
71
141
  end
72
142
 
73
- def self.abstract(obj)
143
+ def self.abstract_method!(obj)
74
144
  file, line, meth = caller_parts
75
- self.new("#{MESSAGE} -- must be implemented by the subclass" % [meth, obj, obj.class])
145
+ raise AbstractMethodError.new("#{MESSAGE} -- must be implemented by the subclass" % [meth, obj, obj.class])
76
146
  end
77
147
 
78
148
  end
@@ -0,0 +1,550 @@
1
+ require 'pathname'
2
+ require 'time'
3
+ require 'gorillib/metaprogramming/class_attribute'
4
+ require 'gorillib/string/inflector'
5
+ require 'gorillib/exception/raisers'
6
+ require 'gorillib/hash/compact'
7
+ require 'gorillib/object/try_dup'
8
+
9
+ def Gorillib::Factory(*args)
10
+ ::Gorillib::Factory.find(*args)
11
+ end
12
+
13
+ module Gorillib
14
+
15
+ module Factory
16
+ class FactoryMismatchError < TypeMismatchError ; end
17
+
18
+ def self.find(type)
19
+ case
20
+ when factories.include?(type) then return factories[type]
21
+ when type.respond_to?(:receive) then return type
22
+ when type.is_a?(Proc) || type.is_a?(Method) then return Gorillib::Factory::ApplyProcFactory.new(type)
23
+ when type.is_a?(String) then
24
+ return( factories[type] = Gorillib::Inflector.constantize(Gorillib::Inflector.camelize(type.gsub(/\./, '/'))) )
25
+ else raise ArgumentError, "Don't know which factory makes a #{type}"
26
+ end
27
+ end
28
+
29
+ def self.factory_for(type, options={})
30
+ return find(type) if options.compact.blank?
31
+ klass = factory_klasses[type] or raise "You can only supply options #{options} to a Factory-mapped class"
32
+ klass.new(options)
33
+ end
34
+
35
+ # Manufactures objects from their raw attributes hash
36
+ #
37
+ # A hash with a value for `:_type` is dispatched to the corresponding factory
38
+ # Everything else is returned directly
39
+ def self.make(obj)
40
+ if obj.respond_to?(:has_key?) && (obj.has_key?(:_type) || obj.has_key?('_type'))
41
+ factory = Gorillib::Factory(attrs[:_type])
42
+ factory.receive(obj)
43
+ else
44
+ obj
45
+ end
46
+ end
47
+
48
+ def self.register_factory(factory, typenames)
49
+ typenames.each{|typename| factories[typename] = factory }
50
+ end
51
+
52
+ def self.register_factory_klass(factory_klass, typenames)
53
+ typenames.each{|typename| factory_klasses[typename] = factory_klass }
54
+ end
55
+
56
+ private
57
+ def self.factories() @factories ||= Hash.new end
58
+ def self.factory_klasses() @factory_klasses ||= Hash.new end
59
+ public
60
+
61
+ #
62
+ # A gorillib Factory should answer to the following:
63
+ #
64
+ # * `typename` -- a handle (symbol, lowercased-underscored) naming this type
65
+ # * `native?` -- native objects do not need type-conversion
66
+ # * `blankish?` -- blankish objects are type-converted to a `nil` value
67
+ # * `product` -- the class of objects produced when non-blank
68
+ # * `receive` -- performs the actual conversion
69
+ #
70
+ class BaseFactory
71
+ # [Class] The type of objects produced by this factory
72
+ class_attribute :product
73
+
74
+ def initialize(options={})
75
+ @product = options.delete(:product){ self.class.product }
76
+ define_blankish_method(options.delete(:blankish)) if options.has_key?(:blankish)
77
+ redefine(:convert, options.delete(:convert)) if options.has_key?(:convert)
78
+ warn "Unknown options #{options.keys}" unless options.empty?
79
+ end
80
+
81
+ def self.typename
82
+ @typename ||= Gorillib::Inflector.underscore(product.name).to_sym
83
+ end
84
+ def typename ; self.class.typename ; end
85
+
86
+ # A `native` object does not need any transformation; it is accepted directly.
87
+ # By default, an object is native if it `is_a?(product)`
88
+ #
89
+ # @param obj [Object] the object that will be received
90
+ # @return [true, false] true if the item does not need conversion
91
+ def native?(obj)
92
+ obj.is_a?(@product)
93
+ end
94
+ def self.native?(obj) self.new.native?(obj) ; end
95
+
96
+ # A `blankish` object should be converted to `nil`, not a value
97
+ #
98
+ # @param [Object] obj the object to convert and receive
99
+ # @return [true, false] true if the item is equivalent to a nil value
100
+ def blankish?(obj)
101
+ obj.nil? || (obj == "")
102
+ end
103
+ def self.blankish?(obj)
104
+ obj.nil? || (obj == "")
105
+ end
106
+
107
+ # performs the actual conversion
108
+ def receive(*args)
109
+ NoMethodError.abstract_method(self)
110
+ end
111
+
112
+ protected
113
+
114
+ def define_blankish_method(blankish)
115
+ FactoryMismatchError.check_type!(blankish, [Proc, Method, :include?])
116
+ if blankish.respond_to?(:include?)
117
+ then meth = ->(val){ blankish.include?(val) }
118
+ else meth = blankish ; end
119
+ define_singleton_method(:blankish?, meth)
120
+ end
121
+
122
+ def redefine(meth, *args, &block)
123
+ if args.present?
124
+ val = args.first
125
+ case
126
+ when block_given? then raise ArgumentError, "Pass a block or a value, not both"
127
+ when val.is_a?(Proc) || val.is_a?(Method) then block = val
128
+ else block = ->(*){ val.try_dup }
129
+ end
130
+ end
131
+ self.define_singleton_method(meth, &block)
132
+ self
133
+ end
134
+
135
+ # Raises a FactoryMismatchError.
136
+ def mismatched!(obj, message=nil, *args)
137
+ message ||= "item cannot be converted to #{product}"
138
+ FactoryMismatchError.mismatched!(obj, product, message, *args)
139
+ end
140
+
141
+ def self.register_factory!(*typenames)
142
+ typenames = [typename, product] if typenames.empty?
143
+ Gorillib::Factory.register_factory_klass(self, typenames)
144
+ Gorillib::Factory.register_factory( self.new, typenames)
145
+ end
146
+ end
147
+
148
+ class ConvertingFactory < BaseFactory
149
+ def receive(obj)
150
+ return nil if blankish?(obj)
151
+ return obj if native?(obj)
152
+ convert(obj)
153
+ rescue NoMethodError, TypeError, RangeError, ArgumentError => err
154
+ mismatched!(obj, err.message, err.backtrace)
155
+ end
156
+ protected
157
+ # Convert a receivable object to the factory's product type. This method
158
+ # should convert an object to `native?` form or die trying; any variant
159
+ # types (eg nil for an empty string) are handled elsewhere by `receive`.
160
+ #
161
+ # @param [Object] obj the object to convert.
162
+ def convert(obj)
163
+ obj.dup
164
+ end
165
+ end
166
+
167
+ #
168
+ # A NonConvertingFactory accepts objects that are *already* native, and
169
+ # throws a mismatch error for anything else.
170
+ #
171
+ # @example
172
+ # ff = Gorillib::Factory::NonConvertingFactory.new(:product => String, :blankish => ->(obj){ obj.nil? })
173
+ # ff.receive(nil) #=> nil
174
+ # ff.receive("bob") #=> "bob"
175
+ # ff.receive(:bob) #=> Gorillib::Factory::FactoryMismatchError: must be an instance of String, got 3
176
+ #
177
+ class NonConvertingFactory < BaseFactory
178
+ def blankish?(obj) obj.nil? ; end
179
+ def receive(obj)
180
+ return nil if blankish?(obj)
181
+ return obj if native?(obj)
182
+ mismatched!(obj, "must be an instance of #{product},")
183
+ rescue NoMethodError => err
184
+ mismatched!(obj, err.message, err.backtrace)
185
+ end
186
+ end
187
+
188
+ class ::Whatever < BaseFactory
189
+ def initialize(options={})
190
+ options.slice!(:convert, :blankish)
191
+ super(options)
192
+ end
193
+ def native?(obj) true ; end
194
+ def blankish?(obj) false ; end
195
+ def receive(obj) obj ; end
196
+ def self.receive(obj)
197
+ obj
198
+ end
199
+ Gorillib::Factory.register_factory(self, [self, :identical, :whatever])
200
+ end
201
+ IdenticalFactory = ::Whatever unless defined?(IdenticalFactory)
202
+
203
+ # __________________________________________________________________________
204
+ #
205
+ # Concrete Factories
206
+ # __________________________________________________________________________
207
+
208
+ class StringFactory < ConvertingFactory
209
+ self.product = String
210
+ def blankish?(obj) obj.nil? end
211
+ def native?(obj) obj.respond_to?(:to_str) end
212
+ def convert(obj) String(obj) end
213
+ register_factory!
214
+ end
215
+
216
+ class BinaryFactory < StringFactory
217
+ def convert(obj)
218
+ super.force_encoding("BINARY")
219
+ end
220
+ register_factory!(:binary)
221
+ end
222
+
223
+ class PathnameFactory < ConvertingFactory
224
+ self.product = ::Pathname
225
+ def convert(obj) Pathname.new(obj) end
226
+ register_factory!
227
+ end
228
+
229
+ class SymbolFactory < ConvertingFactory
230
+ self.product = Symbol
231
+ def convert(obj) obj.to_sym end
232
+ register_factory!
233
+ end
234
+
235
+ class RegexpFactory < ConvertingFactory
236
+ self.product = Regexp
237
+ def convert(obj) Regexp.new(obj) end
238
+ register_factory!
239
+ end
240
+
241
+ #
242
+ # In the following, we use eg `Float(val)` and not `val.to_f` --
243
+ # they round-trip things
244
+ #
245
+ # Float("0x1.999999999999ap-4") # => 0.1
246
+ # "0x1.999999999999ap-4".to_f # => 0
247
+ #
248
+
249
+ FLT_CRUFT_CHARS = ',fFlL'
250
+ FLT_NOT_INT_RE = /[\.eE]/
251
+
252
+ #
253
+ # Converts arg to a Fixnum or Bignum.
254
+ #
255
+ # * Numeric types are converted directly, with floating point numbers being truncated
256
+ # * Strings are interpreted using `Integer()`, so:
257
+ # ** radix indicators (0, 0b, and 0x) are HONORED -- '011' means 9, not 11; '0x22' means 0, not 34
258
+ # ** They must strictly conform to numeric representation or an error is raised (which differs from the behavior of String#to_i)
259
+ # * Non-string values will be converted using to_int, and to_i.
260
+ #
261
+ # @example
262
+ # IntegerFactory.receive(123.999) #=> 123
263
+ # IntegerFactory.receive(Time.new) #=> 1204973019
264
+ #
265
+ # @example IntegerFactory() handles floating-point numbers correctly (as opposed to `Integer()` and GraciousIntegerFactory)
266
+ # IntegerFactory.receive("98.6") #=> 98
267
+ # IntegerFactory.receive("1234.5e3") #=> 1_234_500
268
+ #
269
+ # @example IntegerFactory has love for your hexadecimal, and disturbingly considers 0-prefixed numbers to be octal.
270
+ # IntegerFactory.receive("0x1a") #=> 26
271
+ # IntegerFactory.receive("011") #=> 9
272
+ #
273
+ # @example IntegerFactory() is not as gullible, or generous as GraciousIntegerFactory
274
+ # IntegerFactory.receive("7eleven") #=> (error)
275
+ # IntegerFactory.receive("nonzero") #=> (error)
276
+ # IntegerFactory.receive("123_456L") #=> (error)
277
+ #
278
+ # @note returns Bignum or Fixnum (instances of either are `is_a?(Integer)`)
279
+ class IntegerFactory < ConvertingFactory
280
+ self.product = Integer
281
+ def convert(obj)
282
+ Integer(obj)
283
+ end
284
+ register_factory!(:int, :integer, Integer)
285
+ end
286
+
287
+ #
288
+ # Converts arg to a Fixnum or Bignum.
289
+ #
290
+ # * Numeric types are converted directly, with floating point numbers being truncated
291
+ # * Strings are interpreted using `#to_i`, so:
292
+ # ** radix indicators (0, 0b, and 0x) are IGNORED -- '011' means 11, not 9; '0x22' means 0, not 34
293
+ # ** Strings will be very generously interpreted
294
+ # * Non-string values will be converted using to_i
295
+ #
296
+ # @example
297
+ # GraciousIntegerFactory.receive(123.999) #=> 123
298
+ # GraciousIntegerFactory.receive(Time.new) #=> 1204973019
299
+ #
300
+ # @example GraciousIntegerFactory quietly mangles your floating-pointish strings
301
+ # GraciousIntegerFactory.receive("123.4e-3") #=> 123
302
+ # GraciousIntegerFactory.receive("1e9") #=> 1
303
+ #
304
+ # @example GraciousIntegerFactory does not care for your hexadecimal
305
+ # GraciousIntegerFactory.receive("0x1a") #=> 0
306
+ # GraciousIntegerFactory.receive("011") #=> 11
307
+ #
308
+ # @example GraciousIntegerFactory is generous (perhaps too generous) where IntegerFactory() is not
309
+ # GraciousIntegerFactory.receive("123_456L") #=> 123_456
310
+ # GraciousIntegerFactory.receive("7eleven") #=> 7
311
+ # GraciousIntegerFactory.receive("nonzero") #=> 0
312
+ #
313
+ # @note returns Bignum or Fixnum (instances of either are `is_a?(Integer)`)
314
+ class GraciousIntegerFactory < IntegerFactory
315
+ # See examples/benchmark before 'improving' this method.
316
+ def convert(obj)
317
+ if ::String === obj then
318
+ obj = obj.to_s.tr(::Gorillib::Factory::FLT_CRUFT_CHARS, '') ;
319
+ obj = ::Kernel::Float(obj) if ::Gorillib::Factory::FLT_NOT_INT_RE === obj ;
320
+ end
321
+ ::Kernel::Integer(obj)
322
+ end
323
+ register_factory!(:gracious_int)
324
+ end
325
+
326
+ # Same behavior (and conversion) as IntegerFactory, but specifies its
327
+ # product as `Bignum`.
328
+ #
329
+ # @note returns Bignum or Fixnum (instances of either are `is_a?(Integer)`)
330
+ class BignumFactory < IntegerFactory
331
+ self.product = Bignum
332
+ register_factory!
333
+ end
334
+
335
+ # Returns arg converted to a float.
336
+ # * Numeric types are converted directly
337
+ # * Strings strictly conform to numeric representation or an error is raised (which differs from the behavior of String#to_f)
338
+ # * Strings in radix format (an exact hexadecimal encoding of a number) are properly interpreted.
339
+ # * Octal is not interpreted! This means an IntegerFactory receiving '011' will get 9, a FloatFactory 11.0
340
+ # * Other types are converted using obj.to_f.
341
+ #
342
+ # @example
343
+ # FloatFactory.receive(1) #=> 1.0
344
+ # FloatFactory.receive("123.456") #=> 123.456
345
+ # FloatFactory.receive("0x1.999999999999ap-4" #=> 0.1
346
+ #
347
+ # @example FloatFactory is strict in some cases where GraciousFloatFactory is not
348
+ # FloatFactory.receive("1_23e9f") #=> (error)
349
+ #
350
+ # @example FloatFactory() is not as gullible as GraciousFloatFactory
351
+ # FloatFactory.receive("7eleven") #=> (error)
352
+ # FloatFactory.receive("nonzero") #=> (error)
353
+ #
354
+ class FloatFactory < ConvertingFactory
355
+ self.product = Float
356
+ def convert(obj) Float(obj) ; end
357
+ register_factory!
358
+ end
359
+
360
+ # Returns arg converted to a float.
361
+ # * Numeric types are converted directly
362
+ # * Strings can have ',' (which are removed) or end in `/LlFf/` (pig format);
363
+ # they should other conform to numeric representation or an error is raised.
364
+ # (this differs from the behavior of String#to_f)
365
+ # * Strings in radix format (an exact hexadecimal encoding of a number) are properly interpreted.
366
+ # * Octal is not interpreted! This means an IntegerFactory receiving '011' will get 9, a FloatFactory 11.0
367
+ # * Other types are converted using obj.to_f.
368
+ #
369
+ # @example
370
+ # GraciousFloatFactory.receive(1) #=> 1.0
371
+ # GraciousFloatFactory.receive("123.456") #=> 123.456
372
+ # GraciousFloatFactory.receive("0x1.999999999999ap-4" #=> 0.1
373
+ # GraciousFloatFactory.receive("1_234.5") #=> 1234.5
374
+ #
375
+ # @example GraciousFloatFactory is generous in some cases where FloatFactory is not
376
+ # GraciousFloatFactory.receive("1234.5f") #=> 1234.5
377
+ # GraciousFloatFactory.receive("1,234.5") #=> 1234.5
378
+ # GraciousFloatFactory.receive("1234L") #=> 1234.0
379
+ #
380
+ # @example GraciousFloatFactory is not as gullible as #to_f
381
+ # GraciousFloatFactory.receive("7eleven") #=> (error)
382
+ # GraciousFloatFactory.receive("nonzero") #=> (error)
383
+ #
384
+ class GraciousFloatFactory < FloatFactory
385
+ self.product = Float
386
+ def convert(obj)
387
+ if String === obj then obj = obj.to_s.tr(FLT_CRUFT_CHARS,'') ; end
388
+ super(obj)
389
+ end
390
+ register_factory!(:gracious_float)
391
+ end
392
+
393
+ class ComplexFactory < ConvertingFactory
394
+ self.product = Complex
395
+ def convert(obj)
396
+ if obj.respond_to?(:to_ary)
397
+ x_y = obj.to_ary
398
+ mismatched!(obj, "expected tuple to be a pair") unless (x_y.length == 2)
399
+ Complex(* x_y)
400
+ else
401
+ Complex(obj)
402
+ end
403
+ end
404
+ register_factory!
405
+ end
406
+ class RationalFactory < ConvertingFactory
407
+ self.product = Rational
408
+ def convert(obj)
409
+ if obj.respond_to?(:to_ary)
410
+ x_y = obj.to_ary
411
+ mismatched!(obj, "expected tuple to be a pair") unless (x_y.length == 2)
412
+ Rational(* x_y)
413
+ else
414
+ Rational(obj)
415
+ end
416
+ end
417
+ register_factory!
418
+ end
419
+
420
+ class TimeFactory < ConvertingFactory
421
+ self.product = Time
422
+ FLAT_TIME_RE = /\A\d{14}Z?\z/ unless defined?(Gorillib::Factory::TimeFactory::FLAT_TIME_RE)
423
+ def native?(obj) super(obj) && obj.utc_offset == 0 ; end
424
+ def convert(obj)
425
+ case obj
426
+ 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)
427
+ when Time then obj.getutc
428
+ when Date then product.utc(obj.year, obj.month, obj.day)
429
+ when String then product.parse(obj).utc
430
+ when Numeric then product.at(obj)
431
+ else mismatched!(obj)
432
+ end
433
+ rescue ArgumentError => err
434
+ raise if err.is_a?(TypeMismatchError)
435
+ warn "Cannot parse time #{obj}: #{err}"
436
+ return nil
437
+ end
438
+ register_factory!
439
+ end
440
+
441
+ # __________________________________________________________________________
442
+
443
+ class ClassFactory < NonConvertingFactory ; self.product = Class ; register_factory! ; end
444
+ class ModuleFactory < NonConvertingFactory ; self.product = Module ; register_factory! ; end
445
+ class TrueFactory < NonConvertingFactory ; self.product = TrueClass ; register_factory!(:true, TrueClass) ; end
446
+ class FalseFactory < NonConvertingFactory ; self.product = FalseClass ; register_factory!(:false, FalseClass) ; end
447
+
448
+ class ExceptionFactory < NonConvertingFactory ; self.product = Exception ; register_factory!(:exception, Exception) ; end
449
+
450
+ class NilFactory < NonConvertingFactory
451
+ self.product = NilClass
452
+ def blankish?(obj) false ; end
453
+ register_factory!(:nil, NilClass)
454
+ end
455
+
456
+ class BooleanFactory < ConvertingFactory
457
+ def self.typename() :boolean ; end
458
+ self.product = [TrueClass, FalseClass]
459
+ def blankish?(obj) obj.nil? ; end
460
+ def native?(obj) obj.equal?(true) || obj.equal?(false) ; end
461
+ def convert(obj) (obj.to_s == "false") ? false : true ; end
462
+ register_factory! :boolean
463
+ end
464
+
465
+ #
466
+ #
467
+ #
468
+
469
+ class EnumerableFactory < ConvertingFactory
470
+ # [#receive] factory for converting items
471
+ attr_reader :items_factory
472
+
473
+ def initialize(options={})
474
+ @items_factory = Gorillib::Factory( options.delete(:items){ :identical } )
475
+ redefine(:empty_product, options.delete(:empty_product)) if options.has_key?(:empty_product)
476
+ super(options)
477
+ end
478
+
479
+ def blankish?(obj) obj.nil? ; end
480
+ def native?(obj) false ; end
481
+
482
+ def empty_product
483
+ @product.new
484
+ end
485
+
486
+ def convert(obj)
487
+ clxn = empty_product
488
+ obj.each do |val|
489
+ clxn << items_factory.receive(val)
490
+ end
491
+ clxn
492
+ end
493
+ end
494
+
495
+ class ArrayFactory < EnumerableFactory
496
+ self.product = Array
497
+ register_factory!
498
+ end
499
+
500
+ class HashFactory < EnumerableFactory
501
+ # [#receive] factory for converting keys
502
+ attr_reader :keys_factory
503
+ self.product = Hash
504
+
505
+ def initialize(options={})
506
+ @keys_factory = Gorillib::Factory( options.delete(:keys){ Gorillib::Factory(:identical) } )
507
+ super(options)
508
+ end
509
+
510
+ def convert(obj)
511
+ hsh = empty_product
512
+ obj.each_pair do |key, val|
513
+ hsh[keys_factory.receive(key)] = items_factory.receive(val)
514
+ end
515
+ hsh
516
+ end
517
+ register_factory!
518
+ end
519
+
520
+ class RangeFactory < NonConvertingFactory
521
+ self.product = Range
522
+ def blankish?(obj) obj.nil? || obj == [] ; end
523
+ register_factory!
524
+ end
525
+
526
+ # __________________________________________________________________________
527
+
528
+ class ApplyProcFactory < ConvertingFactory
529
+ attr_reader :callable
530
+
531
+ def initialize(callable=nil, options={}, &block)
532
+ if block_given?
533
+ raise ArgumentError, "Pass a block or a value, not both" unless callable.nil?
534
+ callable = block
535
+ end
536
+ @callable = callable
537
+ super(options)
538
+ end
539
+ def blankish?(obj) obj.nil? ; end
540
+ def native?(val) false ; end
541
+ def convert(obj)
542
+ callable.call(obj)
543
+ end
544
+ register_factory!(:proc)
545
+ end
546
+
547
+
548
+ end
549
+
550
+ end