coercive 1.0.1 → 1.5.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: 867918b9a3ee09bdfc89a10e802032e229fdf4ab250f80727a68197f8144d615
4
- data.tar.gz: 9d1dcfcc05db65d1669049c87f36bf9a252fe91ce68b9298a7578ce630aeae50
3
+ metadata.gz: a9ce029330ac405fde945a05e6f81c62361027ebe5d03659fd82b067554087e7
4
+ data.tar.gz: 89cfe35313fde4e0b0cea25d6050b008e75c8457ca440b71cf1c2acca26d13d1
5
5
  SHA512:
6
- metadata.gz: 2dc34142c2553b747290451db7ca631dd12b31e9e7488502e4a54b3fc1cf372a7dd498c951498f8c3b86d519172bb879fb7f650e97448260dd8fad2c5c5c8662
7
- data.tar.gz: 513cb240285c58fbf784d3a61fae066bf7c768836cdc06281357b92f8c7a62ad72ea2c0114f3ba6645da29b4ed01fbabff616f10db91f93b8194c587d2f9c623
6
+ metadata.gz: d5260061302b501653bf64ce29988622f5ea06739a32058778a64106ad25ddd28567773048ac48addf510adc5f08357f0f6258c2877b67b8e2c0a3e3352baf48
7
+ data.tar.gz: 25e42dec9a930a6cea139f581114abf59d166a95210a5884edb0276a29bbbee7cb365959d51c6caddc4c76c827d0dfecf5bdb71386e262d2e8fccd3b20decacc
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,20 +171,58 @@ CoerceFoo.call("foo" => 4)
147
171
  # => Coercive::Error: {"foo"=>"not_valid"}
148
172
  ```
149
173
 
150
- ### `float`
174
+ ### `integer(min:, max:)`
151
175
 
152
- `float` expects, well, a float value.
176
+ `integer` expects an integer value. It supports optional `min` and `max` options to check if the user input is within certain bounds.
153
177
 
154
178
  ```ruby
155
179
  module CoerceFoo
156
180
  extend Coercive
157
181
 
158
- attribute :foo, float, optional
182
+ attribute :foo, integer, optional
183
+ attribute :foo_bounds, integer(min: 1, max: 10), optional
159
184
  end
160
185
 
186
+ CoerceFoo.call("foo" => "1")
187
+ # => {"foo"=>1}
188
+
161
189
  CoerceFoo.call("foo" => "bar")
162
190
  # => Coercive::Error: {"foo"=>"not_valid"}
163
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:)`
206
+
207
+ `float` expects, well, a float value. It supports optional `min` and `max` options to check if the user input is within certain bounds.
208
+
209
+ ```ruby
210
+ module CoerceFoo
211
+ extend Coercive
212
+
213
+ attribute :foo, float, optional
214
+ attribute :foo_bounds, float(min: 1.0, max: 5.5), optional
215
+ end
216
+
217
+ CoerceFoo.call("foo" => "bar")
218
+ # => Coercive::Error: {"foo"=>"not_valid"}
219
+
220
+ CoerceFoo.call("foo" => "0.5")
221
+ # => Coercive::Error: {"foo"=>"too_low"}
222
+
223
+ CoerceFoo.call("foo" => 6.5)
224
+ # => Coercive::Error: {"foo"=>"too_high"}
225
+
164
226
  CoerceFoo.call("foo" => "0.1")
165
227
  # => {"foo"=>0.1}
166
228
 
@@ -212,4 +274,45 @@ CoerceFoo.call("foo" => {"barrrr" => "0.1"})
212
274
 
213
275
  ### `uri`
214
276
 
215
- The `uri` coercion validates
277
+ The `uri` coercion function really showcases how it's very easy to build custom logic to validate and coerce any kind of input. `uri` is meant to verify IP and URLs and has a variety of options.
278
+
279
+ ```ruby
280
+ module CoerceFoo
281
+ extend Coercive
282
+
283
+ attribute :foo, uri(string), optional
284
+ end
285
+
286
+ CoerceFoo.call("foo" => "http://github.com")
287
+ # => {"foo"=>"http://github.com"}
288
+
289
+ CoerceFoo.call("foo" => "not a url")
290
+ # => Coercive::Error: {"foo"=>"not_valid"}
291
+ ```
292
+
293
+ #### Requiring a specific URI schema
294
+
295
+ The `schema_fn` option allows you to compose additional coercion functions to verify the schema.
296
+
297
+ ```ruby
298
+ module CoerceFoo
299
+ extend Coercive
300
+
301
+ attribute :foo, uri(string, schema_fn: member(%w{http https})), optional
302
+ end
303
+
304
+ CoerceFoo.call("foo" => "https://github.com")
305
+ # => {"foo"=>"https://github.com"}
306
+
307
+ CoerceFoo.call("foo" => "ftp://github.com")
308
+ # => Coercive::Error: {"foo"=>"unsupported_schema"}
309
+ ```
310
+
311
+ #### Requiring URI elements
312
+
313
+ There's a number of boolean options to enforce the presence of parts of a URI to be present. By default they're all false.
314
+
315
+ * `require_path`: for example, `"https://github.com/Theorem"`
316
+ * `require_port`: for example, `"https://github.com:433"`
317
+ * `require_user`: for example, `"https://user@github.com"`
318
+ * `require_password`: for example, `"https://:password@github.com"`
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "coercive"
3
- s.version = "1.0.1"
3
+ s.version = "1.5.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,15 +150,39 @@ 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")
160
- end
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
161
186
  end
