hash_validator 1.1.1 → 1.2.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: 4074014e1480acf0709f7a6c5c4f444e7a9b740bdb58eb04345fc930e1528f90
4
- data.tar.gz: 196e0be47b751403869cf47a48f946e63bfd949d88fda63f4f04e57327b1541e
3
+ metadata.gz: 30fa62bd65cac6823fc1de4b4039397e3926d9a141d758523db6cec2c6837042
4
+ data.tar.gz: 444275199c9e1a25cc7966ea32776923b2d988d1a881dcfb1f2d7bebea62641c
5
5
  SHA512:
6
- metadata.gz: 6f121a0d98d4c6e9c825290951cec72985e169a5fd9ef8e69fda5216554a1ae17405a397a0c82041677ae96881c24104fcdf04dc76f9ccf91647207766e739ea
7
- data.tar.gz: 67b9c1941c04c075c247496d9b69c509c272eaa2f5b633af74fb94e1c9b135149cf76cd8f6a344ad41424804ff224662f353fd65070cc157517a19d4f70ca049
6
+ metadata.gz: 937942a775e75724c7e04eeecabd15ce1fb796ce25838de469508120e0fc78d7890d3bb3210f4bfa7c2ee74a528cb7a1b501e02b587a5b35a565b510b9d1072b
7
+ data.tar.gz: 1a894031114c6a2a72bc6ca4f03e33190e6466355b053a08b0728a28d89ceda98ceb65c77d948a148bef99bf55cbf8025a554d90a1cc3e691d24417a8c176301
@@ -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/Plan.md ADDED
@@ -0,0 +1,309 @@
1
+ # Hash Validator Enhancement Plan - Living Document
2
+
3
+ ## Document Instructions
4
+ **This is a living plan document.** It should be updated throughout the implementation process to track:
5
+ - Current progress and completion status
6
+ - Any deviations or improvements discovered during implementation
7
+ - Lessons learned that may benefit subsequent phases
8
+ - Test results and quality check outcomes
9
+
10
+ **Update Protocol:**
11
+ 1. Mark tasks as `[x]` when completed
12
+ 2. Add notes in **Implementation Notes** sections for important discoveries
13
+ 3. Update status indicators for each phase
14
+ 4. Document any blockers or changes in approach
15
+
16
+ ## Overall Objective
17
+ Add 11 new validators to the hash_validator gem to expand its validation capabilities for common use cases.
18
+
19
+ ## Phase Structure
20
+ Between each major phase:
21
+ 1. Run full test suite: `rake spec`
22
+ 2. Verify no breaking changes to existing functionality
23
+ 3. Provide summary to user
24
+ 4. Request approval before proceeding to next phase
25
+
26
+ ---
27
+
28
+ ## Phase 1: Simple String Pattern Validators
29
+ **Status:** [ ] Not Started | [ ] In Progress | [x] Completed
30
+ **Validators:** URL, JSON, Hex Color, Alphanumeric, Alpha, Digits
31
+
32
+ ### Tasks:
33
+ - [x] Create URL validator (`lib/hash_validator/validators/url_validator.rb`)
34
+ - Validation: Use `URI.parse` and check for valid scheme
35
+ - Error message: "is not a valid URL"
36
+ - Accept: http, https, ftp schemes
37
+
38
+ - [x] Create JSON validator (`lib/hash_validator/validators/json_validator.rb`)
39
+ - Validation: `JSON.parse(value)` with rescue
40
+ - Error message: "is not valid JSON"
41
+ - Must be string type first
42
+
43
+ - [x] Create Hex Color validator (`lib/hash_validator/validators/hex_color_validator.rb`)
44
+ - Validation: Regex `/\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/`
45
+ - Error message: "is not a valid hex color"
46
+ - Accept: #RGB and #RRGGBB formats
47
+
48
+ - [x] Create Alphanumeric validator (`lib/hash_validator/validators/alphanumeric_validator.rb`)
49
+ - Validation: Regex `/\A[a-zA-Z0-9]+\z/`
50
+ - Error message: "must contain only letters and numbers"
51
+
52
+ - [x] Create Alpha validator (`lib/hash_validator/validators/alpha_validator.rb`)
53
+ - Validation: Regex `/\A[a-zA-Z]+\z/`
54
+ - Error message: "must contain only letters"
55
+
56
+ - [x] Create Digits validator (`lib/hash_validator/validators/digits_validator.rb`)
57
+ - Validation: Regex `/\A\d+\z/`
58
+ - Error message: "must contain only digits"
59
+
60
+ ### Tests:
61
+ - [x] Write specs for each validator in `spec/validators/[validator_name]_spec.rb`
62
+ - [x] Test valid inputs, invalid inputs, nil values, wrong types
63
+ - [x] Run test suite and ensure all pass
64
+
65
+ ### Registration:
66
+ - [x] Add requires to `lib/hash_validator/validators.rb`
67
+ - [x] Ensure validators auto-register via `HashValidator.append_validator`
68
+
69
+ **Implementation Notes:**
70
+ - **Ruby Version Updated:** Changed minimum Ruby requirement from 2.0.0 to 3.0.0 in gemspec
71
+ - **URI Library:** Confirmed URI standard library is available in Ruby 3.0+ and works perfectly for URL validation
72
+ - **JSON Library:** Confirmed JSON standard library is available in Ruby 3.0+ and works perfectly for JSON validation
73
+ - **All Tests Pass:** 74 new tests added (70 validator tests + 4 Rails integration tests), all passing (451 total)
74
+ - **Regex Patterns:** All regex patterns use `\A` and `\z` anchors for exact string matching
75
+ - **Error Messages:** Follow established pattern ("is not a valid [type]" vs "must contain only [constraint]")
76
+ - **Performance:** All validators use simple regex patterns for optimal performance
77
+ - **Rails Integration:** Added ActionController::Parameters support - no more need for `.to_unsafe_h`
78
+ - **README Updated:** Comprehensive table format with validation configs and example payloads, Rails controller example
79
+ - **Documentation:** Added Requirements section, Quick Start, better Examples section structure
80
+
81
+ ---
82
+
83
+ ## Additional Improvements (Beyond Original Plan)
84
+ **Status:** [x] Completed
85
+ **Description:** Enhancements made during Phase 1 implementation
86
+
87
+ ### Rails Integration Enhancement:
88
+ - [x] **ActionController::Parameters Support**: Modified hash validator to automatically detect and handle Rails params
89
+ - [x] **Seamless Conversion**: Internally calls `.to_unsafe_h` when `ActionController::Parameters` detected
90
+ - [x] **Backward Compatibility**: Regular Hash validation continues to work unchanged
91
+ - [x] **Integration Tests**: Added 4 comprehensive tests for Rails params support
92
+
93
+ ### Documentation Improvements:
94
+ - [x] **README Restructuring**: Better flow from Installation → Quick Start → Examples → Usage Table → Advanced Features
95
+ - [x] **Usage Table Enhancement**: Three-column table with Validator, Configuration, and Example Payload
96
+ - [x] **Rails Controller Example**: Practical example showing API parameter validation
97
+ - [x] **Requirements Section**: Added Ruby 3.0+ requirement documentation
98
+ - [x] **Quick Start Guide**: Simple 4-line example to get users started immediately
99
+
100
+ ### Quality Improvements:
101
+ - [x] **Test Coverage**: Increased from 447 to 451 total tests
102
+ - [x] **Error Handling**: Verified all new validators provide clear, consistent error messages
103
+ - [x] **Performance**: All new validators use efficient regex patterns
104
+ - [x] **Integration**: Full compatibility testing with existing codebase
105
+
106
+ **Implementation Notes:**
107
+ - **Rails Support**: Uses `defined?(ActionController::Parameters)` for safe detection without requiring Rails
108
+ - **User Experience**: Rails developers can now use `HashValidator.validate(params, validations)` directly
109
+ - **Maintenance**: No breaking changes introduced, all existing APIs remain unchanged
110
+
111
+ ---
112
+
113
+ ## Phase 2: IP Address Validators
114
+ **Status:** [ ] Not Started | [ ] In Progress | [ ] Completed
115
+ **Validators:** IP (generic), IPv4, IPv6
116
+
117
+ ### Tasks:
118
+ - [ ] Create IPv4 validator (`lib/hash_validator/validators/ipv4_validator.rb`)
119
+ - Validation: Check 4 octets, each 0-255
120
+ - Regex: `/\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\z/`
121
+ - Error message: "is not a valid IPv4 address"
122
+
123
+ - [ ] Create IPv6 validator (`lib/hash_validator/validators/ipv6_validator.rb`)
124
+ - Validation: Standard IPv6 format including compressed notation
125
+ - Consider using `IPAddr` class for validation
126
+ - Error message: "is not a valid IPv6 address"
127
+
128
+ - [ ] Create IP validator (`lib/hash_validator/validators/ip_validator.rb`)
129
+ - Validation: Accept either IPv4 or IPv6
130
+ - Can leverage IPv4 and IPv6 validators
131
+ - Error message: "is not a valid IP address"
132
+
133
+ ### Tests:
134
+ - [ ] Test standard IPv4 addresses (192.168.1.1)
135
+ - [ ] Test IPv6 addresses (standard and compressed)
136
+ - [ ] Test edge cases (0.0.0.0, 255.255.255.255, ::1)
137
+ - [ ] Test invalid formats
138
+
139
+ ### Registration:
140
+ - [ ] Add requires to `lib/hash_validator/validators.rb`
141
+
142
+ **Implementation Notes:**
143
+ <!-- Add discoveries and changes here during implementation -->
144
+
145
+ ---
146
+
147
+ ## Phase 3: Parameterized Validators - Length
148
+ **Status:** [ ] Not Started | [ ] In Progress | [ ] Completed
149
+ **Validators:** Length with min/max/exactly options
150
+
151
+ ### Tasks:
152
+ - [ ] Create Length validation class (`lib/hash_validator/validations/length.rb`)
153
+ - Similar structure to `Optional`, `Many`, `Multiple` classes
154
+ - Store min, max, exactly options
155
+
156
+ - [ ] Create Length validator (`lib/hash_validator/validators/length_validator.rb`)
157
+ - Handle strings, arrays, hashes
158
+ - Support options: `min`, `max`, `exactly`
159
+ - Dynamic error messages based on constraints
160
+
161
+ - [ ] Add factory method to `lib/hash_validator.rb`:
162
+ ```ruby
163
+ def self.length(options)
164
+ Validations::Length.new(options)
165
+ end
166
+ ```
167
+
168
+ ### Usage Examples:
169
+ ```ruby
170
+ HashValidator.length(min: 5, max: 10)
171
+ HashValidator.length(exactly: 8)
172
+ HashValidator.length(min: 5)
173
+ ```
174
+
175
+ ### Tests:
176
+ - [ ] Test all option combinations
177
+ - [ ] Test with strings, arrays, hashes
178
+ - [ ] Test boundary conditions
179
+ - [ ] Test invalid option combinations
180
+
181
+ **Implementation Notes:**
182
+ <!-- Add discoveries and changes here during implementation -->
183
+
184
+ ---
185
+
186
+ ## Phase 4: Parameterized Validators - Numeric Ranges
187
+ **Status:** [ ] Not Started | [ ] In Progress | [ ] Completed
188
+ **Validators:** Between, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual
189
+
190
+ ### Tasks:
191
+ - [ ] Create Range validation classes in `lib/hash_validator/validations/`:
192
+ - `between.rb` - Between two values (inclusive)
193
+ - `greater_than.rb` - Greater than value
194
+ - `less_than.rb` - Less than value
195
+ - `greater_than_or_equal.rb` - Greater than or equal
196
+ - `less_than_or_equal.rb` - Less than or equal
197
+
198
+ - [ ] Create corresponding validators in `lib/hash_validator/validators/`
199
+
200
+ - [ ] Add factory methods to `lib/hash_validator.rb`:
201
+ ```ruby
202
+ def self.between(min, max)
203
+ Validations::Between.new(min, max)
204
+ end
205
+
206
+ def self.greater_than(value)
207
+ Validations::GreaterThan.new(value)
208
+ end
209
+ # ... etc
210
+ ```
211
+
212
+ ### Usage Examples:
213
+ ```ruby
214
+ HashValidator.between(1, 100)
215
+ HashValidator.greater_than(0)
216
+ HashValidator.less_than_or_equal(100)
217
+ ```
218
+
219
+ ### Tests:
220
+ - [ ] Test numeric types (Integer, Float, Rational)
221
+ - [ ] Test boundary values
222
+ - [ ] Test with non-numeric types
223
+ - [ ] Test with nil
224
+
225
+ **Implementation Notes:**
226
+ <!-- Add discoveries and changes here during implementation -->
227
+
228
+ ---
229
+
230
+ ## Phase 5: Documentation and Final Testing
231
+ **Status:** [ ] Not Started | [ ] In Progress | [ ] Completed
232
+
233
+ ### Tasks:
234
+ - [ ] Update README.md with all new validators
235
+ - Add to simple types list
236
+ - Add to "Additional validations" section
237
+ - Provide usage examples for each
238
+
239
+ - [ ] Update CLAUDE.md if needed
240
+
241
+ - [ ] Run full test suite
242
+
243
+ - [ ] Check for any performance impacts
244
+
245
+ - [ ] Verify backward compatibility
246
+
247
+ ### Documentation Sections to Update:
248
+ - [ ] Usage section - add new simple validators
249
+ - [ ] Additional validations - add parameterized validators
250
+ - [ ] Examples section - comprehensive examples
251
+
252
+ **Implementation Notes:**
253
+ <!-- Add discoveries and changes here during implementation -->
254
+
255
+ ---
256
+
257
+ ## Quality Checklist (Run after each phase)
258
+ - [ ] All new tests pass
259
+ - [ ] All existing tests still pass
260
+ - [ ] No rubocop/style violations (if configured)
261
+ - [ ] Documentation is updated
262
+ - [ ] Code follows existing patterns
263
+
264
+ ## Technical Decisions & Rationale
265
+
266
+ ### Pattern Consistency
267
+ All validators follow the existing pattern:
268
+ 1. Inherit from `HashValidator::Validator::Base`
269
+ 2. Initialize with validator name
270
+ 3. Implement `validate` method
271
+ 4. Auto-register using `HashValidator.append_validator`
272
+
273
+ ### Error Message Style
274
+ Consistent with existing validators:
275
+ - Simple validators: "is not a valid [type]"
276
+ - Constraint validators: "must be [constraint]"
277
+ - Type validators: "[type] required"
278
+
279
+ ### Validation Approach
280
+ - **URL**: Use Ruby's built-in `URI.parse` for robustness
281
+ - **IP Addresses**: Regex for IPv4, consider `IPAddr` for IPv6
282
+ - **JSON**: Use `JSON.parse` with exception handling
283
+ - **Patterns**: Simple regex patterns for performance
284
+
285
+ ### File Organization
286
+ - Simple validators: `lib/hash_validator/validators/[name]_validator.rb`
287
+ - Parameterized validations: `lib/hash_validator/validations/[name].rb`
288
+ - Tests mirror structure: `spec/validators/[name]_spec.rb`
289
+
290
+ ## Notes for Future Sessions
291
+ When resuming work on this plan:
292
+ 1. Check this document's status indicators
293
+ 2. Review Implementation Notes for each completed phase
294
+ 3. Run test suite to verify current state
295
+ 4. Continue from the next uncompleted task
296
+
297
+ ## Completion Criteria
298
+ - [x] Phase 1: All 6 string pattern validators implemented and tested
299
+ - [ ] Phase 2: All 3 IP address validators implemented and tested
300
+ - [ ] Phase 3: Length validator implemented and tested
301
+ - [ ] Phase 4: All 5 numeric range validators implemented and tested
302
+ - [x] README fully updated with new validators and Rails integration
303
+ - [x] All tests passing (451/451)
304
+ - [x] Rails ActionController::Parameters support added
305
+ - [x] Ruby 3.0+ minimum version requirement updated
306
+ - [ ] Gem builds successfully
307
+ - [ ] Version bump considered
308
+
309
+ **Progress: Phase 1 Complete (6/11 validators) + Major Rails Integration Enhancement**
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,76 +24,183 @@ Or install it yourself as:
20
24
 
21
25
  $ gem install hash_validator
22
26
 
23
- ## Example
27
+ ## Quick Start
24
28
 
25
29
  ```ruby
26
- # Validations hash
27
- validations = {
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
+ ```
55
+
56
+ ### Failed Validation
57
+ ```ruby
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
- hash = {
38
- foo: 1,
39
- bar: 'baz',
66
+ hash = {
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
+ | `json` | `{ config: 'json' }` | `{ config: '{"theme": "dark"}' }` |
141
+ | `numeric` | `{ score: 'numeric' }` | `{ score: 95.5 }` |
142
+ | `range` | `{ priority: 1..10 }` | `{ priority: 5 }` |
143
+ | `rational` | `{ ratio: 'rational' }` | `{ ratio: Rational(3, 4) }` |
144
+ | `regexp` | `{ code: /^[A-Z]{3}$/ }` | `{ code: 'ABC' }` |
145
+ | `required` | `{ id: 'required' }` | `{ id: 'any_value' }` |
146
+ | `string` | `{ title: 'string' }` | `{ title: 'Hello World' }` |
147
+ | `symbol` | `{ type: 'symbol' }` | `{ type: :user }` |
148
+ | `time` | `{ created_at: 'time' }` | `{ created_at: Time.now }` |
149
+ | `url` | `{ website: 'url' }` | `{ website: 'https://example.com' }` |
150
+
151
+ For hash validation, use nested validations or `{}` to just require a hash to be present.
152
+
153
+ ## Advanced Features
154
+
155
+ ### Class Validation
156
+ On top of the pre-defined validators, classes can be used directly to validate the presence of a value of a specific class:
64
157
 
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.
158
+ ```ruby
159
+ validations = { created_at: Time, user_id: Integer }
160
+ hash = { created_at: Time.now, user_id: 123 }
161
+ HashValidator.validate(hash, validations).valid? # => true
162
+ ```
163
+
164
+ ### Enumerable Validation
165
+ Validate that a value is contained within a supplied enumerable:
166
+
167
+ ```ruby
168
+ validations = { status: ['active', 'inactive', 'pending'] }
169
+ hash = { status: 'active' }
170
+ HashValidator.validate(hash, validations).valid? # => true
171
+ ```
80
172
 
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.
173
+ ### Lambda/Proc Validation
174
+ Use custom logic with lambdas or procs (must accept only one argument):
175
+
176
+ ```ruby
177
+ validations = { age: ->(value) { value.is_a?(Integer) && value >= 18 } }
178
+ hash = { age: 25 }
179
+ HashValidator.validate(hash, validations).valid? # => true
180
+ ```
181
+
182
+ ### Regular Expression Validation
183
+ Validate that a string matches a regex pattern:
184
+
185
+ ```ruby
186
+ validations = { product_code: /^[A-Z]{2}\d{4}$/ }
187
+ hash = { product_code: 'AB1234' }
188
+ HashValidator.validate(hash, validations).valid? # => true
189
+ ```
82
190
 
83
- Additional validations exist to validate beyond simple typing, such as:
191
+ ### Multiple Validators
192
+ Apply multiple validators to a single key:
84
193
 
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).
194
+ ```ruby
195
+ HashValidator.validate(
196
+ { score: 85 },
197
+ { score: HashValidator.multiple('numeric', 1..100) }
198
+ ).valid? # => true
199
+ ```
89
200
 
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).
201
+ This is particularly useful when combining built-in validators with custom validation logic.
91
202
 
92
- ## Custom validations
203
+ ## Custom Validations
93
204
 
94
205
  Allows custom defined validations (must inherit from `HashValidator::Validator::Base`). Example:
95
206
 
@@ -116,19 +227,6 @@ validator.valid? # => true
116
227
  validator.errors # => {}
117
228
  ```
