coercive 1.1.0 → 1.6.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 +4 -4
- data/README.md +91 -4
- data/coercive.gemspec +1 -1
- data/lib/coercive.rb +99 -5
- data/test/coercive.rb +192 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af1258f102aed9654f229199a6999f7dc817550b51fd009d9e8057de26c8cb11
|
4
|
+
data.tar.gz: 297d85ff20739bf100f7434d657a842adbaa783a17b10c459325724391d84196
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e9e1e6a9d98a32bc3819ef6ea4765f21a45ed103000186d71d394d10ac657178eb6498d55bebd2b7a54182cb6565593efc8f377f1ba8bf4074dbed030a81704f
|
7
|
+
data.tar.gz: 9cde425ae7847267b73118f741304a2a1b1b04cd7585d52c5d1bb0a5f51b664dd530c00a5e50c3bdecf9f0c0e3e3c1cc4a8a567a472e0d6745d28e26f84b76cb
|
data/README.md
CHANGED
@@ -128,6 +128,30 @@ CoerceFoo.call("foo" => "DEADBEEF")
|
|
128
128
|
# => {"foo"=>"DEADBEEF"}
|
129
129
|
```
|
130
130
|
|
131
|
+
### `date` and `datetime`
|
132
|
+
|
133
|
+
The `date` and `datetime` coercion functions will receive a `String` and give you `Date` and `DateTime` objects, respectively.
|
134
|
+
|
135
|
+
By default they expect an ISO 8601 string, but they provide a `format` option in case you need to parse something different, following the `strftime` format.
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
module CoerceFoo
|
139
|
+
extend Coercive
|
140
|
+
|
141
|
+
attribute :date_foo, date, optional
|
142
|
+
attribute :american_date, date(format: "%m-%d-%Y"), optional
|
143
|
+
attribute :datetime_foo, datetime, optional
|
144
|
+
end
|
145
|
+
|
146
|
+
CoerceFoo.call("date_foo" => "1988-05-18", "datetime_foo" => "1988-05-18T21:00:00Z", "american_date" => "05-18-1988")
|
147
|
+
# => {"date_foo"=>#<Date: 1988-05-18 ((2447300j,0s,0n),+0s,2299161j)>,
|
148
|
+
# "american_date"=>#<Date: 1988-05-18 ((2447300j,0s,0n),+0s,2299161j)>,
|
149
|
+
# "datetime_foo"=>#<DateTime: 1988-05-18T21:00:00+00:00 ((2447300j,75600s,0n),+0s,2299161j)>}
|
150
|
+
|
151
|
+
CoerceFoo.call("date_foo" => "18th May 1988")
|
152
|
+
# => Coercive::Error: {"date_foo"=>"not_valid"}
|
153
|
+
```
|
154
|
+
|
131
155
|
### `any`
|
132
156
|
|
133
157
|
The `any` coercion function lets anything pass through. It's commonly used with the `optional` fetch function when an attribute may or many not be a part of the input.
|
@@ -147,27 +171,90 @@ CoerceFoo.call("foo" => 4)
|
|
147
171
|
# => Coercive::Error: {"foo"=>"not_valid"}
|
148
172
|
```
|
149
173
|
|
150
|
-
### `
|
174
|
+
### `integer(min:, max:)`
|
175
|
+
|
176
|
+
`integer` expects an integer value. It supports optional `min` and `max` options to check if the user input is within certain bounds.
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
module CoerceFoo
|
180
|
+
extend Coercive
|
181
|
+
|
182
|
+
attribute :foo, integer, optional
|
183
|
+
attribute :foo_bounds, integer(min: 1, max: 10), optional
|
184
|
+
end
|
185
|
+
|
186
|
+
CoerceFoo.call("foo" => "1")
|
187
|
+
# => {"foo"=>1}
|
188
|
+
|
189
|
+
CoerceFoo.call("foo" => "bar")
|
190
|
+
# => Coercive::Error: {"foo"=>"not_valid"}
|
191
|
+
|
192
|
+
CoerceFoo.call("foo" => "1.5")
|
193
|
+
# => Coercive::Error: {"foo"=>"not_numeric"}
|
194
|
+
|
195
|
+
CoerceFoo.call("foo" => 1.5)
|
196
|
+
# => Coercive::Error: {"foo"=>"float_not_permitted"}
|
197
|
+
|
198
|
+
CoerceFoo.call("foo_bounds" => 0)
|
199
|
+
# => Coercive::Error: {"foo_bounds"=>"too_low"}
|
200
|
+
|
201
|
+
CoerceFoo.call("foo_bounds" => 11)
|
202
|
+
# => Coercive::Error: {"foo_bounds"=>"too_high"}
|
203
|
+
```
|
204
|
+
|
205
|
+
### `float(min:, max:)`
|
151
206
|
|
152
|
-
`float` expects, well, a float value.
|
207
|
+
`float` expects, well, a float value. It supports optional `min` and `max` options to check if the user input is within certain bounds.
|
153
208
|
|
154
209
|
```ruby
|
155
210
|
module CoerceFoo
|
156
211
|
extend Coercive
|
157
212
|
|
158
|
-
attribute :foo,
|
213
|
+
attribute :foo, float, optional
|
214
|
+
attribute :foo_bounds, float(min: 1.0, max: 5.5), optional
|
159
215
|
end
|
160
216
|
|
161
217
|
CoerceFoo.call("foo" => "bar")
|
162
218
|
# => Coercive::Error: {"foo"=>"not_valid"}
|
163
219
|
|
220
|
+
CoerceFoo.call("foo_bounds" => "0.5")
|
221
|
+
# => Coercive::Error: {"foo_bounds"=>"too_low"}
|
222
|
+
|
223
|
+
CoerceFoo.call("foo_bounds" => 6.5)
|
224
|
+
# => Coercive::Error: {"foo_bounds"=>"too_high"}
|
225
|
+
|
164
226
|
CoerceFoo.call("foo" => "0.1")
|
165
227
|
# => {"foo"=>0.1}
|
166
|
-
|
228
|
+
|
167
229
|
CoerceFoo.call("foo" => "0.1e5")
|
168
230
|
# => {"foo"=>10000.0}
|
169
231
|
```
|
170
232
|
|
233
|
+
### `boolean(true_if:, false_if:)
|
234
|
+
|
235
|
+
`boolean` will coerce input into `true` or `false`. You can also specifiy additional values to coerce into `true` or `false` with the `true_if` and `false_if` options.
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
module CoerceFoo
|
239
|
+
extend Coercive
|
240
|
+
|
241
|
+
attribute :foo, boolean, optional
|
242
|
+
attribute :foo_if, boolean(true_if: member(["1", "on"])), optional
|
243
|
+
end
|
244
|
+
|
245
|
+
CoerceFoo.call("foo" => true)
|
246
|
+
# => {"foo"=>true}
|
247
|
+
|
248
|
+
CoerceFoo.call("foo" => "true")
|
249
|
+
# => {"foo"=>true}
|
250
|
+
|
251
|
+
CoerceFoo.call("foo" => nil)
|
252
|
+
# => Coercive::Error: {"foo"=>"not_valid"}
|
253
|
+
|
254
|
+
CoerceFoo.call("foo_if" => "on")
|
255
|
+
# => {"foo_if"=>true}
|
256
|
+
```
|
257
|
+
|
171
258
|
### `array`
|
172
259
|
|
173
260
|
The `array` coercion is interesting because it's where `Coercive` starts to shine, by letting you compose coercion functions together. Let's see:
|
data/coercive.gemspec
CHANGED
data/lib/coercive.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require "date"
|
1
2
|
require_relative "coercive/uri"
|
2
3
|
|
3
4
|
# Public: The Coercive module implements a succinct DSL for declaring callable
|
@@ -149,14 +150,65 @@ module Coercive
|
|
149
150
|
end
|
150
151
|
end
|
151
152
|
|
153
|
+
# Public DSL: Return a coerce function to coerce input to an Integer.
|
154
|
+
# Used when declaring an attribute. See documentation for attr_coerce_fns.
|
155
|
+
def integer(min: nil, max: nil)
|
156
|
+
->(input) do
|
157
|
+
fail Coercive::Error.new("float_not_permitted") if input.is_a?(Float)
|
158
|
+
|
159
|
+
input = begin
|
160
|
+
Integer(input)
|
161
|
+
rescue TypeError, ArgumentError
|
162
|
+
fail Coercive::Error.new("not_numeric")
|
163
|
+
end
|
164
|
+
|
165
|
+
fail Coercive::Error.new("too_low") if min && input < min
|
166
|
+
fail Coercive::Error.new("too_high") if max && input > max
|
167
|
+
|
168
|
+
input
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
152
172
|
# Public DSL: Return a coerce function to coerce input to a Float.
|
153
173
|
# Used when declaring an attribute. See documentation for attr_coerce_fns.
|
154
|
-
def float
|
174
|
+
def float(min: nil, max: nil)
|
155
175
|
->(input) do
|
156
|
-
begin
|
157
|
-
|
158
|
-
|
159
|
-
|
176
|
+
input = begin
|
177
|
+
Float(input)
|
178
|
+
rescue TypeError, ArgumentError
|
179
|
+
fail Coercive::Error.new("not_numeric")
|
180
|
+
end
|
181
|
+
|
182
|
+
fail Coercive::Error.new("too_low") if min && input < min
|
183
|
+
fail Coercive::Error.new("too_high") if max && input > max
|
184
|
+
|
185
|
+
input
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Public DSL: Return a coerce function to coerce input to true or false.
|
190
|
+
# Used when declaring an attribute. See documentation for attr_coerce_fns.
|
191
|
+
#
|
192
|
+
# true_if - coerce function to override which values will coerce to true.
|
193
|
+
# false_if - coerce function to override which values will coerce to false.
|
194
|
+
def boolean(true_if: member([true, "true"]), false_if: member([false, "false"]))
|
195
|
+
->(input) do
|
196
|
+
test_success = ->(coerce_fn) do
|
197
|
+
begin
|
198
|
+
coerce_fn.call(input)
|
199
|
+
rescue Coercive::Error
|
200
|
+
return false
|
201
|
+
end
|
202
|
+
|
203
|
+
true
|
204
|
+
end
|
205
|
+
|
206
|
+
if test_success.(true_if)
|
207
|
+
true
|
208
|
+
elsif test_success.(false_if)
|
209
|
+
false
|
210
|
+
else
|
211
|
+
fail Coercive::Error.new("not_valid")
|
160
212
|
end
|
161
213
|
end
|
162
214
|
end
|
@@ -192,6 +244,48 @@ module Coercive
|
|
192
244
|
end
|
193
245
|
end
|
194
246
|
|
247
|
+
# Public DSL: Return a coercion function to coerce input into a Date.
|
248
|
+
# Used when declaring an attribute. See documentation for attr_coerce_fns.
|
249
|
+
#
|
250
|
+
# format - String following Ruby's `strftime` format to change the parsing behavior. When empty
|
251
|
+
# it will expect the String to be ISO 8601 compatible.
|
252
|
+
def date(format: nil)
|
253
|
+
->(input) do
|
254
|
+
input = begin
|
255
|
+
if format
|
256
|
+
Date.strptime(input, format)
|
257
|
+
else
|
258
|
+
Date.iso8601(input)
|
259
|
+
end
|
260
|
+
rescue ArgumentError
|
261
|
+
fail Coercive::Error.new("not_valid")
|
262
|
+
end
|
263
|
+
|
264
|
+
input
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Public DSL: Return a coercion function to coerce input into a DateTime.
|
269
|
+
# Used when declaring an attribute. See documentation for attr_coerce_fns.
|
270
|
+
#
|
271
|
+
# format - String following Ruby's `strftime` format to change the parsing behavior. When empty
|
272
|
+
# it will expect the String to be ISO 8601 compatible.
|
273
|
+
def datetime(format: nil)
|
274
|
+
->(input) do
|
275
|
+
input = begin
|
276
|
+
if format
|
277
|
+
DateTime.strptime(input, format)
|
278
|
+
else
|
279
|
+
DateTime.iso8601(input)
|
280
|
+
end
|
281
|
+
rescue ArgumentError
|
282
|
+
fail Coercive::Error.new("not_valid")
|
283
|
+
end
|
284
|
+
|
285
|
+
input
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
195
289
|
# Public DSL: Return a coercion function to coerce input to an Array.
|
196
290
|
# Used when declaring an attribute. See documentation for attr_coerce_fns.
|
197
291
|
#
|
data/test/coercive.rb
CHANGED
@@ -100,12 +100,57 @@ describe "Coercive" do
|
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
103
|
+
describe "integer" do
|
104
|
+
before do
|
105
|
+
@coercion = Module.new do
|
106
|
+
extend Coercive
|
107
|
+
|
108
|
+
attribute :foo, integer, optional
|
109
|
+
attribute :bar, integer, optional
|
110
|
+
attribute :baz, integer(min: 1, max: 10), optional
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
it "coerces the input value to an integer" do
|
115
|
+
attributes = { "foo" => "100", "bar" => 100 }
|
116
|
+
|
117
|
+
expected = { "foo" => 100, "bar" => 100 }
|
118
|
+
|
119
|
+
assert_equal expected, @coercion.call(attributes)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "doesn't allow Float" do
|
123
|
+
expected_errors = { "foo" => "float_not_permitted" }
|
124
|
+
|
125
|
+
assert_coercion_error(expected_errors) { @coercion.call("foo" => 100.5) }
|
126
|
+
end
|
127
|
+
|
128
|
+
it "errors if the input can't be coerced into an Integer" do
|
129
|
+
["nope", "100.5", "1e5"].each do |value|
|
130
|
+
expected_errors = { "foo" => "not_numeric" }
|
131
|
+
|
132
|
+
assert_coercion_error(expected_errors) { @coercion.call("foo" => value) }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
it "errors if the input is out of bounds" do
|
137
|
+
expected_errors = { "baz" => "too_low" }
|
138
|
+
|
139
|
+
assert_coercion_error(expected_errors) { @coercion.call("baz" => 0) }
|
140
|
+
|
141
|
+
expected_errors = { "baz" => "too_high" }
|
142
|
+
|
143
|
+
assert_coercion_error(expected_errors) { @coercion.call("baz" => 11) }
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
103
147
|
describe "float" do
|
104
148
|
before do
|
105
149
|
@coercion = Module.new do
|
106
150
|
extend Coercive
|
107
151
|
|
108
|
-
attribute :foo, float,
|
152
|
+
attribute :foo, float, optional
|
153
|
+
attribute :bar, float(min: 1.0, max: 5.5), optional
|
109
154
|
end
|
110
155
|
end
|
111
156
|
|
@@ -132,6 +177,74 @@ describe "Coercive" do
|
|
132
177
|
assert_coercion_error(expected_errors) { @coercion.call("foo" => bad) }
|
133
178
|
end
|
134
179
|
end
|
180
|
+
|
181
|
+
it "errors if the input is out of bounds" do
|
182
|
+
expected_errors = { "bar" => "too_low" }
|
183
|
+
|
184
|
+
assert_coercion_error(expected_errors) { @coercion.call("bar" => 0.5) }
|
185
|
+
|
186
|
+
expected_errors = { "bar" => "too_high" }
|
187
|
+
|
188
|
+
assert_coercion_error(expected_errors) { @coercion.call("bar" => 6.0) }
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe "boolean" do
|
193
|
+
before do
|
194
|
+
@coercion = Module.new do
|
195
|
+
extend Coercive
|
196
|
+
|
197
|
+
attribute :foo, boolean, optional
|
198
|
+
attribute :foo_true, boolean(true_if: member(["on", 1])), optional
|
199
|
+
attribute :foo_false, boolean(false_if: member(["off", "0"])), optional
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
it "supports true or false as String by default" do
|
204
|
+
[true, "true"].each do |value|
|
205
|
+
attributes = { "foo" => value }
|
206
|
+
|
207
|
+
expected = { "foo" => true }
|
208
|
+
|
209
|
+
assert_equal expected, @coercion.call(attributes)
|
210
|
+
end
|
211
|
+
|
212
|
+
[false, "false"].each do |value|
|
213
|
+
attributes = { "foo" => value }
|
214
|
+
|
215
|
+
expected = { "foo" => false }
|
216
|
+
|
217
|
+
assert_equal expected, @coercion.call(attributes)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
it "supports customizing values to coerce into true or false" do
|
222
|
+
["on", 1].each do |value|
|
223
|
+
attributes = { "foo_true" => value }
|
224
|
+
|
225
|
+
expected = { "foo_true" => true }
|
226
|
+
|
227
|
+
assert_equal expected, @coercion.call(attributes)
|
228
|
+
end
|
229
|
+
|
230
|
+
["off", "0"].each do |value|
|
231
|
+
attributes = { "foo_false" => value }
|
232
|
+
|
233
|
+
expected = { "foo_false" => false }
|
234
|
+
|
235
|
+
assert_equal expected, @coercion.call(attributes)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
it "fails on unknown values" do
|
240
|
+
["nope", nil].each do |value|
|
241
|
+
attributes = { "foo" => value }
|
242
|
+
|
243
|
+
expected_errors = { "foo" => "not_valid" }
|
244
|
+
|
245
|
+
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
246
|
+
end
|
247
|
+
end
|
135
248
|
end
|
136
249
|
|
137
250
|
describe "string" do
|
@@ -199,6 +312,84 @@ describe "Coercive" do
|
|
199
312
|
end
|
200
313
|
end
|
201
314
|
|
315
|
+
describe "date" do
|
316
|
+
before do
|
317
|
+
@coercion = Module.new do
|
318
|
+
extend Coercive
|
319
|
+
|
320
|
+
attribute :date, date, optional
|
321
|
+
attribute :american_date, date(format: "%m-%d-%Y"), optional
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
it "coerces a string into a Date object with ISO 8601 format by default" do
|
326
|
+
attributes = { "date" => "1988-05-18" }
|
327
|
+
|
328
|
+
expected = { "date" => Date.new(1988, 5, 18) }
|
329
|
+
|
330
|
+
assert_equal expected, @coercion.call(attributes)
|
331
|
+
end
|
332
|
+
|
333
|
+
it "supports a custom date format" do
|
334
|
+
attributes = { "american_date" => "05-18-1988" }
|
335
|
+
|
336
|
+
expected = { "american_date" => Date.new(1988, 5, 18) }
|
337
|
+
|
338
|
+
assert_equal expected, @coercion.call(attributes)
|
339
|
+
end
|
340
|
+
|
341
|
+
it "errors if the input doesn't parse" do
|
342
|
+
attributes = { "date" => "12-31-1990" }
|
343
|
+
|
344
|
+
expected_errors = { "date" => "not_valid" }
|
345
|
+
|
346
|
+
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
describe "datetime" do
|
351
|
+
before do
|
352
|
+
@coercion = Module.new do
|
353
|
+
extend Coercive
|
354
|
+
|
355
|
+
attribute :datetime, datetime, optional
|
356
|
+
attribute :american_date, datetime(format: "%m-%d-%Y %I:%M:%S %p"), optional
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
it "coerces a string into a DateTime object with ISO 8601 format by default" do
|
361
|
+
attributes = { "datetime" => "1988-05-18T21:00:00Z" }
|
362
|
+
|
363
|
+
expected = { "datetime" => DateTime.new(1988, 5, 18, 21, 00, 00) }
|
364
|
+
|
365
|
+
assert_equal expected, @coercion.call(attributes)
|
366
|
+
end
|
367
|
+
|
368
|
+
it "honors the timezone" do
|
369
|
+
attributes = { "datetime" => "1988-05-18T21:00:00-0300" }
|
370
|
+
|
371
|
+
expected = { "datetime" => DateTime.new(1988, 5, 18, 21, 00, 00, "-03:00") }
|
372
|
+
|
373
|
+
assert_equal expected, @coercion.call(attributes)
|
374
|
+
end
|
375
|
+
|
376
|
+
it "supports a custom date format" do
|
377
|
+
attributes = { "american_date" => "05-18-1988 09:00:00 PM" }
|
378
|
+
|
379
|
+
expected = { "american_date" => DateTime.new(1988, 5, 18, 21, 00, 00) }
|
380
|
+
|
381
|
+
assert_equal expected, @coercion.call(attributes)
|
382
|
+
end
|
383
|
+
|
384
|
+
it "errors if the input doesn't parse" do
|
385
|
+
attributes = { "datetime" => "12-31-1990T21:00:00Z" }
|
386
|
+
|
387
|
+
expected_errors = { "datetime" => "not_valid" }
|
388
|
+
|
389
|
+
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
202
393
|
describe "array" do
|
203
394
|
before do
|
204
395
|
@coercion = Module.new do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: coercive
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joe McIlvain
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2020-
|
12
|
+
date: 2020-09-21 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Coercive is a library to validate and coerce user input
|
15
15
|
email:
|