162
187
  end
163
188
 
@@ -192,6 +217,48 @@ module Coercive
192
217
  end
193
218
  end
194
219
 
220
+ # Public DSL: Return a coercion function to coerce input into a Date.
221
+ # Used when declaring an attribute. See documentation for attr_coerce_fns.
222
+ #
223
+ # format - String following Ruby's `strftime` format to change the parsing behavior. When empty
224
+ # it will expect the String to be ISO 8601 compatible.
225
+ def date(format: nil)
226
+ ->(input) do
227
+ input = begin
228
+ if format
229
+ Date.strptime(input, format)
230
+ else
231
+ Date.iso8601(input)
232
+ end
233
+ rescue ArgumentError
234
+ fail Coercive::Error.new("not_valid")
235
+ end
236
+
237
+ input
238
+ end
239
+ end
240
+
241
+ # Public DSL: Return a coercion function to coerce input into a DateTime.
242
+ # Used when declaring an attribute. See documentation for attr_coerce_fns.
243
+ #
244
+ # format - String following Ruby's `strftime` format to change the parsing behavior. When empty
245
+ # it will expect the String to be ISO 8601 compatible.
246
+ def datetime(format: nil)
247
+ ->(input) do
248
+ input = begin
249
+ if format
250
+ DateTime.strptime(input, format)
251
+ else
252
+ DateTime.iso8601(input)
253
+ end
254
+ rescue ArgumentError
255
+ fail Coercive::Error.new("not_valid")
256
+ end
257
+
258
+ input
259
+ end
260
+ end
261
+
195
262
  # Public DSL: Return a coercion function to coerce input to an Array.
196
263
  # Used when declaring an attribute. See documentation for attr_coerce_fns.
197
264
  #
@@ -3,27 +3,6 @@ require "uri"
3
3
 
4
4
  module Coercive
5
5
  module URI
6
- # The IP ranges below are considered private and by default not permitted by the `uri`
7
- # coercion function. To allow connecting to local services (in development, for example)
8
- # users can set the `allow_private_ip` option, which ignores if the URI resolves to a public
9
- # address or not.
10
- PRIVATE_IP_RANGES = [
11
- IPAddr.new("0.0.0.0/8"), # Broadcasting to the current network. RFC 1700.
12
- IPAddr.new("10.0.0.0/8"), # Local private network. RFC 1918.
13
- IPAddr.new("127.0.0.0/8"), # Loopback addresses to the localhost. RFC 990.
14
- IPAddr.new("169.254.0.0/16"), # link-local addresses between two hosts on a single link. RFC 3927.
15
- IPAddr.new("172.16.0.0/12"), # Local private network. RFC 1918.
16
- IPAddr.new("192.168.0.0/16"), # Local private network. RFC 1918.
17
- IPAddr.new("198.18.0.0/15"), # Testing of inter-network communications between two separate subnets. RFC 2544.
18
- IPAddr.new("198.51.100.0/24"), # Assigned as "TEST-NET-2" in RFC 5737.
19
- IPAddr.new("203.0.113.0/24"), # Assigned as "TEST-NET-3" in RFC 5737.
20
- IPAddr.new("240.0.0.0/4"), # Reserved for future use, as specified by RFC 6890
21
- IPAddr.new("::1/128"), # Loopback addresses to the localhost. RFC 5156.
22
- IPAddr.new("2001:20::/28"), # Non-routed IPv6 addresses used for Cryptographic Hash Identifiers. RFC 7343.
23
- IPAddr.new("fc00::/7"), # Unique Local Addresses (ULAs). RFC 1918.
24
- IPAddr.new("fe80::/10"), # link-local addresses between two hosts on a single link. RFC 3927.
25
- ].freeze
26
-
27
6
  # Public DSL: Return a coercion function to coerce input to a URI.
