motion_coercible 0.2.0
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.
- checksums.yaml +15 -0
- data/README.md +62 -0
- data/lib/motion_coercible.rb +12 -0
- data/lib/project/coercer/array.rb +24 -0
- data/lib/project/coercer/configurable.rb +63 -0
- data/lib/project/coercer/false_class.rb +25 -0
- data/lib/project/coercer/float.rb +25 -0
- data/lib/project/coercer/hash.rb +46 -0
- data/lib/project/coercer/integer.rb +92 -0
- data/lib/project/coercer/numeric.rb +53 -0
- data/lib/project/coercer/object.rb +187 -0
- data/lib/project/coercer/string.rb +228 -0
- data/lib/project/coercer/symbol.rb +25 -0
- data/lib/project/coercer/time.rb +41 -0
- data/lib/project/coercer/time_coercions.rb +58 -0
- data/lib/project/coercer/true_class.rb +25 -0
- data/lib/project/coercer.rb +136 -0
- data/lib/project/coercible.rb +18 -0
- data/lib/project/configuration.rb +28 -0
- data/lib/project/support/options.rb +125 -0
- data/lib/project/support/type_lookup.rb +113 -0
- data/lib/project/version.rb +3 -0
- metadata +108 -0
@@ -0,0 +1,228 @@
|
|
1
|
+
module Coercible
|
2
|
+
class Coercer
|
3
|
+
|
4
|
+
# Coerce String values
|
5
|
+
class String < Object
|
6
|
+
extend Configurable
|
7
|
+
|
8
|
+
primitive ::String
|
9
|
+
|
10
|
+
config_keys [ :boolean_map ]
|
11
|
+
|
12
|
+
TRUE_VALUES = %w[ 1 on t true y yes ].freeze
|
13
|
+
FALSE_VALUES = %w[ 0 off f false n no ].freeze
|
14
|
+
BOOLEAN_MAP = ::Hash[ TRUE_VALUES.product([ true ]) + FALSE_VALUES.product([ false ]) ].freeze
|
15
|
+
|
16
|
+
INTEGER_REGEXP = /[-+]?(?:[0-9]\d*)/.freeze
|
17
|
+
EXPONENT_REGEXP = /(?:[eE][-+]?\d+)/.freeze
|
18
|
+
FRACTIONAL_REGEXP = /(?:\.\d+)/.freeze
|
19
|
+
|
20
|
+
NUMERIC_REGEXP = /\A(
|
21
|
+
#{INTEGER_REGEXP}#{FRACTIONAL_REGEXP}?#{EXPONENT_REGEXP}? |
|
22
|
+
#{FRACTIONAL_REGEXP}#{EXPONENT_REGEXP}?
|
23
|
+
)\z/x.freeze
|
24
|
+
|
25
|
+
# Return default configuration for string coercer type
|
26
|
+
#
|
27
|
+
# @return [Configuration]
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
def self.config
|
31
|
+
super { |config| config.boolean_map = BOOLEAN_MAP }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return boolean map from the config
|
35
|
+
#
|
36
|
+
# @return [::Hash]
|
37
|
+
#
|
38
|
+
# @api private
|
39
|
+
attr_reader :boolean_map
|
40
|
+
|
41
|
+
# Initialize a new string coercer instance
|
42
|
+
#
|
43
|
+
# @param [Coercer]
|
44
|
+
#
|
45
|
+
# @param [Configuration]
|
46
|
+
#
|
47
|
+
# @return [undefined]
|
48
|
+
#
|
49
|
+
# @api private
|
50
|
+
def initialize(coercer = Coercer.new, config = self.class.config)
|
51
|
+
super(coercer)
|
52
|
+
@boolean_map = config.boolean_map
|
53
|
+
end
|
54
|
+
|
55
|
+
# Coerce give value to a constant
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# coercer[String].to_constant('String') # => String
|
59
|
+
#
|
60
|
+
# @param [String] value
|
61
|
+
#
|
62
|
+
# @return [Object]
|
63
|
+
#
|
64
|
+
# @api public
|
65
|
+
def to_constant(value)
|
66
|
+
names = value.split('::')
|
67
|
+
names.shift if names.first.empty?
|
68
|
+
names.inject(::Object) { |*args| constant_lookup(*args) }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Coerce give value to a symbol
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# coercer[String].to_symbol('string') # => :string
|
75
|
+
#
|
76
|
+
# @param [String] value
|
77
|
+
#
|
78
|
+
# @return [Symbol]
|
79
|
+
#
|
80
|
+
# @api public
|
81
|
+
def to_symbol(value)
|
82
|
+
value.to_sym
|
83
|
+
end
|
84
|
+
|
85
|
+
# Coerce given value to Time
|
86
|
+
#
|
87
|
+
# @example
|
88
|
+
# coercer[String].to_time(string) # => Time object
|
89
|
+
#
|
90
|
+
# @param [String] value
|
91
|
+
#
|
92
|
+
# @return [Time]
|
93
|
+
#
|
94
|
+
# @api public
|
95
|
+
def to_time(value)
|
96
|
+
parse_value(::Time, value, __method__)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Coerce value to TrueClass or FalseClass
|
100
|
+
#
|
101
|
+
# @example with "T"
|
102
|
+
# coercer[String].to_boolean('T') # => true
|
103
|
+
#
|
104
|
+
# @example with "F"
|
105
|
+
# coercer[String].to_boolean('F') # => false
|
106
|
+
#
|
107
|
+
# @param [#to_s]
|
108
|
+
#
|
109
|
+
# @return [Boolean]
|
110
|
+
#
|
111
|
+
# @api public
|
112
|
+
def to_boolean(value)
|
113
|
+
boolean_map.fetch(value.downcase) {
|
114
|
+
raise_unsupported_coercion(value, __method__)
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
# Coerce value to integer
|
119
|
+
#
|
120
|
+
# @example
|
121
|
+
# coercer[String].to_integer('1') # => 1
|
122
|
+
#
|
123
|
+
# @param [Object] value
|
124
|
+
#
|
125
|
+
# @return [Integer]
|
126
|
+
#
|
127
|
+
# @api public
|
128
|
+
def to_integer(value)
|
129
|
+
if value =~ /\A#{INTEGER_REGEXP}\z/
|
130
|
+
value.to_i
|
131
|
+
else
|
132
|
+
# coerce to a Float first to evaluate scientific notation (if any)
|
133
|
+
# that may change the integer part, then convert to an integer
|
134
|
+
to_float(value).to_i
|
135
|
+
end
|
136
|
+
rescue UnsupportedCoercion
|
137
|
+
raise_unsupported_coercion(value, __method__)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Coerce value to float
|
141
|
+
#
|
142
|
+
# @example
|
143
|
+
# coercer[String].to_float('1.2') # => 1.2
|
144
|
+
#
|
145
|
+
# @param [Object] value
|
146
|
+
#
|
147
|
+
# @return [Float]
|
148
|
+
#
|
149
|
+
# @api public
|
150
|
+
def to_float(value)
|
151
|
+
to_numeric(value, :to_f)
|
152
|
+
rescue UnsupportedCoercion
|
153
|
+
raise_unsupported_coercion(value, __method__)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Coerce value to decimal
|
157
|
+
#
|
158
|
+
# @example
|
159
|
+
# coercer[String].to_decimal('1.2') # => #<BigDecimal:b72157d4,'0.12E1',8(8)>
|
160
|
+
#
|
161
|
+
# @param [Object] value
|
162
|
+
#
|
163
|
+
# @return [BigDecimal]
|
164
|
+
#
|
165
|
+
# @api public
|
166
|
+
def to_decimal(value)
|
167
|
+
to_numeric(value, :to_d)
|
168
|
+
rescue UnsupportedCoercion
|
169
|
+
raise_unsupported_coercion(value, __method__)
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
# Lookup a constant within a module
|
175
|
+
#
|
176
|
+
# @param [Module] mod
|
177
|
+
#
|
178
|
+
# @param [String] name
|
179
|
+
#
|
180
|
+
# @return [Object]
|
181
|
+
#
|
182
|
+
# @api private
|
183
|
+
def constant_lookup(mod, name)
|
184
|
+
if mod.const_defined?(name, *EXTRA_CONST_ARGS)
|
185
|
+
mod.const_get(name, *EXTRA_CONST_ARGS)
|
186
|
+
else
|
187
|
+
mod.const_missing(name)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Match numeric string
|
192
|
+
#
|
193
|
+
# @param [String] value
|
194
|
+
# value to typecast
|
195
|
+
# @param [Symbol] method
|
196
|
+
# method to typecast with
|
197
|
+
#
|
198
|
+
# @return [Numeric]
|
199
|
+
# number if matched, value if no match
|
200
|
+
#
|
201
|
+
# @api private
|
202
|
+
def to_numeric(value, method)
|
203
|
+
if value =~ NUMERIC_REGEXP
|
204
|
+
$1.public_send(method)
|
205
|
+
else
|
206
|
+
raise_unsupported_coercion(value, method)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Parse the value or return it as-is if it is invalid
|
211
|
+
#
|
212
|
+
# @param [#parse] parser
|
213
|
+
#
|
214
|
+
# @param [String] value
|
215
|
+
#
|
216
|
+
# @return [Time]
|
217
|
+
#
|
218
|
+
# @api private
|
219
|
+
def parse_value(parser, value, method)
|
220
|
+
parser.parse(value)
|
221
|
+
rescue ArgumentError
|
222
|
+
raise_unsupported_coercion(value, method)
|
223
|
+
end
|
224
|
+
|
225
|
+
end # class String
|
226
|
+
|
227
|
+
end # class Coercer
|
228
|
+
end # module Coercible
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Coercible
|
2
|
+
class Coercer
|
3
|
+
|
4
|
+
# Coerce Symbol values
|
5
|
+
class Symbol < Object
|
6
|
+
primitive ::Symbol
|
7
|
+
|
8
|
+
# Coerce given value to String
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# coercer[Symbol].to_string(:name) # => "name"
|
12
|
+
#
|
13
|
+
# @param [Symbol] value
|
14
|
+
#
|
15
|
+
# @return [String]
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
def to_string(value)
|
19
|
+
value.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
end # class Symbol
|
23
|
+
|
24
|
+
end # class Coercer
|
25
|
+
end # module Coercible
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Coercible
|
2
|
+
class Coercer
|
3
|
+
|
4
|
+
# Coerce Time values
|
5
|
+
class Time < Object
|
6
|
+
include TimeCoercions
|
7
|
+
|
8
|
+
primitive ::Time
|
9
|
+
|
10
|
+
# Passthrough the value
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# coercer[DateTime].to_time(time) # => Time object
|
14
|
+
#
|
15
|
+
# @param [DateTime] value
|
16
|
+
#
|
17
|
+
# @return [Date]
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
def to_time(value)
|
21
|
+
value
|
22
|
+
end
|
23
|
+
|
24
|
+
# Creates a Fixnum instance from a Time object
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# Coercible::Coercion::Time.to_integer(time) # => Fixnum object
|
28
|
+
#
|
29
|
+
# @param [Time] value
|
30
|
+
#
|
31
|
+
# @return [Fixnum]
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def to_integer(value)
|
35
|
+
value.to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
end # class Time
|
39
|
+
|
40
|
+
end # class Coercer
|
41
|
+
end # module Coercible
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Coercible
|
2
|
+
class Coercer
|
3
|
+
|
4
|
+
# Common time coercion methods
|
5
|
+
module TimeCoercions
|
6
|
+
|
7
|
+
# Coerce given value to String
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# coercer[Time].to_string(time) # => "Wed Jul 20 10:30:41 -0700 2011"
|
11
|
+
#
|
12
|
+
# @param [Date,Time,DateTime] value
|
13
|
+
#
|
14
|
+
# @return [String]
|
15
|
+
#
|
16
|
+
# @api public
|
17
|
+
def to_string(value)
|
18
|
+
value.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
# Coerce given value to Time
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# coercer[DateTime].to_time(datetime) # => Time object
|
25
|
+
#
|
26
|
+
# @param [Date,DateTime] value
|
27
|
+
#
|
28
|
+
# @return [Time]
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def to_time(value)
|
32
|
+
coerce_with_method(value, :to_time)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Try to use native coercion method on the given value
|
38
|
+
#
|
39
|
+
# Falls back to String-based parsing
|
40
|
+
#
|
41
|
+
# @param [Date,DateTime,Time] value
|
42
|
+
# @param [Symbol] method
|
43
|
+
#
|
44
|
+
# @return [Date,DateTime,Time]
|
45
|
+
#
|
46
|
+
# @api private
|
47
|
+
def coerce_with_method(value, method)
|
48
|
+
if value.respond_to?(method)
|
49
|
+
value.public_send(method)
|
50
|
+
else
|
51
|
+
coercers[::String].public_send(method, to_string(value))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end # module TimeCoercions
|
56
|
+
|
57
|
+
end # class Coercer
|
58
|
+
end # module Coercible
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Coercible
|
2
|
+
class Coercer
|
3
|
+
|
4
|
+
# Coerce true values
|
5
|
+
class TrueClass < Object
|
6
|
+
primitive ::TrueClass
|
7
|
+
|
8
|
+
# Coerce given value to String
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# coercer[TrueClass].to_string(true) # => "true"
|
12
|
+
#
|
13
|
+
# @param [TrueClass] value
|
14
|
+
#
|
15
|
+
# @return [String]
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
def to_string(value)
|
19
|
+
value.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
end # class TrueClass
|
23
|
+
|
24
|
+
end # class Coercer
|
25
|
+
end # module Coercible
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Coercible
|
2
|
+
|
3
|
+
# Coercer object
|
4
|
+
#
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
#
|
8
|
+
# coercer = Coercible::Coercer.new
|
9
|
+
#
|
10
|
+
# coercer[String].to_boolean('yes') # => true
|
11
|
+
# coercer[Integer].to_string(1) # => '1'
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
class Coercer
|
15
|
+
|
16
|
+
# Return coercer instances
|
17
|
+
#
|
18
|
+
# @return [Array<Coercer::Object>]
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
attr_reader :coercers
|
22
|
+
|
23
|
+
# Returns global configuration for coercers
|
24
|
+
#
|
25
|
+
# @return [Configuration]
|
26
|
+
#
|
27
|
+
# @api private
|
28
|
+
attr_reader :config
|
29
|
+
|
30
|
+
# Build a new coercer
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
#
|
34
|
+
# Coercible::Coercer.new { |config| # set configuration }
|
35
|
+
#
|
36
|
+
# @yieldparam [Configuration]
|
37
|
+
#
|
38
|
+
# @return [Coercer]
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
def self.new(&block)
|
42
|
+
configuration = Configuration.build(config_keys)
|
43
|
+
|
44
|
+
configurable_coercers.each do |coercer|
|
45
|
+
configuration.send("#{coercer.config_name}=", coercer.config)
|
46
|
+
end
|
47
|
+
|
48
|
+
yield(configuration) if block_given?
|
49
|
+
|
50
|
+
super(configuration)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return configuration keys for Coercer instance
|
54
|
+
#
|
55
|
+
# @return [Array<Symbol>]
|
56
|
+
#
|
57
|
+
# @api private
|
58
|
+
def self.config_keys
|
59
|
+
configurable_coercers.map(&:config_name)
|
60
|
+
end
|
61
|
+
private_class_method :config_keys
|
62
|
+
|
63
|
+
# Return coercer classes that are configurable
|
64
|
+
#
|
65
|
+
# @return [Array<Class>]
|
66
|
+
#
|
67
|
+
# @api private
|
68
|
+
def self.configurable_coercers(&block)
|
69
|
+
Coercer::Object.descendants.select { |descendant|
|
70
|
+
descendant.respond_to?(:config)
|
71
|
+
}
|
72
|
+
end
|
73
|
+
private_class_method :configurable_coercers
|
74
|
+
|
75
|
+
# Initialize a new coercer instance
|
76
|
+
#
|
77
|
+
# @param [Hash] coercers
|
78
|
+
#
|
79
|
+
# @param [Configuration] config
|
80
|
+
#
|
81
|
+
# @return [undefined]
|
82
|
+
#
|
83
|
+
# @api private
|
84
|
+
def initialize(config, coercers = {})
|
85
|
+
@coercers = coercers
|
86
|
+
@config = config
|
87
|
+
end
|
88
|
+
|
89
|
+
# Access a specific coercer object for the given type
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
#
|
93
|
+
# coercer[String] # => string coercer
|
94
|
+
# coercer[Integer] # => integer coercer
|
95
|
+
#
|
96
|
+
# @param [Class] type
|
97
|
+
#
|
98
|
+
# @return [Coercer::Object]
|
99
|
+
#
|
100
|
+
# @api public
|
101
|
+
def [](klass)
|
102
|
+
coercers[klass] || initialize_coercer(klass)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Initialize a new coercer instance for the given type
|
108
|
+
#
|
109
|
+
# If a coercer class supports configuration it will receive it from the
|
110
|
+
# global configuration object
|
111
|
+
#
|
112
|
+
# @return [Coercer::Object]
|
113
|
+
#
|
114
|
+
# @api private
|
115
|
+
def initialize_coercer(klass)
|
116
|
+
coercers[klass] =
|
117
|
+
begin
|
118
|
+
coercer = Coercer::Object.determine_type(klass) || Coercer::Object
|
119
|
+
args = [ self ]
|
120
|
+
args << config_for(coercer) if coercer.respond_to?(:config_name)
|
121
|
+
coercer.new(*args)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Find configuration for the given coercer type
|
126
|
+
#
|
127
|
+
# @return [Configuration]
|
128
|
+
#
|
129
|
+
# @api private
|
130
|
+
def config_for(coercer)
|
131
|
+
config.send(coercer.config_name)
|
132
|
+
end
|
133
|
+
|
134
|
+
end # class Coercer
|
135
|
+
|
136
|
+
end # module Coercible
|