gorillib 0.4.1pre → 0.4.2pre

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