hash_validator 1.1.1 → 2.0.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/.github/workflows/ruby.yml +1 -1
- data/.ruby-version +1 -1
- data/README.md +315 -56
- data/hash_validator.gemspec +1 -1
- data/lib/hash_validator/configuration.rb +16 -0
- data/lib/hash_validator/validators/alpha_validator.rb +15 -0
- data/lib/hash_validator/validators/alphanumeric_validator.rb +15 -0
- data/lib/hash_validator/validators/array_validator.rb +1 -1
- data/lib/hash_validator/validators/base.rb +28 -3
- data/lib/hash_validator/validators/boolean_validator.rb +3 -5
- data/lib/hash_validator/validators/class_validator.rb +1 -1
- data/lib/hash_validator/validators/digits_validator.rb +15 -0
- data/lib/hash_validator/validators/dynamic_func_validator.rb +26 -0
- data/lib/hash_validator/validators/dynamic_pattern_validator.rb +23 -0
- data/lib/hash_validator/validators/email_validator.rb +4 -6
- data/lib/hash_validator/validators/enumerable_validator.rb +4 -6
- data/lib/hash_validator/validators/hash_validator.rb +8 -3
- data/lib/hash_validator/validators/hex_color_validator.rb +15 -0
- data/lib/hash_validator/validators/ip_validator.rb +22 -0
- data/lib/hash_validator/validators/ipv4_validator.rb +18 -0
- data/lib/hash_validator/validators/ipv6_validator.rb +22 -0
- data/lib/hash_validator/validators/json_validator.rb +21 -0
- data/lib/hash_validator/validators/lambda_validator.rb +5 -8
- data/lib/hash_validator/validators/many_validator.rb +3 -3
- data/lib/hash_validator/validators/multiple_validator.rb +1 -1
- data/lib/hash_validator/validators/optional_validator.rb +1 -1
- data/lib/hash_validator/validators/presence_validator.rb +4 -6
- data/lib/hash_validator/validators/regex_validator.rb +4 -6
- data/lib/hash_validator/validators/simple_type_validators.rb +1 -1
- data/lib/hash_validator/validators/simple_validator.rb +2 -4
- data/lib/hash_validator/validators/url_validator.rb +21 -0
- data/lib/hash_validator/validators.rb +46 -4
- data/lib/hash_validator/version.rb +1 -1
- data/lib/hash_validator.rb +1 -0
- data/spec/configuration_spec.rb +189 -0
- data/spec/hash_validator_spec.rb +4 -4
- data/spec/validators/alpha_validator_spec.rb +93 -0
- data/spec/validators/alphanumeric_validator_spec.rb +99 -0
- data/spec/validators/base_spec.rb +2 -2
- data/spec/validators/digits_validator_spec.rb +99 -0
- data/spec/validators/dynamic_func_validator_spec.rb +252 -0
- data/spec/validators/dynamic_pattern_validator_spec.rb +150 -0
- data/spec/validators/hash_validator_spec.rb +102 -0
- data/spec/validators/hex_color_validator_spec.rb +111 -0
- data/spec/validators/ip_validator_spec.rb +105 -0
- data/spec/validators/ipv4_validator_spec.rb +99 -0
- data/spec/validators/ipv6_validator_spec.rb +99 -0
- data/spec/validators/json_validator_spec.rb +88 -0
- data/spec/validators/url_validator_spec.rb +75 -0
- data/spec/validators/user_defined_spec.rb +2 -2
- metadata +42 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01246137fa6612aec48d3ebabfb97fa856482f73d0827c520765ad636690cc85
|
4
|
+
data.tar.gz: 4e20f8ad39a7a5f1b4f978d5f832ae30a6896687dc3373d7885c41e637efbad4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6503553a0ec7c997006ac24dc183ddfbda551c2a94daaefff08d40733fbf474f2e99d181feccde06faa2312b9bb0d6d7c5eaac810a5d776dd928b8f21946253
|
7
|
+
data.tar.gz: 7710ce2712a4e67f0e164bd8ca648c4233ecab28e73542fe93b9dbc52cda2a77c1ebb5eee2e22608ca52a398cba0c9c21c99ec744dc155c512a08f751d404d31
|
data/.github/workflows/ruby.yml
CHANGED
@@ -16,7 +16,7 @@ jobs:
|
|
16
16
|
strategy:
|
17
17
|
fail-fast: false
|
18
18
|
matrix:
|
19
|
-
ruby: ["
|
19
|
+
ruby: ["3.0", "3.1", "3.2", "3.3", "3.4"]
|
20
20
|
steps:
|
21
21
|
- run: sudo apt-get install libcurl4-openssl-dev
|
22
22
|
- uses: actions/checkout@v4
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.0.7
|
data/README.md
CHANGED
@@ -6,6 +6,10 @@
|
|
6
6
|
|
7
7
|
Ruby library to validate hashes (Hash) against user-defined requirements
|
8
8
|
|
9
|
+
## Requirements
|
10
|
+
|
11
|
+
* Ruby 3.0+
|
12
|
+
|
9
13
|
## Installation
|
10
14
|
|
11
15
|
Add this line to your application's Gemfile:
|
@@ -20,78 +24,192 @@ Or install it yourself as:
|
|
20
24
|
|
21
25
|
$ gem install hash_validator
|
22
26
|
|
23
|
-
##
|
27
|
+
## Quick Start
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'hash_validator'
|
31
|
+
|
32
|
+
# Define validation rules
|
33
|
+
validations = { name: 'string', age: 'integer', email: 'email' }
|
34
|
+
|
35
|
+
# Validate a hash
|
36
|
+
validator = HashValidator.validate(
|
37
|
+
{ name: 'John', age: 30, email: 'john@example.com' },
|
38
|
+
validations
|
39
|
+
)
|
40
|
+
|
41
|
+
validator.valid? # => true
|
42
|
+
```
|
43
|
+
|
44
|
+
## Examples
|
45
|
+
|
46
|
+
### Successful Validation
|
47
|
+
```ruby
|
48
|
+
validations = { name: 'string', active: 'boolean', tags: 'array' }
|
49
|
+
hash = { name: 'Product', active: true, tags: ['new', 'featured'] }
|
50
|
+
|
51
|
+
validator = HashValidator.validate(hash, validations)
|
52
|
+
validator.valid? # => true
|
53
|
+
validator.errors # => {}
|
54
|
+
```
|
24
55
|
|
56
|
+
### Failed Validation
|
25
57
|
```ruby
|
26
|
-
# Validations hash
|
27
58
|
validations = {
|
28
59
|
user: {
|
29
|
-
first_name:
|
30
|
-
|
31
|
-
|
32
|
-
likes: 'array'
|
60
|
+
first_name: 'string',
|
61
|
+
age: 'integer',
|
62
|
+
email: 'email'
|
33
63
|
}
|
34
64
|
}
|
35
65
|
|
36
|
-
# Hash to validate
|
37
66
|
hash = {
|
38
|
-
foo: 1,
|
39
|
-
bar: 'baz',
|
40
67
|
user: {
|
41
68
|
first_name: 'James',
|
42
|
-
|
69
|
+
age: 'thirty', # Should be integer
|
70
|
+
# email missing
|
43
71
|
}
|
44
72
|
}
|
45
73
|
|
46
74
|
validator = HashValidator.validate(hash, validations)
|
75
|
+
validator.valid? # => false
|
76
|
+
validator.errors # => { user: { age: "integer required", email: "email required" } }
|
77
|
+
```
|
47
78
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
:
|
54
|
-
|
55
|
-
|
56
|
-
|
79
|
+
### Rails Controller Example
|
80
|
+
```ruby
|
81
|
+
class UsersController < ApplicationController
|
82
|
+
def create
|
83
|
+
validations = {
|
84
|
+
user: {
|
85
|
+
name: 'string',
|
86
|
+
email: 'email',
|
87
|
+
age: 'integer',
|
88
|
+
website: 'url',
|
89
|
+
preferences: {
|
90
|
+
theme: ['light', 'dark'],
|
91
|
+
notifications: 'boolean'
|
92
|
+
}
|
57
93
|
}
|
58
94
|
}
|
95
|
+
|
96
|
+
validator = HashValidator.validate(params, validations)
|
97
|
+
|
98
|
+
if validator.valid?
|
99
|
+
user = User.create(params[:user])
|
100
|
+
render json: { user: user }, status: :created
|
101
|
+
else
|
102
|
+
render json: { errors: validator.errors }, status: :unprocessable_entity
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Example request that would pass validation:
|
108
|
+
# POST /users
|
109
|
+
# {
|
110
|
+
# "user": {
|
111
|
+
# "name": "John Doe",
|
112
|
+
# "email": "john@example.com",
|
113
|
+
# "age": 30,
|
114
|
+
# "website": "https://johndoe.com",
|
115
|
+
# "preferences": {
|
116
|
+
# "theme": "dark",
|
117
|
+
# "notifications": true
|
118
|
+
# }
|
119
|
+
# }
|
120
|
+
# }
|
59
121
|
```
|
60
122
|
|
61
123
|
## Usage
|
62
124
|
|
63
|
-
Define a validation hash which will be used to validate. This
|
125
|
+
Define a validation hash which will be used to validate. This hash can be nested as deeply as required using the following validators:
|
126
|
+
|
127
|
+
| Validator | Validation Configuration | Example Valid Payload |
|
128
|
+
|-----------|-------------------------|----------------------|
|
129
|
+
| `alpha` | `{ name: 'alpha' }` | `{ name: 'James' }` |
|
130
|
+
| `alphanumeric` | `{ username: 'alphanumeric' }` | `{ username: 'user123' }` |
|
131
|
+
| `array` | `{ tags: 'array' }` | `{ tags: ['ruby', 'rails'] }` |
|
132
|
+
| `boolean` | `{ active: 'boolean' }` | `{ active: true }` |
|
133
|
+
| `complex` | `{ number: 'complex' }` | `{ number: Complex(1, 2) }` |
|
134
|
+
| `digits` | `{ zip_code: 'digits' }` | `{ zip_code: '12345' }` |
|
135
|
+
| `email` | `{ contact: 'email' }` | `{ contact: 'user@example.com' }` |
|
136
|
+
| `enumerable` | `{ status: ['active', 'inactive'] }` | `{ status: 'active' }` |
|
137
|
+
| `float` | `{ price: 'float' }` | `{ price: 19.99 }` |
|
138
|
+
| `hex_color` | `{ color: 'hex_color' }` | `{ color: '#ff0000' }` |
|
139
|
+
| `integer` | `{ age: 'integer' }` | `{ age: 25 }` |
|
140
|
+
| `ip` | `{ address: 'ip' }` | `{ address: '192.168.1.1' }` |
|
141
|
+
| `ipv4` | `{ address: 'ipv4' }` | `{ address: '10.0.0.1' }` |
|
142
|
+
| `ipv6` | `{ address: 'ipv6' }` | `{ address: '2001:db8::1' }` |
|
143
|
+
| `json` | `{ config: 'json' }` | `{ config: '{"theme": "dark"}' }` |
|
144
|
+
| `numeric` | `{ score: 'numeric' }` | `{ score: 95.5 }` |
|
145
|
+
| `range` | `{ priority: 1..10 }` | `{ priority: 5 }` |
|
146
|
+
| `rational` | `{ ratio: 'rational' }` | `{ ratio: Rational(3, 4) }` |
|
147
|
+
| `regexp` | `{ code: /^[A-Z]{3}$/ }` | `{ code: 'ABC' }` |
|
148
|
+
| `required` | `{ id: 'required' }` | `{ id: 'any_value' }` |
|
149
|
+
| `string` | `{ title: 'string' }` | `{ title: 'Hello World' }` |
|
150
|
+
| `symbol` | `{ type: 'symbol' }` | `{ type: :user }` |
|
151
|
+
| `time` | `{ created_at: 'time' }` | `{ created_at: Time.now }` |
|
152
|
+
| `url` | `{ website: 'url' }` | `{ website: 'https://example.com' }` |
|
153
|
+
|
154
|
+
For hash validation, use nested validations or `{}` to just require a hash to be present.
|
155
|
+
|
156
|
+
## Advanced Features
|
157
|
+
|
158
|
+
### Class Validation
|
159
|
+
On top of the pre-defined validators, classes can be used directly to validate the presence of a value of a specific class:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
validations = { created_at: Time, user_id: Integer }
|
163
|
+
hash = { created_at: Time.now, user_id: 123 }
|
164
|
+
HashValidator.validate(hash, validations).valid? # => true
|
165
|
+
```
|
166
|
+
|
167
|
+
### Enumerable Validation
|
168
|
+
Validate that a value is contained within a supplied enumerable:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
validations = { status: ['active', 'inactive', 'pending'] }
|
172
|
+
hash = { status: 'active' }
|
173
|
+
HashValidator.validate(hash, validations).valid? # => true
|
174
|
+
```
|
175
|
+
|
176
|
+
### Lambda/Proc Validation
|
177
|
+
Use custom logic with lambdas or procs (must accept only one argument):
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
validations = { age: ->(value) { value.is_a?(Integer) && value >= 18 } }
|
181
|
+
hash = { age: 25 }
|
182
|
+
HashValidator.validate(hash, validations).valid? # => true
|
183
|
+
```
|
184
|
+
|
185
|
+
### Regular Expression Validation
|
186
|
+
Validate that a string matches a regex pattern:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
validations = { product_code: /^[A-Z]{2}\d{4}$/ }
|
190
|
+
hash = { product_code: 'AB1234' }
|
191
|
+
HashValidator.validate(hash, validations).valid? # => true
|
192
|
+
```
|
64
193
|
|
65
|
-
|
66
|
-
|
67
|
-
* `complex`
|
68
|
-
* `enumerable`
|
69
|
-
* `float`
|
70
|
-
* `integer`
|
71
|
-
* `numeric`
|
72
|
-
* `range`
|
73
|
-
* `rational`
|
74
|
-
* `regexp`
|
75
|
-
* `string`
|
76
|
-
* `symbol`
|
77
|
-
* `time`
|
78
|
-
* `required`: just requires any value to be present for the designated key.
|
79
|
-
* hashes are validates by nesting validations, or if just the presence of a hash is required `{}` can be used.
|
194
|
+
### Multiple Validators
|
195
|
+
Apply multiple validators to a single key:
|
80
196
|
|
81
|
-
|
197
|
+
```ruby
|
198
|
+
HashValidator.validate(
|
199
|
+
{ score: 85 },
|
200
|
+
{ score: HashValidator.multiple('numeric', 1..100) }
|
201
|
+
).valid? # => true
|
202
|
+
```
|
82
203
|
|
83
|
-
|
204
|
+
This is particularly useful when combining built-in validators with custom validation logic.
|
84
205
|
|
85
|
-
|
86
|
-
* A lambda/Proc instance: validates that the lambda/proc returns true when the value is supplied (lambdas must accept only one argument).
|
87
|
-
* A regexp instance: validates that the regex returns a match when the value is supplied (Regexp#match(value) is not nil).
|
88
|
-
* `email`: email address validation (string + email address).
|
206
|
+
## Custom Validations
|
89
207
|
|
90
|
-
|
208
|
+
Allows custom defined validations (must inherit from `HashValidator::Validator::Base`).
|
91
209
|
|
92
|
-
|
210
|
+
### Simple Example (using `valid?`)
|
93
211
|
|
94
|
-
|
212
|
+
For simple boolean validations, implement the `valid?` method:
|
95
213
|
|
96
214
|
```ruby
|
97
215
|
# Define our custom validator
|
@@ -100,34 +218,175 @@ class HashValidator::Validator::OddValidator < HashValidator::Validator::Base
|
|
100
218
|
super('odd') # The name of the validator
|
101
219
|
end
|
102
220
|
|
103
|
-
def
|
104
|
-
|
105
|
-
|
106
|
-
|
221
|
+
def error_message
|
222
|
+
'must be an odd number'
|
223
|
+
end
|
224
|
+
|
225
|
+
def valid?(value)
|
226
|
+
value.is_a?(Integer) && value.odd?
|
107
227
|
end
|
108
228
|
end
|
109
229
|
|
110
230
|
# Add the validator
|
111
|
-
HashValidator.
|
231
|
+
HashValidator.add_validator(HashValidator::Validator::OddValidator.new)
|
112
232
|
|
113
|
-
# Now the validator can be used!
|
233
|
+
# Now the validator can be used!
|
114
234
|
validator = HashValidator.validate({ age: 27 }, { age: 'odd' })
|
115
235
|
validator.valid? # => true
|
116
236
|
validator.errors # => {}
|
237
|
+
|
238
|
+
validator = HashValidator.validate({ age: 26 }, { age: 'odd' })
|
239
|
+
validator.valid? # => false
|
240
|
+
validator.errors # => { age: 'must be an odd number' }
|
117
241
|
```
|
118
242
|
|
119
|
-
|
243
|
+
### Complex Example (using `validate`)
|
120
244
|
|
121
|
-
|
245
|
+
For more complex validations that need access to the validation parameters or custom error handling, override the `validate` method:
|
122
246
|
|
123
247
|
```ruby
|
124
|
-
|
125
|
-
|
126
|
-
|
248
|
+
# Define a validator that checks if a number is within a range
|
249
|
+
class HashValidator::Validator::RangeValidator < HashValidator::Validator::Base
|
250
|
+
def initialize
|
251
|
+
super('_range') # Underscore prefix as it's invoked through the validation parameter
|
252
|
+
end
|
253
|
+
|
254
|
+
def should_validate?(validation)
|
255
|
+
validation.is_a?(Range)
|
256
|
+
end
|
257
|
+
|
258
|
+
def error_message
|
259
|
+
'is out of range'
|
260
|
+
end
|
261
|
+
|
262
|
+
def validate(key, value, range, errors)
|
263
|
+
unless range.include?(value)
|
264
|
+
errors[key] = "must be between #{range.min} and #{range.max}"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Add the validator
|
270
|
+
HashValidator.add_validator(HashValidator::Validator::RangeValidator.new)
|
271
|
+
|
272
|
+
# Now the validator can be used with Range objects!
|
273
|
+
validator = HashValidator.validate({ age: 25 }, { age: 18..65 })
|
274
|
+
validator.valid? # => true
|
275
|
+
validator.errors # => {}
|
276
|
+
|
277
|
+
validator = HashValidator.validate({ age: 10 }, { age: 18..65 })
|
278
|
+
validator.valid? # => false
|
279
|
+
validator.errors # => { age: 'must be between 18 and 65' }
|
280
|
+
```
|
281
|
+
|
282
|
+
## Simple Custom Validators
|
283
|
+
|
284
|
+
For simpler use cases, you can define custom validators without creating a full class using pattern matching or custom functions.
|
285
|
+
|
286
|
+
### Configuration DSL
|
287
|
+
|
288
|
+
Use the configuration DSL to define multiple validators at once, similar to a Rails initializer:
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
# In a Rails app, this would go in config/initializers/hash_validator.rb
|
292
|
+
HashValidator.configure do |config|
|
293
|
+
# Add instance-based validators
|
294
|
+
config.add_validator HashValidator::Validator::CustomValidator.new
|
295
|
+
|
296
|
+
# Add pattern-based validators
|
297
|
+
config.add_validator 'phone',
|
298
|
+
pattern: /\A\+?[1-9]\d{1,14}\z/,
|
299
|
+
error_message: 'must be a valid international phone number'
|
300
|
+
|
301
|
+
config.add_validator 'postal_code',
|
302
|
+
pattern: /\A[A-Z0-9]{3,10}\z/i,
|
303
|
+
error_message: 'must be a valid postal code'
|
304
|
+
|
305
|
+
# Add function-based validators
|
306
|
+
config.add_validator 'adult',
|
307
|
+
func: ->(age) { age.is_a?(Integer) && age >= 18 },
|
308
|
+
error_message: 'must be 18 or older'
|
309
|
+
|
310
|
+
config.add_validator 'business_hours',
|
311
|
+
func: ->(hour) { hour.between?(9, 17) },
|
312
|
+
error_message: 'must be between 9 AM and 5 PM'
|
313
|
+
end
|
314
|
+
```
|
315
|
+
|
316
|
+
### Pattern-Based Validators
|
317
|
+
|
318
|
+
Use regular expressions to validate string formats:
|
319
|
+
|
320
|
+
```ruby
|
321
|
+
# Add a validator for odd numbers using a pattern
|
322
|
+
HashValidator.add_validator('odd_string',
|
323
|
+
pattern: /\A\d*[13579]\z/,
|
324
|
+
error_message: 'must be an odd number string')
|
325
|
+
|
326
|
+
# Add a validator for US phone numbers
|
327
|
+
HashValidator.add_validator('us_phone',
|
328
|
+
pattern: /\A\d{3}-\d{3}-\d{4}\z/,
|
329
|
+
error_message: 'must be a valid US phone number (XXX-XXX-XXXX)')
|
330
|
+
|
331
|
+
# Use the validators
|
332
|
+
validator = HashValidator.validate(
|
333
|
+
{ number: '27', phone: '555-123-4567' },
|
334
|
+
{ number: 'odd_string', phone: 'us_phone' }
|
335
|
+
)
|
336
|
+
validator.valid? # => true
|
337
|
+
|
338
|
+
validator = HashValidator.validate(
|
339
|
+
{ number: '26', phone: '5551234567' },
|
340
|
+
{ number: 'odd_string', phone: 'us_phone' }
|
341
|
+
)
|
342
|
+
validator.valid? # => false
|
343
|
+
validator.errors # => { number: 'must be an odd number string', phone: 'must be a valid US phone number (XXX-XXX-XXXX)' }
|
344
|
+
```
|
345
|
+
|
346
|
+
### Function-Based Validators
|
347
|
+
|
348
|
+
Use lambdas or procs for custom validation logic:
|
349
|
+
|
350
|
+
```ruby
|
351
|
+
# Add a validator for adult age using a lambda
|
352
|
+
HashValidator.add_validator('adult_age',
|
353
|
+
func: ->(age) { age.is_a?(Integer) && age >= 18 },
|
354
|
+
error_message: 'must be 18 or older')
|
355
|
+
|
356
|
+
# Add a validator for palindromes using a proc
|
357
|
+
HashValidator.add_validator('palindrome',
|
358
|
+
func: proc { |str| str.to_s == str.to_s.reverse },
|
359
|
+
error_message: 'must be a palindrome')
|
360
|
+
|
361
|
+
# Use the validators
|
362
|
+
validator = HashValidator.validate(
|
363
|
+
{ age: 25, word: 'racecar' },
|
364
|
+
{ age: 'adult_age', word: 'palindrome' }
|
365
|
+
)
|
366
|
+
validator.valid? # => true
|
367
|
+
|
368
|
+
validator = HashValidator.validate(
|
369
|
+
{ age: 16, word: 'hello' },
|
370
|
+
{ age: 'adult_age', word: 'palindrome' }
|
127
371
|
)
|
372
|
+
validator.valid? # => false
|
373
|
+
validator.errors # => { age: 'must be 18 or older', word: 'must be a palindrome' }
|
374
|
+
```
|
375
|
+
|
376
|
+
### Removing Custom Validators
|
377
|
+
|
378
|
+
You can remove custom validators when they're no longer needed:
|
379
|
+
|
380
|
+
```ruby
|
381
|
+
# Remove a specific validator
|
382
|
+
HashValidator.remove_validator('adult_age')
|
128
383
|
```
|
129
384
|
|
130
|
-
|
385
|
+
These simple validators are ideal for:
|
386
|
+
- Quick format validation without regex in your main code
|
387
|
+
- Reusable validation logic across your application
|
388
|
+
- Keeping validation definitions close to your configuration
|
389
|
+
- Avoiding the overhead of creating full validator classes for simple rules
|
131
390
|
|
132
391
|
## Contributing
|
133
392
|
|
data/hash_validator.gemspec
CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.homepage = "https://github.com/JamesBrooks/hash_validator"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
|
-
spec.required_ruby_version = ">=
|
16
|
+
spec.required_ruby_version = ">= 3.0.0"
|
17
17
|
spec.files = `git ls-files`.split($/)
|
18
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module HashValidator
|
2
|
+
class Configuration
|
3
|
+
def add_validator(*args)
|
4
|
+
HashValidator.add_validator(*args)
|
5
|
+
end
|
6
|
+
|
7
|
+
def remove_validator(name)
|
8
|
+
HashValidator.remove_validator(name)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.configure
|
13
|
+
config = Configuration.new
|
14
|
+
yield(config) if block_given?
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class HashValidator::Validator::AlphaValidator < HashValidator::Validator::Base
|
2
|
+
def initialize
|
3
|
+
super('alpha') # The name of the validator
|
4
|
+
end
|
5
|
+
|
6
|
+
def error_message
|
7
|
+
'must contain only letters'
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid?(value)
|
11
|
+
value.is_a?(String) && /\A[a-zA-Z]+\z/.match?(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
HashValidator.add_validator(HashValidator::Validator::AlphaValidator.new)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class HashValidator::Validator::AlphanumericValidator < HashValidator::Validator::Base
|
2
|
+
def initialize
|
3
|
+
super('alphanumeric') # The name of the validator
|
4
|
+
end
|
5
|
+
|
6
|
+
def error_message
|
7
|
+
'must contain only letters and numbers'
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid?(value)
|
11
|
+
value.is_a?(String) && /\A[a-zA-Z0-9]+\z/.match?(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
HashValidator.add_validator(HashValidator::Validator::AlphanumericValidator.new)
|
@@ -14,11 +14,36 @@ class HashValidator::Validator::Base
|
|
14
14
|
self.name == name.to_s
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
17
|
+
def error_message
|
18
18
|
"#{self.name} required"
|
19
19
|
end
|
20
20
|
|
21
|
-
def validate(
|
22
|
-
|
21
|
+
def validate(key, value, validations, errors)
|
22
|
+
# If the subclass implements valid?, use that for simple boolean validation
|
23
|
+
if self.class.instance_methods(false).include?(:valid?)
|
24
|
+
# Check the arity of the valid? method to determine how many arguments to pass
|
25
|
+
valid_result = case method(:valid?).arity
|
26
|
+
when 1
|
27
|
+
valid?(value)
|
28
|
+
when 2
|
29
|
+
valid?(value, validations)
|
30
|
+
else
|
31
|
+
raise StandardError.new("valid? method must accept either 1 argument (value) or 2 arguments (value, validations)")
|
32
|
+
end
|
33
|
+
|
34
|
+
unless valid_result
|
35
|
+
errors[key] = error_message
|
36
|
+
end
|
37
|
+
else
|
38
|
+
# Otherwise, subclass must override validate
|
39
|
+
raise StandardError.new('Validator must implement either valid? or override validate method')
|
40
|
+
end
|
23
41
|
end
|
42
|
+
|
43
|
+
# Subclasses can optionally implement this for simple boolean validation
|
44
|
+
# Return true if valid, false if invalid
|
45
|
+
# Either:
|
46
|
+
# def valid?(value) # For simple validations
|
47
|
+
# def valid?(value, validations) # When validation context is needed
|
48
|
+
# end
|
24
49
|
end
|
@@ -3,11 +3,9 @@ class HashValidator::Validator::BooleanValidator < HashValidator::Validator::Bas
|
|
3
3
|
super('boolean') # The name of the validator
|
4
4
|
end
|
5
5
|
|
6
|
-
def
|
7
|
-
|
8
|
-
errors[key] = presence_error_message
|
9
|
-
end
|
6
|
+
def valid?(value)
|
7
|
+
[TrueClass, FalseClass].include?(value.class)
|
10
8
|
end
|
11
9
|
end
|
12
10
|
|
13
|
-
HashValidator.
|
11
|
+
HashValidator.add_validator(HashValidator::Validator::BooleanValidator.new)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class HashValidator::Validator::DigitsValidator < HashValidator::Validator::Base
|
2
|
+
def initialize
|
3
|
+
super('digits') # The name of the validator
|
4
|
+
end
|
5
|
+
|
6
|
+
def error_message
|
7
|
+
'must contain only digits'
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid?(value)
|
11
|
+
value.is_a?(String) && /\A\d+\z/.match?(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
HashValidator.add_validator(HashValidator::Validator::DigitsValidator.new)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class HashValidator::Validator::DynamicFuncValidator < HashValidator::Validator::Base
|
2
|
+
attr_accessor :func, :custom_error_message
|
3
|
+
|
4
|
+
def initialize(name, func, error_message = nil)
|
5
|
+
super(name)
|
6
|
+
|
7
|
+
unless func.respond_to?(:call)
|
8
|
+
raise ArgumentError, "Function must be callable (proc or lambda)"
|
9
|
+
end
|
10
|
+
|
11
|
+
@func = func
|
12
|
+
@custom_error_message = error_message
|
13
|
+
end
|
14
|
+
|
15
|
+
def error_message
|
16
|
+
@custom_error_message || super
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid?(value)
|
20
|
+
begin
|
21
|
+
!!@func.call(value)
|
22
|
+
rescue => e
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class HashValidator::Validator::DynamicPatternValidator < HashValidator::Validator::Base
|
2
|
+
attr_accessor :pattern, :custom_error_message
|
3
|
+
|
4
|
+
def initialize(name, pattern, error_message = nil)
|
5
|
+
super(name)
|
6
|
+
|
7
|
+
unless pattern.is_a?(Regexp)
|
8
|
+
raise ArgumentError, "Pattern must be a regular expression"
|
9
|
+
end
|
10
|
+
|
11
|
+
@pattern = pattern
|
12
|
+
@custom_error_message = error_message
|
13
|
+
end
|
14
|
+
|
15
|
+
def error_message
|
16
|
+
@custom_error_message || super
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid?(value)
|
20
|
+
return false unless value.respond_to?(:to_s)
|
21
|
+
@pattern.match?(value.to_s)
|
22
|
+
end
|
23
|
+
end
|
@@ -3,15 +3,13 @@ class HashValidator::Validator::EmailValidator < HashValidator::Validator::Base
|
|
3
3
|
super('email') # The name of the validator
|
4
4
|
end
|
5
5
|
|
6
|
-
def
|
6
|
+
def error_message
|
7
7
|
'is not a valid email'
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
12
|
-
errors[key] = presence_error_message
|
13
|
-
end
|
10
|
+
def valid?(value)
|
11
|
+
value.is_a?(String) && value.include?("@")
|
14
12
|
end
|
15
13
|
end
|
16
14
|
|
17
|
-
HashValidator.
|
15
|
+
HashValidator.add_validator(HashValidator::Validator::EmailValidator.new)
|