28
7
  # Used when declaring an attribute. See documentation for attr_coerce_fns.
29
8
  #
@@ -34,8 +13,7 @@ module Coercive
34
13
  # require_user - set true to make the URI user a required element
35
14
  # require_password - set true to make the URI password a required element
36
15
  def self.coerce_fn(string_coerce_fn, schema_fn: nil, require_path: false,
37
- require_port: false, require_user: false, require_password: false,
38
- allow_private_ip: false)
16
+ require_port: false, require_user: false, require_password: false)
39
17
  ->(input) do
40
18
  uri = begin
41
19
  ::URI.parse(string_coerce_fn.call(input))
@@ -44,7 +22,6 @@ module Coercive
44
22
  end
45
23
 
46
24
  fail Coercive::Error.new("no_host") unless uri.host
47
- fail Coercive::Error.new("not_resolvable") unless allow_private_ip || resolvable_public_ip?(uri)
48
25
  fail Coercive::Error.new("no_path") if require_path && uri.path.empty?
49
26
  fail Coercive::Error.new("no_port") if require_port && !uri.port
50
27
  fail Coercive::Error.new("no_user") if require_user && !uri.user
@@ -61,40 +38,5 @@ module Coercive
61
38
  uri.to_s
62
39
  end
63
40
  end
64
-
65
- # Internal: Return true if the given URI is resolvable to a non-private IP.
66
- #
67
- # uri - the URI to check.
68
- def self.resolvable_public_ip?(uri)
69
- begin
70
- _, _, _, *resolved_addresses = Socket.gethostbyname(uri.host)
71
- rescue SocketError
72
- return false
73
- end
74
-
75
- resolved_addresses.none? do |bytes|
76
- ip = ip_from_bytes(bytes)
77
-
78
- ip.nil? || PRIVATE_IP_RANGES.any? { |range| range.include?(ip) }
79
- end
80
- end
81
-
82
- # Internal: Return an IPAddr built from the given address bytes.
83
- #
84
- # bytes - the binary-encoded String returned by Socket.gethostbyname.
85
- def self.ip_from_bytes(bytes)
86
- octets = bytes.unpack("C*")
87
-
88
- string =
89
- if octets.length == 4 # IPv4
90
- octets.join(".")
91
- else # IPv6
92
- octets.map { |i| "%02x" % i }.each_slice(2).map(&:join).join(":")
93
- end
94
-
95
- IPAddr.new(string)
96
- rescue IPAddr::InvalidAddressError
97
- nil
98
- end
99
41
  end
100
42
  end
@@ -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,16 @@ 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
135
190
  end
136
191
 
137
192
  describe "string" do
@@ -199,6 +254,84 @@ describe "Coercive" do
199
254
  end
200
255
  end
201
256
 
257
+ describe "date" do
258
+ before do
259
+ @coercion = Module.new do
260
+ extend Coercive
261
+
262
+ attribute :date, date, optional
263
+ attribute :american_date, date(format: "%m-%d-%Y"), optional
264
+ end
265
+ end
266
+
267
+ it "coerces a string into a Date object with ISO 8601 format by default" do
268
+ attributes = { "date" => "1988-05-18" }
269
+
270
+ expected = { "date" => Date.new(1988, 5, 18) }
271
+
272
+ assert_equal expected, @coercion.call(attributes)
273
+ end
274
+
275
+ it "supports a custom date format" do
276
+ attributes = { "american_date" => "05-18-1988" }
277
+
278
+ expected = { "american_date" => Date.new(1988, 5, 18) }
279
+
280
+ assert_equal expected, @coercion.call(attributes)
281
+ end
282
+
283
+ it "errors if the input doesn't parse" do
284
+ attributes = { "date" => "12-31-1990" }
285
+
286
+ expected_errors = { "date" => "not_valid" }
287
+
288
+ assert_coercion_error(expected_errors) { @coercion.call(attributes) }
289
+ end
290
+ end
291
+
292
+ describe "datetime" do
293
+ before do
294
+ @coercion = Module.new do
295
+ extend Coercive
296
+
297
+ attribute :datetime, datetime, optional
298
+ attribute :american_date, datetime(format: "%m-%d-%Y %I:%M:%S %p"), optional
299
+ end
300
+ end
301
+
302
+ it "coerces a string into a DateTime object with ISO 8601 format by default" do
303
+ attributes = { "datetime" => "1988-05-18T21:00:00Z" }
304
+
305
+ expected = { "datetime" => DateTime.new(1988, 5, 18, 21, 00, 00) }
306
+
307
+ assert_equal expected, @coercion.call(attributes)
308
+ end
309
+
310
+ it "honors the timezone" do
311
+ attributes = { "datetime" => "1988-05-18T21:00:00-0300" }
312
+
313
+ expected = { "datetime" => DateTime.new(1988, 5, 18, 21, 00, 00, "-03:00") }
314
+
315
+ assert_equal expected, @coercion.call(attributes)
316
+ end
317
+
318
+ it "supports a custom date format" do
319
+ attributes = { "american_date" => "05-18-1988 09:00:00 PM" }
320
+
321
+ expected = { "american_date" => DateTime.new(1988, 5, 18, 21, 00, 00) }
322
+
323
+ assert_equal expected, @coercion.call(attributes)
324
+ end
325
+
326
+ it "errors if the input doesn't parse" do
327
+ attributes = { "datetime" => "12-31-1990T21:00:00Z" }
328
+
329
+ expected_errors = { "datetime" => "not_valid" }
330
+
331
+ assert_coercion_error(expected_errors) { @coercion.call(attributes) }
332
+ end
333
+ end
334
+
202
335
  describe "array" do
