coercive 1.0.0 → 1.4.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 +112 -2
- data/coercive.gemspec +12 -0
- data/lib/coercive.rb +62 -0
- data/lib/coercive/uri.rb +9 -70
- data/test/coercive.rb +123 -1
- data/test/uri.rb +13 -54
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5ece0274ec3187ab9ae2407837048143d6c1c516bd4ddcfc953a208e5bee6ed
|
4
|
+
data.tar.gz: 6b9ffb6fa9c18a0d9049193c7e352201f559b86a98cd4258d49b756ca8bf4df9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d036ba5c9a7b0526903330fe17aff3e34f47f0c0a338769fb0762fed5da8ca7d0210143b0f87533dba2010637feed320cbf454d78d629ffd5d3ebb13a8ab18fb
|
7
|
+
data.tar.gz: c5eb1b8af3fb355081d15108bf037fd4f5ec9b85be892f875500b419a343a8d4136c26ce0ff3ca0c8fca8bf3bb91f7ac4acf8439369009fbec14dfaf98e5e043
|
data/README.md
CHANGED
@@ -1,10 +1,20 @@
|
|
1
|
-
#
|
1
|
+
# Coercive
|
2
|
+
|
3
|
+
# Install
|
4
|
+
|
5
|
+
```
|
6
|
+
$ gem install coercive
|
7
|
+
```
|
8
|
+
|
9
|
+
# Usage
|
2
10
|
|
3
11
|
`Coercive` is a Ruby library to validate and coerce user input.
|
4
12
|
|
5
13
|
Define your coercion modules like this:
|
6
14
|
|
7
15
|
```ruby
|
16
|
+
require "coercive"
|
17
|
+
|
8
18
|
module CoerceFoo
|
9
19
|
extend Coercive
|
10
20
|
|
@@ -118,6 +128,30 @@ CoerceFoo.call("foo" => "DEADBEEF")
|
|
118
128
|
# => {"foo"=>"DEADBEEF"}
|
119
129
|
```
|
120
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
|
+
|
121
155
|
### `any`
|
122
156
|
|
123
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.
|
@@ -137,6 +171,37 @@ CoerceFoo.call("foo" => 4)
|
|
137
171
|
# => Coercive::Error: {"foo"=>"not_valid"}
|
138
172
|
```
|
139
173
|
|
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
|
+
|
140
205
|
### `float`
|
141
206
|
|
142
207
|
`float` expects, well, a float value.
|
@@ -184,7 +249,7 @@ CoerceFoo.call("foo" => [BasicObject.new])
|
|
184
249
|
|
185
250
|
### `hash`
|
186
251
|
|
187
|
-
`hash` coercion let's you manipulate the key and values, similarly to how `array` does
|
252
|
+
`hash` coercion let's you manipulate the key and values, similarly to how `array` does.
|
188
253
|
|
189
254
|
```ruby
|
190
255
|
module CoerceFoo
|
@@ -199,3 +264,48 @@ CoerceFoo.call("foo" => {"bar" => "0.1"})
|
|
199
264
|
CoerceFoo.call("foo" => {"barrrr" => "0.1"})
|
200
265
|
# => Coercive::Error: {"foo"=>{"barrrr"=>"too_long"}}
|
201
266
|
```
|
267
|
+
|
268
|
+
### `uri`
|
269
|
+
|
270
|
+
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.
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
module CoerceFoo
|
274
|
+
extend Coercive
|
275
|
+
|
276
|
+
attribute :foo, uri(string), optional
|
277
|
+
end
|
278
|
+
|
279
|
+
CoerceFoo.call("foo" => "http://github.com")
|
280
|
+
# => {"foo"=>"http://github.com"}
|
281
|
+
|
282
|
+
CoerceFoo.call("foo" => "not a url")
|
283
|
+
# => Coercive::Error: {"foo"=>"not_valid"}
|
284
|
+
```
|
285
|
+
|
286
|
+
#### Requiring a specific URI schema
|
287
|
+
|
288
|
+
The `schema_fn` option allows you to compose additional coercion functions to verify the schema.
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
module CoerceFoo
|
292
|
+
extend Coercive
|
293
|
+
|
294
|
+
attribute :foo, uri(string, schema_fn: member(%w{http https})), optional
|
295
|
+
end
|
296
|
+
|
297
|
+
CoerceFoo.call("foo" => "https://github.com")
|
298
|
+
# => {"foo"=>"https://github.com"}
|
299
|
+
|
300
|
+
CoerceFoo.call("foo" => "ftp://github.com")
|
301
|
+
# => Coercive::Error: {"foo"=>"unsupported_schema"}
|
302
|
+
```
|
303
|
+
|
304
|
+
#### Requiring URI elements
|
305
|
+
|
306
|
+
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.
|
307
|
+
|
308
|
+
* `require_path`: for example, `"https://github.com/Theorem"`
|
309
|
+
* `require_port`: for example, `"https://github.com:433"`
|
310
|
+
* `require_user`: for example, `"https://user@github.com"`
|
311
|
+
* `require_password`: for example, `"https://:password@github.com"`
|
data/coercive.gemspec
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "coercive"
|
3
|
+
s.version = "1.4.0"
|
4
|
+
s.summary = "Coercive is a library to validate and coerce user input"
|
5
|
+
s.description = s.summary
|
6
|
+
s.authors = ["Joe McIlvain", "Lucas Tolchinsky"]
|
7
|
+
s.email = ["joe.eli.mac@gmail.com", "tonchis@protonmail.com"]
|
8
|
+
s.homepage = "https://github.com/Theorem/coercive"
|
9
|
+
s.license = "MIT"
|
10
|
+
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
end
|
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,6 +150,25 @@ 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
174
|
def float
|
@@ -192,6 +212,48 @@ module Coercive
|
|
192
212
|
end
|
193
213
|
end
|
194
214
|
|
215
|
+
# Public DSL: Return a coercion function to coerce input into a Date.
|
216
|
+
# Used when declaring an attribute. See documentation for attr_coerce_fns.
|
217
|
+
#
|
218
|
+
# format - String following Ruby's `strftime` format to change the parsing behavior. When empty
|
219
|
+
# it will expect the String to be ISO 8601 compatible.
|
220
|
+
def date(format: nil)
|
221
|
+
->(input) do
|
222
|
+
input = begin
|
223
|
+
if format
|
224
|
+
Date.strptime(input, format)
|
225
|
+
else
|
226
|
+
Date.iso8601(input)
|
227
|
+
end
|
228
|
+
rescue ArgumentError
|
229
|
+
fail Coercive::Error.new("not_valid")
|
230
|
+
end
|
231
|
+
|
232
|
+
input
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Public DSL: Return a coercion function to coerce input into a DateTime.
|
237
|
+
# Used when declaring an attribute. See documentation for attr_coerce_fns.
|
238
|
+
#
|
239
|
+
# format - String following Ruby's `strftime` format to change the parsing behavior. When empty
|
240
|
+
# it will expect the String to be ISO 8601 compatible.
|
241
|
+
def datetime(format: nil)
|
242
|
+
->(input) do
|
243
|
+
input = begin
|
244
|
+
if format
|
245
|
+
DateTime.strptime(input, format)
|
246
|
+
else
|
247
|
+
DateTime.iso8601(input)
|
248
|
+
end
|
249
|
+
rescue ArgumentError
|
250
|
+
fail Coercive::Error.new("not_valid")
|
251
|
+
end
|
252
|
+
|
253
|
+
input
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
195
257
|
# Public DSL: Return a coercion function to coerce input to an Array.
|
196
258
|
# Used when declaring an attribute. See documentation for attr_coerce_fns.
|
197
259
|
#
|
data/lib/coercive/uri.rb
CHANGED
@@ -1,33 +1,8 @@
|
|
1
1
|
require "ipaddr"
|
2
2
|
require "uri"
|
3
3
|
|
4
|
-
module
|
4
|
+
module Coercive
|
5
5
|
module URI
|
6
|
-
# Setting this `true` allows outbound connections to private IP addresses,
|
7
|
-
# bypassing the security check that the IP address is public. This is designed
|
8
|
-
# to be used in devlopment so that the tests can connect to local services.
|
9
|
-
#
|
10
|
-
# This SHOULD NOT be set in PRODUCTION.
|
11
|
-
ALLOW_PRIVATE_IP_CONNECTIONS =
|
12
|
-
ENV.fetch("ALLOW_PRIVATE_IP_CONNECTIONS", "").downcase == "true"
|
13
|
-
|
14
|
-
PRIVATE_IP_RANGES = [
|
15
|
-
IPAddr.new("0.0.0.0/8"), # Broadcasting to the current network. RFC 1700.
|
16
|
-
IPAddr.new("10.0.0.0/8"), # Local private network. RFC 1918.
|
17
|
-
IPAddr.new("127.0.0.0/8"), # Loopback addresses to the localhost. RFC 990.
|
18
|
-
IPAddr.new("169.254.0.0/16"), # link-local addresses between two hosts on a single link. RFC 3927.
|
19
|
-
IPAddr.new("172.16.0.0/12"), # Local private network. RFC 1918.
|
20
|
-
IPAddr.new("192.168.0.0/16"), # Local private network. RFC 1918.
|
21
|
-
IPAddr.new("198.18.0.0/15"), # Testing of inter-network communications between two separate subnets. RFC 2544.
|
22
|
-
IPAddr.new("198.51.100.0/24"), # Assigned as "TEST-NET-2" in RFC 5737.
|
23
|
-
IPAddr.new("203.0.113.0/24"), # Assigned as "TEST-NET-3" in RFC 5737.
|
24
|
-
IPAddr.new("240.0.0.0/4"), # Reserved for future use, as specified by RFC 6890
|
25
|
-
IPAddr.new("::1/128"), # Loopback addresses to the localhost. RFC 5156.
|
26
|
-
IPAddr.new("2001:20::/28"), # Non-routed IPv6 addresses used for Cryptographic Hash Identifiers. RFC 7343.
|
27
|
-
IPAddr.new("fc00::/7"), # Unique Local Addresses (ULAs). RFC 1918.
|
28
|
-
IPAddr.new("fe80::/10"), # link-local addresses between two hosts on a single link. RFC 3927.
|
29
|
-
].freeze
|
30
|
-
|
31
6
|
# Public DSL: Return a coercion function to coerce input to a URI.
|
32
7
|
# Used when declaring an attribute. See documentation for attr_coerce_fns.
|
33
8
|
#
|
@@ -43,61 +18,25 @@ module Coercion
|
|
43
18
|
uri = begin
|
44
19
|
::URI.parse(string_coerce_fn.call(input))
|
45
20
|
rescue ::URI::InvalidURIError
|
46
|
-
fail
|
21
|
+
fail Coercive::Error.new("not_valid")
|
47
22
|
end
|
48
23
|
|
49
|
-
fail
|
50
|
-
fail
|
51
|
-
fail
|
52
|
-
fail
|
53
|
-
fail
|
54
|
-
fail Coercion::Error.new("no_password") if require_password && !uri.password
|
24
|
+
fail Coercive::Error.new("no_host") unless uri.host
|
25
|
+
fail Coercive::Error.new("no_path") if require_path && uri.path.empty?
|
26
|
+
fail Coercive::Error.new("no_port") if require_port && !uri.port
|
27
|
+
fail Coercive::Error.new("no_user") if require_user && !uri.user
|
28
|
+
fail Coercive::Error.new("no_password") if require_password && !uri.password
|
55
29
|
|
56
30
|
if schema_fn
|
57
31
|
begin
|
58
32
|
schema_fn.call(uri.scheme)
|
59
|
-
rescue
|
60
|
-
fail
|
33
|
+
rescue Coercive::Error
|
34
|
+
fail Coercive::Error.new("unsupported_schema")
|
61
35
|
end
|
62
36
|
end
|
63
37
|
|
64
38
|
uri.to_s
|
65
39
|
end
|
66
40
|
end
|
67
|
-
|
68
|
-
# Internal: Return true if the given URI is resolvable to a non-private IP.
|
69
|
-
#
|
70
|
-
# uri - the URI to check.
|
71
|
-
def self.resolvable_public_ip?(uri)
|
72
|
-
begin
|
73
|
-
_, _, _, *resolved_addresses = Socket.gethostbyname(uri.host)
|
74
|
-
rescue SocketError
|
75
|
-
return false
|
76
|
-
end
|
77
|
-
|
78
|
-
resolved_addresses.none? do |bytes|
|
79
|
-
ip = ip_from_bytes(bytes)
|
80
|
-
|
81
|
-
ip.nil? || PRIVATE_IP_RANGES.any? { |range| range.include?(ip) }
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
# Internal: Return an IPAddr built from the given address bytes.
|
86
|
-
#
|
87
|
-
# bytes - the binary-encoded String returned by Socket.gethostbyname.
|
88
|
-
def self.ip_from_bytes(bytes)
|
89
|
-
octets = bytes.unpack("C*")
|
90
|
-
|
91
|
-
string =
|
92
|
-
if octets.length == 4 # IPv4
|
93
|
-
octets.join(".")
|
94
|
-
else # IPv6
|
95
|
-
octets.map { |i| "%02x" % i }.each_slice(2).map(&:join).join(":")
|
96
|
-
end
|
97
|
-
|
98
|
-
IPAddr.new(string)
|
99
|
-
rescue IPAddr::InvalidAddressError
|
100
|
-
nil
|
101
|
-
end
|
102
41
|
end
|
103
42
|
end
|
data/test/coercive.rb
CHANGED
@@ -100,6 +100,50 @@ 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
|
@@ -113,7 +157,7 @@ describe "Coercive" do
|
|
113
157
|
fixnum = 2
|
114
158
|
rational = 2 ** -2
|
115
159
|
bignum = 2 ** 64
|
116
|
-
bigdecimal = BigDecimal
|
160
|
+
bigdecimal = BigDecimal("0.1")
|
117
161
|
|
118
162
|
[fixnum, rational, bignum, bigdecimal].each do |value|
|
119
163
|
attributes = { "foo" => value }
|
@@ -199,6 +243,84 @@ describe "Coercive" do
|
|
199
243
|
end
|
200
244
|
end
|
201
245
|
|
246
|
+
describe "date" do
|
247
|
+
before do
|
248
|
+
@coercion = Module.new do
|
249
|
+
extend Coercive
|
250
|
+
|
251
|
+
attribute :date, date, optional
|
252
|
+
attribute :american_date, date(format: "%m-%d-%Y"), optional
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
it "coerces a string into a Date object with ISO 8601 format by default" do
|
257
|
+
attributes = { "date" => "1988-05-18" }
|
258
|
+
|
259
|
+
expected = { "date" => Date.new(1988, 5, 18) }
|
260
|
+
|
261
|
+
assert_equal expected, @coercion.call(attributes)
|
262
|
+
end
|
263
|
+
|
264
|
+
it "supports a custom date format" do
|
265
|
+
attributes = { "american_date" => "05-18-1988" }
|
266
|
+
|
267
|
+
expected = { "american_date" => Date.new(1988, 5, 18) }
|
268
|
+
|
269
|
+
assert_equal expected, @coercion.call(attributes)
|
270
|
+
end
|
271
|
+
|
272
|
+
it "errors if the input doesn't parse" do
|
273
|
+
attributes = { "date" => "12-31-1990" }
|
274
|
+
|
275
|
+
expected_errors = { "date" => "not_valid" }
|
276
|
+
|
277
|
+
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
describe "datetime" do
|
282
|
+
before do
|
283
|
+
@coercion = Module.new do
|
284
|
+
extend Coercive
|
285
|
+
|
286
|
+
attribute :datetime, datetime, optional
|
287
|
+
attribute :american_date, datetime(format: "%m-%d-%Y %I:%M:%S %p"), optional
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
it "coerces a string into a DateTime object with ISO 8601 format by default" do
|
292
|
+
attributes = { "datetime" => "1988-05-18T21:00:00Z" }
|
293
|
+
|
294
|
+
expected = { "datetime" => DateTime.new(1988, 5, 18, 21, 00, 00) }
|
295
|
+
|
296
|
+
assert_equal expected, @coercion.call(attributes)
|
297
|
+
end
|
298
|
+
|
299
|
+
it "honors the timezone" do
|
300
|
+
attributes = { "datetime" => "1988-05-18T21:00:00-0300" }
|
301
|
+
|
302
|
+
expected = { "datetime" => DateTime.new(1988, 5, 18, 21, 00, 00, "-03:00") }
|
303
|
+
|
304
|
+
assert_equal expected, @coercion.call(attributes)
|
305
|
+
end
|
306
|
+
|
307
|
+
it "supports a custom date format" do
|
308
|
+
attributes = { "american_date" => "05-18-1988 09:00:00 PM" }
|
309
|
+
|
310
|
+
expected = { "american_date" => DateTime.new(1988, 5, 18, 21, 00, 00) }
|
311
|
+
|
312
|
+
assert_equal expected, @coercion.call(attributes)
|
313
|
+
end
|
314
|
+
|
315
|
+
it "errors if the input doesn't parse" do
|
316
|
+
attributes = { "datetime" => "12-31-1990T21:00:00Z" }
|
317
|
+
|
318
|
+
expected_errors = { "datetime" => "not_valid" }
|
319
|
+
|
320
|
+
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
202
324
|
describe "array" do
|
203
325
|
before do
|
204
326
|
@coercion = Module.new do
|
data/test/uri.rb
CHANGED
@@ -11,7 +11,7 @@ describe "Coercive::URI" do
|
|
11
11
|
assert_equal errors, e.errors
|
12
12
|
end
|
13
13
|
|
14
|
-
|
14
|
+
before do
|
15
15
|
@coercion = Module.new do
|
16
16
|
extend Coercive
|
17
17
|
|
@@ -42,7 +42,7 @@ describe "Coercive::URI" do
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
-
|
45
|
+
it "coerces a valid string to a URI" do
|
46
46
|
attributes = {
|
47
47
|
"any" => "http://user:pass@www.example.com:1234/path"
|
48
48
|
}
|
@@ -50,7 +50,7 @@ describe "Coercive::URI" do
|
|
50
50
|
assert_equal attributes, @coercion.call(attributes)
|
51
51
|
end
|
52
52
|
|
53
|
-
|
53
|
+
it "errors if input is an invalid URI" do
|
54
54
|
attributes = { "any" => "%" }
|
55
55
|
|
56
56
|
expected_errors = { "any" => "not_valid" }
|
@@ -58,7 +58,7 @@ describe "Coercive::URI" do
|
|
58
58
|
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
59
59
|
end
|
60
60
|
|
61
|
-
|
61
|
+
it "errors if the input is longer than the declared maximum size" do
|
62
62
|
attributes = {
|
63
63
|
"min" => "http://foo.com",
|
64
64
|
"max" => "http://long.url.com",
|
@@ -70,7 +70,7 @@ describe "Coercive::URI" do
|
|
70
70
|
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
71
71
|
end
|
72
72
|
|
73
|
-
|
73
|
+
it "errors if the input is shorter than the declared minimum size" do
|
74
74
|
attributes = {
|
75
75
|
"min" => "http://a.com",
|
76
76
|
"max" => "http://bar.com",
|
@@ -82,14 +82,14 @@ describe "Coercive::URI" do
|
|
82
82
|
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
83
83
|
end
|
84
84
|
|
85
|
-
|
85
|
+
it "errors if the URI is an empty string" do
|
86
86
|
attributes = { "schema" => "" }
|
87
87
|
expected_errors = { "schema" => "is_empty" }
|
88
88
|
|
89
89
|
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
90
90
|
end
|
91
91
|
|
92
|
-
|
92
|
+
it "errors if no host" do
|
93
93
|
attributes = { "any" => "http://" }
|
94
94
|
|
95
95
|
expected_errors = { "any" => "no_host" }
|
@@ -97,7 +97,7 @@ describe "Coercive::URI" do
|
|
97
97
|
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
98
98
|
end
|
99
99
|
|
100
|
-
|
100
|
+
it "errors if schema is not supported" do
|
101
101
|
attributes = { "schema" => "foo://example.com" }
|
102
102
|
|
103
103
|
expected_errors = { "schema" => "unsupported_schema" }
|
@@ -105,7 +105,7 @@ describe "Coercive::URI" do
|
|
105
105
|
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
106
106
|
end
|
107
107
|
|
108
|
-
|
108
|
+
it "errors if required elements are not provided" do
|
109
109
|
attributes = {
|
110
110
|
"require_path" => "foo://example.com",
|
111
111
|
"require_port" => "foo://example.com",
|
@@ -123,67 +123,26 @@ describe "Coercive::URI" do
|
|
123
123
|
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
124
124
|
end
|
125
125
|
|
126
|
-
|
127
|
-
range = range.to_range
|
128
|
-
first = range.first
|
129
|
-
last = range.last
|
130
|
-
first = first.ipv6? ? "[#{first}]" : first.to_s
|
131
|
-
last = last.ipv6? ? "[#{last}]" : last.to_s
|
132
|
-
|
133
|
-
test "errors when the URI host is an IP in the range #{first}..#{last}" do
|
134
|
-
attributes_first = { "schema" => "http://#{first}/path" }
|
135
|
-
attributes_last = { "schema" => "http://#{last}/path" }
|
136
|
-
expected_errors = { "schema" => "not_resolvable" }
|
137
|
-
|
138
|
-
assert_coercion_error(expected_errors) { @coercion.call(attributes_first) }
|
139
|
-
assert_coercion_error(expected_errors) { @coercion.call(attributes_last) }
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
test "errors when the URI host is not resolvable" do
|
144
|
-
attributes = {
|
145
|
-
"schema" => "http://bogus-host-that-cant-possibly-exist-here/path"
|
146
|
-
}
|
147
|
-
|
148
|
-
expected_errors = { "schema" => "not_resolvable" }
|
149
|
-
|
150
|
-
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
151
|
-
end
|
152
|
-
|
153
|
-
test "errors when the URI host resolves to an IP in a private range" do
|
154
|
-
attributes = { "schema" => "http://localhost/path" }
|
155
|
-
|
156
|
-
expected_errors = { "schema" => "not_resolvable" }
|
157
|
-
|
158
|
-
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
159
|
-
end
|
160
|
-
|
161
|
-
test "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
|
162
127
|
attributes = { "schema" => "http://8.8.8.8/path" }
|
163
128
|
|
164
129
|
assert_equal attributes, @coercion.call(attributes)
|
165
130
|
end
|
166
131
|
|
167
|
-
|
168
|
-
attributes = { "schema" => "http://www.example.com/path" }
|
169
|
-
|
170
|
-
assert_equal attributes, @coercion.call(attributes)
|
171
|
-
end
|
172
|
-
|
173
|
-
test "allows a URI with no explicit path component" do
|
132
|
+
it "allows a URI with no explicit path component" do
|
174
133
|
attributes = { "schema" => "http://www.example.com" }
|
175
134
|
|
176
135
|
assert_equal attributes, @coercion.call(attributes)
|
177
136
|
end
|
178
137
|
|
179
|
-
|
138
|
+
it "errors for a string that does not pass URI.parse" do
|
180
139
|
attributes = { "schema" => "\\" }
|
181
140
|
expected_errors = { "schema" => "not_valid" }
|
182
141
|
|
183
142
|
assert_coercion_error(expected_errors) { @coercion.call(attributes) }
|
184
143
|
end
|
185
144
|
|
186
|
-
|
145
|
+
it "errors for a URL that passes URI.parse, but is ill-formed" do
|
187
146
|
attributes = { "schema" => "http:example.com/path" }
|
188
147
|
|
189
148
|
begin
|
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.4.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-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:
|
@@ -21,6 +21,7 @@ extra_rdoc_files: []
|
|
21
21
|
files:
|
22
22
|
- LICENSE
|
23
23
|
- README.md
|
24
|
+
- coercive.gemspec
|
24
25
|
- lib/coercive.rb
|
25
26
|
- lib/coercive/uri.rb
|
26
27
|
- test/coercive.rb
|