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.
@@ -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