118
229
 
119
- ## Multiple validators
120
-
121
- Multiple validators can be applied to a single key, e.g.
122
-
123
- ```ruby
124
- HashValidator.validate(
125
- { foo: 73 },
126
- { foo: HashValidator.multiple('numeric', 1..100) }
127
- )
128
- ```
129
-
130
- This is particularly useful when defining custom validators.
131
-
132
230
  ## Contributing
133
231
 
134
232
  1. Fork it
@@ -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,23 @@
1
+ class HashValidator::Validator::AlphaValidator < HashValidator::Validator::Base
2
+ def initialize
3
+ super('alpha') # The name of the validator
4
+ end
5
+
6
+ def presence_error_message
7
+ 'must contain only letters'
8
+ end
9
+
10
+ def validate(key, value, _validations, errors)
11
+ unless value.is_a?(String) && valid_alpha?(value)
12
+ errors[key] = presence_error_message
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def valid_alpha?(value)
19
+ /\A[a-zA-Z]+\z/.match?(value)
20
+ end
21
+ end
22
+
23
+ HashValidator.append_validator(HashValidator::Validator::AlphaValidator.new)
@@ -0,0 +1,23 @@
1
+ class HashValidator::Validator::AlphanumericValidator < HashValidator::Validator::Base
2
+ def initialize
3
+ super('alphanumeric') # The name of the validator
4
+ end
5
+
6
+ def presence_error_message
7
+ 'must contain only letters and numbers'
8
+ end
9
+
10
+ def validate(key, value, _validations, errors)
11
+ unless value.is_a?(String) && valid_alphanumeric?(value)
12
+ errors[key] = presence_error_message
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def valid_alphanumeric?(value)
19
+ /\A[a-zA-Z0-9]+\z/.match?(value)
20
+ end
21
+ end
22
+
23
+ HashValidator.append_validator(HashValidator::Validator::AlphanumericValidator.new)
@@ -0,0 +1,23 @@
1
+ class HashValidator::Validator::DigitsValidator < HashValidator::Validator::Base
2
+ def initialize
3
+ super('digits') # The name of the validator
4
+ end
5
+
6
+ def presence_error_message
7
+ 'must contain only digits'
8
+ end
9
+
10
+ def validate(key, value, _validations, errors)
11
+ unless value.is_a?(String) && valid_digits?(value)
12
+ errors[key] = presence_error_message
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def valid_digits?(value)
19
+ /\A\d+\z/.match?(value)
20
+ end
21
+ end
22
+
23
+ HashValidator.append_validator(HashValidator::Validator::DigitsValidator.new)