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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3eee84a8970b92e6b5d1b0e01e51a40c66cc0c7298b0fe37a6ea44aecd7b9064
4
- data.tar.gz: 9504d35e8ce6376fd821110a15d079783f723af1ae78606fa7fd2e565c053487
3
+ metadata.gz: af1258f102aed9654f229199a6999f7dc817550b51fd009d9e8057de26c8cb11
4
+ data.tar.gz: 297d85ff20739bf100f7434d657a842adbaa783a17b10c459325724391d84196
5
5
  SHA512:
6
- metadata.gz: 41fef05cb7a8c6a1d2f9b7f0f3737d76c5756a36903d6e2c45d028fb539d4de50f9ac0720b07e4262b97df2ff187638e0cf6d5fef4692d470ddc9484e00a9b57
7
- data.tar.gz: 94c3e1e38e740c7089d8913977fbd1feea04d1df592d918c76b46be18c53787c95d36fed451a39191781db63718a941f31d53b4ac56d960384e1e854ed4d6066
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
- ### `float`
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, float, optional
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:
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "coercive"
3
- s.version = "1.1.0"
3
+ s.version = "1.6.0"
4
4
  s.summary = "Coercive is a library to validate and coerce user input"
5
5
  s.description = s.summary
6
6
  s.authors = ["Joe McIlvain", "Lucas Tolchinsky"]
@@ -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
- Float(input)
158
- rescue TypeError, ArgumentError
159
- fail Coercive::Error.new("not_numeric")
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
  #
@@ -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, required
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.1.0
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-08-13 00:00:00.000000000 Z
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: