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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -1
  3. data/.ruby-version +1 -1
  4. data/README.md +315 -56
  5. data/hash_validator.gemspec +1 -1
  6. data/lib/hash_validator/configuration.rb +16 -0
  7. data/lib/hash_validator/validators/alpha_validator.rb +15 -0
  8. data/lib/hash_validator/validators/alphanumeric_validator.rb +15 -0
  9. data/lib/hash_validator/validators/array_validator.rb +1 -1
  10. data/lib/hash_validator/validators/base.rb +28 -3
  11. data/lib/hash_validator/validators/boolean_validator.rb +3 -5
  12. data/lib/hash_validator/validators/class_validator.rb +1 -1
  13. data/lib/hash_validator/validators/digits_validator.rb +15 -0
  14. data/lib/hash_validator/validators/dynamic_func_validator.rb +26 -0
  15. data/lib/hash_validator/validators/dynamic_pattern_validator.rb +23 -0
  16. data/lib/hash_validator/validators/email_validator.rb +4 -6
  17. data/lib/hash_validator/validators/enumerable_validator.rb +4 -6
  18. data/lib/hash_validator/validators/hash_validator.rb +8 -3
  19. data/lib/hash_validator/validators/hex_color_validator.rb +15 -0
  20. data/lib/hash_validator/validators/ip_validator.rb +22 -0
  21. data/lib/hash_validator/validators/ipv4_validator.rb +18 -0
  22. data/lib/hash_validator/validators/ipv6_validator.rb +22 -0
  23. data/lib/hash_validator/validators/json_validator.rb +21 -0
  24. data/lib/hash_validator/validators/lambda_validator.rb +5 -8
  25. data/lib/hash_validator/validators/many_validator.rb +3 -3
  26. data/lib/hash_validator/validators/multiple_validator.rb +1 -1
  27. data/lib/hash_validator/validators/optional_validator.rb +1 -1
  28. data/lib/hash_validator/validators/presence_validator.rb +4 -6
  29. data/lib/hash_validator/validators/regex_validator.rb +4 -6
  30. data/lib/hash_validator/validators/simple_type_validators.rb +1 -1
  31. data/lib/hash_validator/validators/simple_validator.rb +2 -4
  32. data/lib/hash_validator/validators/url_validator.rb +21 -0
  33. data/lib/hash_validator/validators.rb +46 -4
  34. data/lib/hash_validator/version.rb +1 -1
  35. data/lib/hash_validator.rb +1 -0
  36. data/spec/configuration_spec.rb +189 -0
  37. data/spec/hash_validator_spec.rb +4 -4
  38. data/spec/validators/alpha_validator_spec.rb +93 -0
  39. data/spec/validators/alphanumeric_validator_spec.rb +99 -0
  40. data/spec/validators/base_spec.rb +2 -2
  41. data/spec/validators/digits_validator_spec.rb +99 -0
  42. data/spec/validators/dynamic_func_validator_spec.rb +252 -0
  43. data/spec/validators/dynamic_pattern_validator_spec.rb +150 -0
  44. data/spec/validators/hash_validator_spec.rb +102 -0
  45. data/spec/validators/hex_color_validator_spec.rb +111 -0
  46. data/spec/validators/ip_validator_spec.rb +105 -0
  47. data/spec/validators/ipv4_validator_spec.rb +99 -0
  48. data/spec/validators/ipv6_validator_spec.rb +99 -0
  49. data/spec/validators/json_validator_spec.rb +88 -0
  50. data/spec/validators/url_validator_spec.rb +75 -0
  51. data/spec/validators/user_defined_spec.rb +2 -2
  52. metadata +42 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4074014e1480acf0709f7a6c5c4f444e7a9b740bdb58eb04345fc930e1528f90
4
- data.tar.gz: 196e0be47b751403869cf47a48f946e63bfd949d88fda63f4f04e57327b1541e
3
+ metadata.gz: 01246137fa6612aec48d3ebabfb97fa856482f73d0827c520765ad636690cc85
4
+ data.tar.gz: 4e20f8ad39a7a5f1b4f978d5f832ae30a6896687dc3373d7885c41e637efbad4
5
5
  SHA512:
6
- metadata.gz: 6f121a0d98d4c6e9c825290951cec72985e169a5fd9ef8e69fda5216554a1ae17405a397a0c82041677ae96881c24104fcdf04dc76f9ccf91647207766e739ea
7
- data.tar.gz: 67b9c1941c04c075c247496d9b69c509c272eaa2f5b633af74fb94e1c9b135149cf76cd8f6a344ad41424804ff224662f353fd65070cc157517a19d4f70ca049
6
+ metadata.gz: a6503553a0ec7c997006ac24dc183ddfbda551c2a94daaefff08d40733fbf474f2e99d181feccde06faa2312b9bb0d6d7c5eaac810a5d776dd928b8f21946253
7
+ data.tar.gz: 7710ce2712a4e67f0e164bd8ca648c4233ecab28e73542fe93b9dbc52cda2a77c1ebb5eee2e22608ca52a398cba0c9c21c99ec744dc155c512a08f751d404d31
@@ -16,7 +16,7 @@ jobs:
16
16
  strategy:
17
17
  fail-fast: false
18
18
  matrix:
19
- ruby: ["2.3", "2.4", "2.5", "2.6", "2.7", "3.0", "3.1", "3.2", "3.3"]
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
- 2.7.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
- ## Example
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: String,
30
- last_name: 'string',
31
- age: 'numeric',
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
- last_name: 12345
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
- validator.valid?
49
- # => false
50
-
51
- validator.errors
52
- # {
53
- :user => {
54
- :last_name => "string required",
55
- :age => "numeric required",
56
- :likes => "array required"
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 has can be nested as deeply as required using the following values to validate specific value types:
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
- * `array`
66
- * `boolean`
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
- On top of the pre-defined simple types, classes can be used directly (e.g. String) to validate the presence of a value of a desired class.
197
+ ```ruby
198
+ HashValidator.validate(
199
+ { score: 85 },
200
+ { score: HashValidator.multiple('numeric', 1..100) }
201
+ ).valid? # => true
202
+ ```
82
203
 
83
- Additional validations exist to validate beyond simple typing, such as:
204
+ This is particularly useful when combining built-in validators with custom validation logic.
84
205
 
85
- * An Enumerable instance: validates that the value is contained within the supplied enumerable.
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
- Example use-cases include Ruby APIs (I'm currently using it in a Rails API that I'm building for better error responses to developers).
208
+ Allows custom defined validations (must inherit from `HashValidator::Validator::Base`).
91
209
 
92
- ## Custom validations
210
+ ### Simple Example (using `valid?`)
93
211
 
94
- Allows custom defined validations (must inherit from `HashValidator::Validator::Base`). Example:
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 validate(key, value, validations, errors)
104
- unless value.is_a?(Integer) && value.odd?
105
- errors[key] = presence_error_message
106
- end
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.append_validator(HashValidator::Validator::OddValidator.new)
231
+ HashValidator.add_validator(HashValidator::Validator::OddValidator.new)
112
232
 
113
- # Now the validator can be used! e.g.
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
- ## Multiple validators
243
+ ### Complex Example (using `validate`)
120
244
 
121
- Multiple validators can be applied to a single key, e.g.
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
- HashValidator.validate(
125
- { foo: 73 },
126
- { foo: HashValidator.multiple('numeric', 1..100) }
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
- This is particularly useful when defining custom validators.
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
 
@@ -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 = ">= 2.0.0"
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)
@@ -77,4 +77,4 @@ class HashValidator::Validator::ArrayValidator < HashValidator::Validator::Base
77
77
  end
78
78
  end
79
79
 
80
- HashValidator.append_validator(HashValidator::Validator::ArrayValidator.new)
80
+ HashValidator.add_validator(HashValidator::Validator::ArrayValidator.new)
@@ -14,11 +14,36 @@ class HashValidator::Validator::Base
14
14
  self.name == name.to_s
15
15
  end
16
16
 
17
- def presence_error_message
17
+ def error_message
18
18
  "#{self.name} required"
19
19
  end
20
20
 
21
- def validate(*)
22
- raise StandardError.new('validate should not be called directly on BaseValidator')
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 validate(key, value, _validations, errors)
7
- unless [TrueClass, FalseClass].include?(value.class)
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.append_validator(HashValidator::Validator::BooleanValidator.new)
11
+ HashValidator.add_validator(HashValidator::Validator::BooleanValidator.new)
@@ -14,4 +14,4 @@ class HashValidator::Validator::ClassValidator < HashValidator::Validator::Base
14
14
  end
15
15
  end
16
16
 
17
- HashValidator.append_validator(HashValidator::Validator::ClassValidator.new)
17
+ HashValidator.add_validator(HashValidator::Validator::ClassValidator.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 presence_error_message
6
+ def error_message
7
7
  'is not a valid email'
8
8
  end
9
9
 
10
- def validate(key, value, _validations, errors)
11
- unless value.is_a?(String) && value.include?("@")
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.append_validator(HashValidator::Validator::EmailValidator.new)
15
+ HashValidator.add_validator(HashValidator::Validator::EmailValidator.new)