203
336
  before do
204
337
  @coercion = Module.new do
@@ -39,10 +39,6 @@ describe "Coercive::URI" do
39
39
  attribute :require_password,
40
40
  uri(string(min: 1, max: 255), require_password: true),
41
41
  optional
42
-
43
- attribute :allow_private_ip,
44
- uri(string(min: 1, max: 255), allow_private_ip: true),
45
- optional
46
42
  end
47
43
  end
48
44
 
@@ -127,61 +123,12 @@ describe "Coercive::URI" do
127
123
  assert_coercion_error(expected_errors) { @coercion.call(attributes) }
128
124
  end
129
125
 
130
- Coercive::URI::PRIVATE_IP_RANGES.each do |range|
131
- range = range.to_range
132
- first = range.first
133
- last = range.last
134
- first = first.ipv6? ? "[#{first}]" : first.to_s
135
- last = last.ipv6? ? "[#{last}]" : last.to_s
136
-
137
- it "errors when the URI host is an IP in the range #{first}..#{last}" do
138
- attributes_first = { "schema" => "http://#{first}/path" }
139
- attributes_last = { "schema" => "http://#{last}/path" }
140
- expected_errors = { "schema" => "not_resolvable" }
141
-
142
- assert_coercion_error(expected_errors) { @coercion.call(attributes_first) }
143
- assert_coercion_error(expected_errors) { @coercion.call(attributes_last) }
144
- end
145
-
146
- it "allows overriding private IP address checks" do
147
- attributes_first = { "allow_private_ip" => "http://#{first}/path" }
148
- attributes_last = { "allow_private_ip" => "http://#{last}/path" }
149
-
150
- assert_equal attributes_first, @coercion.call(attributes_first)
151
- assert_equal attributes_last, @coercion.call(attributes_last)
152
- end
153
- end
154
-
155
- it "errors when the URI host is not resolvable" do
156
- attributes = {
157
- "schema" => "http://bogus-host-that-cant-possibly-exist-here/path"
158
- }
159
-
160
- expected_errors = { "schema" => "not_resolvable" }
161
-
162
- assert_coercion_error(expected_errors) { @coercion.call(attributes) }
163
- end
164
-
165
- it "errors when the URI host resolves to an IP in a private range" do
166
- attributes = { "schema" => "http://localhost/path" }
167
-
168
- expected_errors = { "schema" => "not_resolvable" }
169
-
170
- assert_coercion_error(expected_errors) { @coercion.call(attributes) }
171
- end
172
-
173
- it "allows a URI host to be IP that isn't in a private range" do
126
+ it "allows a URI host to be an IP" do
174
127
  attributes = { "schema" => "http://8.8.8.8/path" }
175
128
 
176
129
  assert_equal attributes, @coercion.call(attributes)
177
130
  end
178
131
 
179
- it "allows a URI host that resolves to an IP not in a private range" do
180
- attributes = { "schema" => "http://www.example.com/path" }
181
-
182
- assert_equal attributes, @coercion.call(attributes)
183
- end
184
-
185
132
  it "allows a URI with no explicit path component" do
186
133
  attributes = { "schema" => "http://www.example.com" }
187
134
 
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.0.1
4
+ version: 1.5.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-05 00:00:00.000000000 Z
12
+ date: 2020-09-03 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Coercive is a library to validate and coerce user input
15
15
  email: