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.
- data/.gitignore +13 -10
- data/.rspec +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +47 -0
- data/Gemfile +22 -19
- data/Guardfile +23 -9
- data/README.md +12 -12
- data/Rakefile +29 -40
- data/VERSION +1 -1
- data/examples/benchmark/factories_benchmark.rb +87 -0
- data/examples/builder/ironfan.rb +1 -19
- data/examples/hash/slicing_methods.rb +101 -0
- data/gorillib.gemspec +36 -35
- data/lib/gorillib/array/deep_compact.rb +4 -3
- data/lib/gorillib/array/simple_statistics.rb +76 -0
- data/lib/gorillib/base.rb +0 -1
- data/lib/gorillib/builder.rb +15 -30
- data/lib/gorillib/collection.rb +159 -57
- data/lib/gorillib/collection/model_collection.rb +136 -43
- data/lib/gorillib/datetime/parse.rb +4 -2
- data/lib/gorillib/{array → deprecated/array}/average.rb +0 -0
- data/lib/gorillib/{array → deprecated/array}/random.rb +2 -1
- data/lib/gorillib/{array → deprecated/array}/sorted_median.rb +0 -0
- data/lib/gorillib/{array → deprecated/array}/sorted_percentile.rb +0 -0
- data/lib/gorillib/deprecated/array/sorted_sample.rb +13 -0
- data/lib/gorillib/{metaprogramming → deprecated/metaprogramming}/aliasing.rb +0 -0
- data/lib/gorillib/enumerable/sum.rb +3 -3
- data/lib/gorillib/exception/raisers.rb +92 -22
- data/lib/gorillib/factories.rb +550 -0
- data/lib/gorillib/hash/mash.rb +15 -58
- data/lib/gorillib/hashlike/deep_compact.rb +2 -2
- data/lib/gorillib/hashlike/slice.rb +55 -40
- data/lib/gorillib/model.rb +5 -3
- data/lib/gorillib/model/base.rb +33 -119
- data/lib/gorillib/model/defaults.rb +58 -14
- data/lib/gorillib/model/errors.rb +10 -0
- data/lib/gorillib/model/factories.rb +1 -367
- data/lib/gorillib/model/field.rb +40 -18
- data/lib/gorillib/model/fixup.rb +16 -0
- data/lib/gorillib/model/positional_fields.rb +35 -0
- data/lib/gorillib/model/schema_magic.rb +162 -0
- data/lib/gorillib/model/serialization.rb +1 -2
- data/lib/gorillib/model/serialization/csv.rb +59 -0
- data/lib/gorillib/pathname.rb +19 -8
- data/lib/gorillib/some.rb +2 -0
- data/lib/gorillib/string/constantize.rb +17 -10
- data/lib/gorillib/string/inflector.rb +11 -7
- data/lib/gorillib/type/boolean.rb +40 -0
- data/lib/gorillib/type/extended.rb +76 -40
- data/lib/gorillib/type/url.rb +6 -4
- data/lib/gorillib/utils/console.rb +1 -18
- data/lib/gorillib/utils/edge_cases.rb +18 -0
- data/spec/examples/builder/ironfan_spec.rb +5 -10
- data/spec/gorillib/array/compact_blank_spec.rb +36 -21
- data/spec/gorillib/array/simple_statistics_spec.rb +143 -0
- data/spec/gorillib/builder_spec.rb +16 -20
- data/spec/gorillib/collection_spec.rb +131 -35
- data/spec/gorillib/exception/raisers_spec.rb +39 -0
- data/spec/gorillib/hash/deep_compact_spec.rb +3 -3
- data/spec/gorillib/model/{record/defaults_spec.rb → defaults_spec.rb} +5 -1
- data/spec/gorillib/model/factories_spec.rb +335 -0
- data/spec/gorillib/model/{record/overlay_spec.rb → overlay_spec.rb} +0 -0
- data/spec/gorillib/model/serialization_spec.rb +2 -2
- data/spec/gorillib/model_spec.rb +19 -18
- data/spec/gorillib/pathname_spec.rb +7 -7
- data/spec/gorillib/string/truncate_spec.rb +3 -13
- data/spec/gorillib/type/extended_spec.rb +50 -2
- data/spec/gorillib/utils/capture_output_spec.rb +1 -1
- data/spec/spec_helper.rb +10 -7
- data/spec/support/factory_test_helpers.rb +76 -0
- data/spec/support/gorillib_test_helpers.rb +36 -24
- data/spec/support/model_test_helpers.rb +39 -2
- metadata +86 -51
- data/lib/alt/kernel/call_stack.rb +0 -56
- data/lib/gorillib/array/sorted_sample.rb +0 -12
- data/lib/gorillib/builder/field.rb +0 -5
- data/lib/gorillib/collection/has_collection.rb +0 -31
- data/lib/gorillib/collection/list_collection.rb +0 -58
- data/lib/gorillib/exception/confidence.rb +0 -17
- data/lib/gorillib/io/system_helpers.rb +0 -30
- data/lib/gorillib/model/record_schema.rb +0 -9
- data/lib/gorillib/utils/stub_module.rb +0 -33
- data/spec/array/average_spec.rb +0 -24
- data/spec/array/sorted_median_spec.rb +0 -18
- data/spec/array/sorted_percentile_spec.rb +0 -24
- data/spec/array/sorted_sample_spec.rb +0 -28
- data/spec/gorillib/metaprogramming/aliasing_spec.rb +0 -180
- data/spec/gorillib/model/record/factories_spec.rb +0 -335
- 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 =>
|
20
|
-
Log.debug
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -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
|
26
|
+
inject{|sum, element| sum + element } || identity
|
27
27
|
end
|
28
|
-
end
|
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
|
38
|
+
end
|
39
39
|
end
|
40
40
|
end
|
@@ -1,25 +1,43 @@
|
|
1
1
|
Exception.class_eval do
|
2
|
-
# @return [Array]
|
3
|
-
def self.caller_parts
|
4
|
-
caller_line = caller
|
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
|
-
#
|
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|
|
15
|
-
|
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
|
22
|
-
#
|
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
|
-
|
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
|
45
|
-
#
|
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
|
-
|
61
|
-
MESSAGE = "undefined method `%s' for %s:%s"
|
80
|
+
class TypeMismatchError < ArgumentError ; end
|
62
81
|
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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(
|
140
|
+
raise self.new(MESSAGE_FMT % [meth, obj, obj.class])
|
71
141
|
end
|
72
142
|
|
73
|
-
def self.
|
143
|
+
def self.abstract_method!(obj)
|
74
144
|
file, line, meth = caller_parts
|
75
|
-
|
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
|