israeli 0.1.0 → 0.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 +4 -4
- data/CHANGELOG.md +46 -0
- data/README.md +98 -0
- data/lib/israeli/active_model/israeli_bank_account_validator.rb +4 -1
- data/lib/israeli/active_model/israeli_id_validator.rb +3 -1
- data/lib/israeli/active_model/israeli_phone_validator.rb +6 -1
- data/lib/israeli/active_model/israeli_postal_code_validator.rb +3 -1
- data/lib/israeli/errors.rb +25 -4
- data/lib/israeli/result.rb +151 -0
- data/lib/israeli/validators/bank_account.rb +50 -0
- data/lib/israeli/validators/id.rb +24 -1
- data/lib/israeli/validators/phone.rb +45 -0
- data/lib/israeli/validators/postal_code.rb +19 -0
- data/lib/israeli/version.rb +1 -1
- data/lib/israeli.rb +211 -2
- 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: c891a985fc73096b35660dd06031c286ab7bd4bcf4257d1a953ba43d56eb0a8a
|
|
4
|
+
data.tar.gz: 57a1d7fda2b852f9528d802bec9e8676500551cf117ac7349b6b7a221d8d39d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da33684de117b727b10e8ad788a552469c8add53e58cd8d12d50dc7bcd9d82a61ba05430b753345a8949f369cef9c43089a0d053b5cf3a3233a207c8bd7f50c2
|
|
7
|
+
data.tar.gz: 2afcff8efddf051796863de345a99f223db86762d0ae4ceb224d0ba4e538d8e1cf3af9a1fa6d260c120e40aadc232037e7ef47d05c23cade2d2eb54747aeb02b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2025-12-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Bang Methods** - Exception-raising validation methods
|
|
15
|
+
- `Israeli.valid_id!` raises `InvalidIdError` with reason
|
|
16
|
+
- `Israeli.valid_phone!` raises `InvalidPhoneError` with reason
|
|
17
|
+
- `Israeli.valid_postal_code!` raises `InvalidPostalCodeError` with reason
|
|
18
|
+
- `Israeli.valid_bank_account!` raises `InvalidBankAccountError` with reason
|
|
19
|
+
|
|
20
|
+
- **Parse Methods** - Rich result objects for detailed validation
|
|
21
|
+
- `Israeli.parse_id` returns `IdResult` with formatting
|
|
22
|
+
- `Israeli.parse_phone` returns `PhoneResult` with type detection
|
|
23
|
+
- `Israeli.parse_postal_code` returns `PostalCodeResult` with formatting
|
|
24
|
+
- `Israeli.parse_bank_account` returns `BankAccountResult` with format detection
|
|
25
|
+
|
|
26
|
+
- **Phone Type Detection**
|
|
27
|
+
- `Israeli.phone_type` returns `:mobile`, `:landline`, `:voip`, or `nil`
|
|
28
|
+
- `Israeli::Validators::Phone.detect_type` for direct access
|
|
29
|
+
|
|
30
|
+
- **Invalid Reason Methods** - Detailed validation failure reasons
|
|
31
|
+
- `Israeli::Validators::Id.invalid_reason` returns `:blank`, `:wrong_length`, or `:invalid_checksum`
|
|
32
|
+
- `Israeli::Validators::Phone.invalid_reason` returns `:blank`, `:invalid_format`, or `:wrong_type`
|
|
33
|
+
- `Israeli::Validators::PostalCode.invalid_reason` returns `:blank` or `:wrong_length`
|
|
34
|
+
- `Israeli::Validators::BankAccount.invalid_reason` returns `:blank`, `:wrong_length`, `:invalid_format`, or `:invalid_checksum`
|
|
35
|
+
|
|
36
|
+
- **Missing Facade Methods**
|
|
37
|
+
- `Israeli.format_postal_code` with `:compact` and `:spaced` styles
|
|
38
|
+
- `Israeli.format_bank_account` with `:domestic`, `:compact` styles
|
|
39
|
+
|
|
40
|
+
- **Rails errors.details Support**
|
|
41
|
+
- All validators now include `reason` in error details
|
|
42
|
+
- Phone validator includes `expected_type` and `detected_type`
|
|
43
|
+
- Bank account validator includes `expected_format`
|
|
44
|
+
|
|
45
|
+
- **Error Classes Hierarchy**
|
|
46
|
+
- `Israeli::InvalidIdError` for ID validation errors
|
|
47
|
+
- `Israeli::InvalidPhoneError` for phone validation errors
|
|
48
|
+
- `Israeli::InvalidPostalCodeError` for postal code validation errors
|
|
49
|
+
- `Israeli::InvalidBankAccountError` for bank account validation errors
|
|
50
|
+
- All inherit from `Israeli::InvalidFormatError` with `reason` accessor
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- `Id.format()` no longer calls `valid?()` redundantly (minor performance improvement)
|
|
55
|
+
|
|
10
56
|
## [0.1.0] - 2025-12-03
|
|
11
57
|
|
|
12
58
|
### Added
|
data/README.md
CHANGED
|
@@ -178,6 +178,104 @@ bundle exec rubocop # Run linter
|
|
|
178
178
|
bundle exec rake # Run both
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
+
## Advanced Usage
|
|
182
|
+
|
|
183
|
+
### Bang Methods (Exception-raising)
|
|
184
|
+
|
|
185
|
+
For fail-fast validation, use bang methods that raise exceptions:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
Israeli.valid_id!("123456789")
|
|
189
|
+
# => raises Israeli::InvalidIdError with reason: :invalid_checksum
|
|
190
|
+
|
|
191
|
+
Israeli.valid_phone!("021234567", type: :mobile)
|
|
192
|
+
# => raises Israeli::InvalidPhoneError with reason: :wrong_type
|
|
193
|
+
|
|
194
|
+
# Catch specific errors
|
|
195
|
+
begin
|
|
196
|
+
Israeli.valid_id!(user_input)
|
|
197
|
+
rescue Israeli::InvalidIdError => e
|
|
198
|
+
puts "Invalid ID: #{e.reason}" # => :blank, :wrong_length, or :invalid_checksum
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Parse Methods (Rich Result Objects)
|
|
203
|
+
|
|
204
|
+
For more detailed validation information, use parse methods:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
# Phone parsing with type detection
|
|
208
|
+
result = Israeli.parse_phone("0501234567")
|
|
209
|
+
result.valid? # => true
|
|
210
|
+
result.type # => :mobile
|
|
211
|
+
result.mobile? # => true
|
|
212
|
+
result.formatted(style: :dashed) # => "050-123-4567"
|
|
213
|
+
result.formatted(style: :international) # => "+972-501234567"
|
|
214
|
+
|
|
215
|
+
# ID parsing
|
|
216
|
+
result = Israeli.parse_id("123456789")
|
|
217
|
+
result.valid? # => false
|
|
218
|
+
result.reason # => :invalid_checksum
|
|
219
|
+
|
|
220
|
+
# Bank account with format detection
|
|
221
|
+
result = Israeli.parse_bank_account("IL620108000000099999999")
|
|
222
|
+
result.iban? # => true
|
|
223
|
+
result.domestic? # => false
|
|
224
|
+
result.format # => :iban
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Phone Type Detection
|
|
228
|
+
|
|
229
|
+
Detect phone number type without validation:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
Israeli.phone_type("0501234567") # => :mobile
|
|
233
|
+
Israeli.phone_type("021234567") # => :landline
|
|
234
|
+
Israeli.phone_type("0721234567") # => :voip
|
|
235
|
+
Israeli.phone_type("invalid") # => nil
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Invalid Reason Detection
|
|
239
|
+
|
|
240
|
+
Get detailed validation failure reasons:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
Israeli::Validators::Id.invalid_reason("123456789") # => :invalid_checksum
|
|
244
|
+
Israeli::Validators::Id.invalid_reason("") # => :blank
|
|
245
|
+
Israeli::Validators::Phone.invalid_reason("021234567", type: :mobile) # => :wrong_type
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Rails errors.details Support
|
|
249
|
+
|
|
250
|
+
Validators include structured error details for Rails 5+:
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
# Model definition
|
|
254
|
+
class Person < ApplicationRecord
|
|
255
|
+
validates :id_number, israeli_id: true
|
|
256
|
+
validates :mobile_phone, israeli_phone: { type: :mobile }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Usage
|
|
260
|
+
person = Person.new(id_number: "123456789", mobile_phone: "021234567")
|
|
261
|
+
person.valid? # => false
|
|
262
|
+
|
|
263
|
+
person.errors.details[:id_number]
|
|
264
|
+
# => [{error: :invalid, reason: :invalid_checksum}]
|
|
265
|
+
|
|
266
|
+
person.errors.details[:mobile_phone]
|
|
267
|
+
# => [{error: :invalid, reason: :wrong_type, expected_type: :mobile, detected_type: :landline}]
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Roadmap
|
|
271
|
+
|
|
272
|
+
Future versions may include:
|
|
273
|
+
|
|
274
|
+
- [ ] **Business/Company Number (Mispar Osek/Hevra)** - 9-digit with checksum validation
|
|
275
|
+
- [ ] **Vehicle License Plates** - Format validation for Israeli plates
|
|
276
|
+
- [ ] **Non-Profit (Amuta) Numbers** - 580-prefix validation
|
|
277
|
+
- [ ] **Luhn Checksum Generator** - Generate valid IDs for testing
|
|
278
|
+
|
|
181
279
|
## Contributing
|
|
182
280
|
|
|
183
281
|
Bug reports and pull requests are welcome on GitHub at https://github.com/dpaluy/israeli.
|
|
@@ -26,9 +26,12 @@ class IsraeliBankAccountValidator < ActiveModel::EachValidator
|
|
|
26
26
|
account_format = options[:format] || :any
|
|
27
27
|
return if Israeli::Validators::BankAccount.valid?(value, format: account_format)
|
|
28
28
|
|
|
29
|
+
reason = Israeli::Validators::BankAccount.invalid_reason(value, format: account_format)
|
|
29
30
|
record.errors.add(
|
|
30
31
|
attribute,
|
|
31
|
-
options[:message] || :invalid
|
|
32
|
+
options[:message] || :invalid,
|
|
33
|
+
reason: reason,
|
|
34
|
+
expected_format: account_format
|
|
32
35
|
)
|
|
33
36
|
end
|
|
34
37
|
end
|
|
@@ -20,9 +20,11 @@ class IsraeliIdValidator < ActiveModel::EachValidator
|
|
|
20
20
|
|
|
21
21
|
return if Israeli::Validators::Id.valid?(value)
|
|
22
22
|
|
|
23
|
+
reason = Israeli::Validators::Id.invalid_reason(value)
|
|
23
24
|
record.errors.add(
|
|
24
25
|
attribute,
|
|
25
|
-
options[:message] || :invalid
|
|
26
|
+
options[:message] || :invalid,
|
|
27
|
+
reason: reason
|
|
26
28
|
)
|
|
27
29
|
end
|
|
28
30
|
end
|
|
@@ -31,9 +31,14 @@ class IsraeliPhoneValidator < ActiveModel::EachValidator
|
|
|
31
31
|
phone_type = options[:type] || :any
|
|
32
32
|
return if Israeli::Validators::Phone.valid?(value, type: phone_type)
|
|
33
33
|
|
|
34
|
+
reason = Israeli::Validators::Phone.invalid_reason(value, type: phone_type)
|
|
35
|
+
detected_type = Israeli::Validators::Phone.detect_type(value)
|
|
34
36
|
record.errors.add(
|
|
35
37
|
attribute,
|
|
36
|
-
options[:message] || :invalid
|
|
38
|
+
options[:message] || :invalid,
|
|
39
|
+
reason: reason,
|
|
40
|
+
expected_type: phone_type,
|
|
41
|
+
detected_type: detected_type
|
|
37
42
|
)
|
|
38
43
|
end
|
|
39
44
|
end
|
|
@@ -20,9 +20,11 @@ class IsraeliPostalCodeValidator < ActiveModel::EachValidator
|
|
|
20
20
|
|
|
21
21
|
return if Israeli::Validators::PostalCode.valid?(value)
|
|
22
22
|
|
|
23
|
+
reason = Israeli::Validators::PostalCode.invalid_reason(value)
|
|
23
24
|
record.errors.add(
|
|
24
25
|
attribute,
|
|
25
|
-
options[:message] || :invalid
|
|
26
|
+
options[:message] || :invalid,
|
|
27
|
+
reason: reason
|
|
26
28
|
)
|
|
27
29
|
end
|
|
28
30
|
end
|
data/lib/israeli/errors.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Israeli
|
|
|
7
7
|
#
|
|
8
8
|
# @example Handling errors
|
|
9
9
|
# begin
|
|
10
|
-
# Israeli.
|
|
10
|
+
# Israeli.valid_id!("invalid")
|
|
11
11
|
# rescue Israeli::Error => e
|
|
12
12
|
# puts "Validation failed: #{e.message}"
|
|
13
13
|
# end
|
|
@@ -16,7 +16,28 @@ module Israeli
|
|
|
16
16
|
# Raised when input format is invalid for the requested validation type.
|
|
17
17
|
#
|
|
18
18
|
# @example
|
|
19
|
-
# Israeli.
|
|
20
|
-
# # => Israeli::InvalidFormatError:
|
|
21
|
-
class InvalidFormatError < Error
|
|
19
|
+
# Israeli.valid_id!("123456789")
|
|
20
|
+
# # => Israeli::InvalidFormatError: Invalid Israeli ID
|
|
21
|
+
class InvalidFormatError < Error
|
|
22
|
+
attr_reader :reason
|
|
23
|
+
|
|
24
|
+
# @param message [String] Human-readable error message
|
|
25
|
+
# @param reason [Symbol, nil] Machine-readable reason code
|
|
26
|
+
def initialize(message = nil, reason: nil)
|
|
27
|
+
@reason = reason
|
|
28
|
+
super(message)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Raised when an Israeli ID number is invalid.
|
|
33
|
+
class InvalidIdError < InvalidFormatError; end
|
|
34
|
+
|
|
35
|
+
# Raised when an Israeli phone number is invalid.
|
|
36
|
+
class InvalidPhoneError < InvalidFormatError; end
|
|
37
|
+
|
|
38
|
+
# Raised when an Israeli postal code is invalid.
|
|
39
|
+
class InvalidPostalCodeError < InvalidFormatError; end
|
|
40
|
+
|
|
41
|
+
# Raised when an Israeli bank account number is invalid.
|
|
42
|
+
class InvalidBankAccountError < InvalidFormatError; end
|
|
22
43
|
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Israeli
|
|
4
|
+
# Result object returned by parse methods.
|
|
5
|
+
#
|
|
6
|
+
# Provides a rich object with validation status, original/normalized values,
|
|
7
|
+
# and formatting methods for a cleaner API than simple booleans.
|
|
8
|
+
#
|
|
9
|
+
# @example Using a parse result
|
|
10
|
+
# result = Israeli.parse_id("123456782")
|
|
11
|
+
# result.valid? # => true
|
|
12
|
+
# result.formatted # => "123456782"
|
|
13
|
+
# result.original # => "123456782"
|
|
14
|
+
#
|
|
15
|
+
# @example Invalid result
|
|
16
|
+
# result = Israeli.parse_id("123456789")
|
|
17
|
+
# result.valid? # => false
|
|
18
|
+
# result.invalid? # => true
|
|
19
|
+
# result.reason # => :invalid_checksum
|
|
20
|
+
class Result
|
|
21
|
+
attr_reader :original, :normalized, :reason
|
|
22
|
+
|
|
23
|
+
# @param original [String, nil] Original input value
|
|
24
|
+
# @param normalized [String, nil] Normalized/sanitized value
|
|
25
|
+
# @param valid [Boolean] Whether the value is valid
|
|
26
|
+
# @param reason [Symbol, nil] Reason for invalidity
|
|
27
|
+
def initialize(original:, normalized:, valid:, reason: nil)
|
|
28
|
+
@original = original
|
|
29
|
+
@normalized = normalized
|
|
30
|
+
@valid = valid
|
|
31
|
+
@reason = reason
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Boolean] true if the value is valid
|
|
35
|
+
def valid?
|
|
36
|
+
@valid
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Boolean] true if the value is invalid
|
|
40
|
+
def invalid?
|
|
41
|
+
!@valid
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [String, nil] The formatted value, or nil if invalid
|
|
45
|
+
def formatted
|
|
46
|
+
return nil unless valid?
|
|
47
|
+
|
|
48
|
+
@normalized
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [String] String representation
|
|
52
|
+
def to_s
|
|
53
|
+
formatted || ""
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [String, nil] The normalized value (alias for formatted)
|
|
57
|
+
def value
|
|
58
|
+
formatted
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Result object for ID validation with formatting.
|
|
63
|
+
class IdResult < Result
|
|
64
|
+
# @return [String, nil] 9-digit formatted ID
|
|
65
|
+
def formatted
|
|
66
|
+
return nil unless valid?
|
|
67
|
+
|
|
68
|
+
@normalized
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Result object for phone validation with type detection and formatting.
|
|
73
|
+
class PhoneResult < Result
|
|
74
|
+
attr_reader :type
|
|
75
|
+
|
|
76
|
+
# @param type [Symbol, nil] Detected phone type (:mobile, :landline, :voip)
|
|
77
|
+
def initialize(original:, normalized:, valid:, reason: nil, type: nil)
|
|
78
|
+
super(original: original, normalized: normalized, valid: valid, reason: reason)
|
|
79
|
+
@type = type
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Boolean] true if this is a mobile phone
|
|
83
|
+
def mobile?
|
|
84
|
+
type == :mobile
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Boolean] true if this is a landline phone
|
|
88
|
+
def landline?
|
|
89
|
+
type == :landline
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Boolean] true if this is a VoIP phone
|
|
93
|
+
def voip?
|
|
94
|
+
type == :voip
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Format the phone number.
|
|
98
|
+
#
|
|
99
|
+
# @param style [Symbol] :dashed, :international, or :compact
|
|
100
|
+
# @return [String, nil] Formatted phone or nil if invalid
|
|
101
|
+
def formatted(style: :dashed)
|
|
102
|
+
return nil unless valid?
|
|
103
|
+
|
|
104
|
+
Validators::Phone.format(@normalized, style: style)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Result object for postal code validation with formatting.
|
|
109
|
+
class PostalCodeResult < Result
|
|
110
|
+
# Format the postal code.
|
|
111
|
+
#
|
|
112
|
+
# @param style [Symbol] :compact or :spaced
|
|
113
|
+
# @return [String, nil] Formatted postal code or nil if invalid
|
|
114
|
+
def formatted(style: :compact)
|
|
115
|
+
return nil unless valid?
|
|
116
|
+
|
|
117
|
+
Validators::PostalCode.format(@normalized, style: style)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Result object for bank account validation with format detection.
|
|
122
|
+
class BankAccountResult < Result
|
|
123
|
+
attr_reader :format
|
|
124
|
+
|
|
125
|
+
# @param format [Symbol, nil] Detected format (:domestic or :iban)
|
|
126
|
+
def initialize(original:, normalized:, valid:, reason: nil, format: nil)
|
|
127
|
+
super(original: original, normalized: normalized, valid: valid, reason: reason)
|
|
128
|
+
@format = format
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @return [Boolean] true if this is a domestic account
|
|
132
|
+
def domestic?
|
|
133
|
+
format == :domestic
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [Boolean] true if this is an IBAN
|
|
137
|
+
def iban?
|
|
138
|
+
format == :iban
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Format the bank account.
|
|
142
|
+
#
|
|
143
|
+
# @param style [Symbol] :domestic, :compact, or :iban
|
|
144
|
+
# @return [String, nil] Formatted bank account or nil if invalid
|
|
145
|
+
def formatted(style: :domestic)
|
|
146
|
+
return nil unless valid?
|
|
147
|
+
|
|
148
|
+
Validators::BankAccount.format(@original, style: style)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -71,6 +71,56 @@ module Israeli
|
|
|
71
71
|
numeric.to_i % 97 == 1
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
# Returns the reason why a bank account is invalid.
|
|
75
|
+
#
|
|
76
|
+
# @param value [String, nil] The bank account to check
|
|
77
|
+
# @param format [Symbol] Format to validate: :domestic, :iban, or :any
|
|
78
|
+
# @return [Symbol, nil] Reason code or nil if valid
|
|
79
|
+
# - :blank - Input is nil or empty
|
|
80
|
+
# - :wrong_length - Incorrect number of digits/characters
|
|
81
|
+
# - :invalid_checksum - IBAN mod 97 checksum failed
|
|
82
|
+
# - :invalid_format - Does not match domestic or IBAN pattern
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# Israeli::Validators::BankAccount.invalid_reason("123") # => :wrong_length
|
|
86
|
+
def self.invalid_reason(value, format: :any)
|
|
87
|
+
return :blank if value.nil? || value.to_s.strip.empty?
|
|
88
|
+
|
|
89
|
+
case format
|
|
90
|
+
when :domestic then domestic_invalid_reason(value)
|
|
91
|
+
when :iban then iban_invalid_reason(value)
|
|
92
|
+
when :any then any_format_invalid_reason(value)
|
|
93
|
+
else :invalid_format
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.domestic_invalid_reason(value)
|
|
98
|
+
digits = Sanitizer.digits_only(value)
|
|
99
|
+
return :blank if digits.nil? || digits.empty?
|
|
100
|
+
|
|
101
|
+
digits.length == 13 ? nil : :wrong_length
|
|
102
|
+
end
|
|
103
|
+
private_class_method :domestic_invalid_reason
|
|
104
|
+
|
|
105
|
+
def self.iban_invalid_reason(value)
|
|
106
|
+
normalized = value.to_s.gsub(/\s/, "").upcase
|
|
107
|
+
return :wrong_length unless normalized.length == 23
|
|
108
|
+
return :invalid_format unless normalized.match?(/\AIL\d{21}\z/)
|
|
109
|
+
|
|
110
|
+
valid_iban?(normalized) ? nil : :invalid_checksum
|
|
111
|
+
end
|
|
112
|
+
private_class_method :iban_invalid_reason
|
|
113
|
+
|
|
114
|
+
def self.any_format_invalid_reason(value)
|
|
115
|
+
digits = Sanitizer.digits_only(value)
|
|
116
|
+
normalized = value.to_s.gsub(/\s/, "").upcase
|
|
117
|
+
|
|
118
|
+
return nil if digits&.length == 13 || valid_iban?(normalized)
|
|
119
|
+
|
|
120
|
+
:invalid_format
|
|
121
|
+
end
|
|
122
|
+
private_class_method :any_format_invalid_reason
|
|
123
|
+
|
|
74
124
|
# Formats a bank account to a specified style.
|
|
75
125
|
#
|
|
76
126
|
# @param value [String, nil] The bank account to format
|
|
@@ -37,6 +37,29 @@ module Israeli
|
|
|
37
37
|
Luhn.valid?(padded)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Returns the reason why an ID is invalid.
|
|
41
|
+
#
|
|
42
|
+
# @param value [String, Integer, nil] The ID number to check
|
|
43
|
+
# @return [Symbol, nil] Reason code or nil if valid
|
|
44
|
+
# - :blank - Input is nil or empty
|
|
45
|
+
# - :wrong_length - Not 9 digits after padding
|
|
46
|
+
# - :invalid_checksum - Luhn checksum failed
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# Israeli::Validators::Id.invalid_reason("123456789") # => :invalid_checksum
|
|
50
|
+
# Israeli::Validators::Id.invalid_reason("") # => :blank
|
|
51
|
+
# Israeli::Validators::Id.invalid_reason("123456782") # => nil (valid)
|
|
52
|
+
def self.invalid_reason(value)
|
|
53
|
+
digits = Sanitizer.digits_only(value)
|
|
54
|
+
return :blank if digits.nil? || digits.empty?
|
|
55
|
+
|
|
56
|
+
padded = digits.rjust(9, "0")
|
|
57
|
+
return :wrong_length unless padded.match?(/\A\d{9}\z/)
|
|
58
|
+
return :invalid_checksum unless Luhn.valid?(padded)
|
|
59
|
+
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
40
63
|
# Formats an Israeli ID to standard 9-digit format.
|
|
41
64
|
#
|
|
42
65
|
# @param value [String, Integer, nil] The ID number to format
|
|
@@ -50,7 +73,7 @@ module Israeli
|
|
|
50
73
|
return nil if digits.nil? || digits.empty?
|
|
51
74
|
|
|
52
75
|
padded = digits.rjust(9, "0")
|
|
53
|
-
return nil unless valid?(padded)
|
|
76
|
+
return nil unless padded.match?(/\A\d{9}\z/) && Luhn.valid?(padded)
|
|
54
77
|
|
|
55
78
|
padded
|
|
56
79
|
end
|
|
@@ -74,6 +74,51 @@ module Israeli
|
|
|
74
74
|
value.match?(VOIP_PATTERN)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# Detects the type of phone number.
|
|
78
|
+
#
|
|
79
|
+
# @param value [String, nil] The phone number to check
|
|
80
|
+
# @return [Symbol, nil] :mobile, :landline, :voip, or nil if invalid
|
|
81
|
+
#
|
|
82
|
+
# @example
|
|
83
|
+
# Israeli::Validators::Phone.detect_type("0501234567") # => :mobile
|
|
84
|
+
# Israeli::Validators::Phone.detect_type("021234567") # => :landline
|
|
85
|
+
# Israeli::Validators::Phone.detect_type("0721234567") # => :voip
|
|
86
|
+
# Israeli::Validators::Phone.detect_type("invalid") # => nil
|
|
87
|
+
def self.detect_type(value)
|
|
88
|
+
normalized = Sanitizer.normalize_phone(value)
|
|
89
|
+
return nil if normalized.nil? || normalized.empty?
|
|
90
|
+
|
|
91
|
+
return :mobile if mobile?(normalized)
|
|
92
|
+
return :landline if landline?(normalized)
|
|
93
|
+
return :voip if voip?(normalized)
|
|
94
|
+
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns the reason why a phone number is invalid.
|
|
99
|
+
#
|
|
100
|
+
# @param value [String, nil] The phone number to check
|
|
101
|
+
# @param type [Symbol] Type to validate: :mobile, :landline, :voip, or :any
|
|
102
|
+
# @return [Symbol, nil] Reason code or nil if valid
|
|
103
|
+
# - :blank - Input is nil or empty
|
|
104
|
+
# - :invalid_format - Does not match any Israeli phone pattern
|
|
105
|
+
# - :wrong_type - Valid phone but wrong type (e.g., landline when mobile expected)
|
|
106
|
+
#
|
|
107
|
+
# @example
|
|
108
|
+
# Israeli::Validators::Phone.invalid_reason("abc") # => :invalid_format
|
|
109
|
+
# Israeli::Validators::Phone.invalid_reason("021234567", type: :mobile) # => :wrong_type
|
|
110
|
+
def self.invalid_reason(value, type: :any)
|
|
111
|
+
normalized = Sanitizer.normalize_phone(value)
|
|
112
|
+
return :blank if normalized.nil? || normalized.empty?
|
|
113
|
+
|
|
114
|
+
detected = detect_type(normalized)
|
|
115
|
+
return :invalid_format if detected.nil?
|
|
116
|
+
|
|
117
|
+
return nil if type == :any || type == detected
|
|
118
|
+
|
|
119
|
+
:wrong_type
|
|
120
|
+
end
|
|
121
|
+
|
|
77
122
|
# Formats a phone number.
|
|
78
123
|
#
|
|
79
124
|
# @param value [String, nil] The phone number to format
|
|
@@ -31,6 +31,25 @@ module Israeli
|
|
|
31
31
|
digits.match?(/\A\d{7}\z/)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Returns the reason why a postal code is invalid.
|
|
35
|
+
#
|
|
36
|
+
# @param value [String, nil] The postal code to check
|
|
37
|
+
# @return [Symbol, nil] Reason code or nil if valid
|
|
38
|
+
# - :blank - Input is nil or empty
|
|
39
|
+
# - :wrong_length - Not exactly 7 digits
|
|
40
|
+
#
|
|
41
|
+
# @example
|
|
42
|
+
# Israeli::Validators::PostalCode.invalid_reason("123") # => :wrong_length
|
|
43
|
+
# Israeli::Validators::PostalCode.invalid_reason("") # => :blank
|
|
44
|
+
# Israeli::Validators::PostalCode.invalid_reason("2610101") # => nil (valid)
|
|
45
|
+
def self.invalid_reason(value)
|
|
46
|
+
digits = Sanitizer.digits_only(value)
|
|
47
|
+
return :blank if digits.nil? || digits.empty?
|
|
48
|
+
return :wrong_length unless digits.match?(/\A\d{7}\z/)
|
|
49
|
+
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
34
53
|
# Formats a postal code to standard representation.
|
|
35
54
|
#
|
|
36
55
|
# @param value [String, nil] The postal code to format
|
data/lib/israeli/version.rb
CHANGED
data/lib/israeli.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "israeli/version"
|
|
|
4
4
|
require_relative "israeli/errors"
|
|
5
5
|
require_relative "israeli/luhn"
|
|
6
6
|
require_relative "israeli/sanitizer"
|
|
7
|
+
require_relative "israeli/result"
|
|
7
8
|
|
|
8
9
|
# Main namespace for the Israeli validators gem.
|
|
9
10
|
#
|
|
@@ -20,8 +21,8 @@ require_relative "israeli/sanitizer"
|
|
|
20
21
|
# @see Israeli::Validators::Phone
|
|
21
22
|
# @see Israeli::Validators::PostalCode
|
|
22
23
|
# @see Israeli::Validators::BankAccount
|
|
23
|
-
module Israeli
|
|
24
|
-
class << self
|
|
24
|
+
module Israeli # rubocop:disable Metrics/ModuleLength
|
|
25
|
+
class << self # rubocop:disable Metrics/ClassLength
|
|
25
26
|
# Validates an Israeli ID number (Mispar Zehut).
|
|
26
27
|
#
|
|
27
28
|
# @param value [String, Integer] The ID number to validate
|
|
@@ -61,6 +62,18 @@ module Israeli
|
|
|
61
62
|
Validators::Phone.valid?(value, type: type)
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
# Detects the type of phone number.
|
|
66
|
+
#
|
|
67
|
+
# @param value [String] The phone number to check
|
|
68
|
+
# @return [Symbol, nil] :mobile, :landline, :voip, or nil if invalid
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# Israeli.phone_type("0501234567") # => :mobile
|
|
72
|
+
# Israeli.phone_type("021234567") # => :landline
|
|
73
|
+
def phone_type(value)
|
|
74
|
+
Validators::Phone.detect_type(value)
|
|
75
|
+
end
|
|
76
|
+
|
|
64
77
|
# Validates an Israeli bank account number.
|
|
65
78
|
#
|
|
66
79
|
# @param value [String] The bank account to validate
|
|
@@ -90,6 +103,202 @@ module Israeli
|
|
|
90
103
|
def format_phone(value, style: :dashed)
|
|
91
104
|
Validators::Phone.format(value, style: style)
|
|
92
105
|
end
|
|
106
|
+
|
|
107
|
+
# Formats an Israeli postal code.
|
|
108
|
+
#
|
|
109
|
+
# @param value [String] The postal code to format
|
|
110
|
+
# @param style [Symbol] Format style: :compact or :spaced
|
|
111
|
+
# @return [String, nil] Formatted postal code or nil if invalid
|
|
112
|
+
def format_postal_code(value, style: :compact)
|
|
113
|
+
Validators::PostalCode.format(value, style: style)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Formats an Israeli bank account number.
|
|
117
|
+
#
|
|
118
|
+
# @param value [String] The bank account to format
|
|
119
|
+
# @param style [Symbol] Format style: :domestic, :compact, or :iban
|
|
120
|
+
# @return [String, nil] Formatted bank account or nil if invalid
|
|
121
|
+
def format_bank_account(value, style: :domestic)
|
|
122
|
+
Validators::BankAccount.format(value, style: style)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Bang Methods - raise exceptions on invalid input
|
|
126
|
+
# These are useful when you want to fail fast rather than check booleans
|
|
127
|
+
|
|
128
|
+
# Validates an Israeli ID number, raising an error if invalid.
|
|
129
|
+
#
|
|
130
|
+
# @param value [String, Integer] The ID number to validate
|
|
131
|
+
# @return [true] Always returns true if valid
|
|
132
|
+
# @raise [Israeli::InvalidIdError] if the ID is invalid
|
|
133
|
+
#
|
|
134
|
+
# @example
|
|
135
|
+
# Israeli.valid_id!("123456782") # => true
|
|
136
|
+
# Israeli.valid_id!("123456789") # => raises InvalidIdError
|
|
137
|
+
def valid_id!(value)
|
|
138
|
+
return true if valid_id?(value)
|
|
139
|
+
|
|
140
|
+
raise InvalidIdError.new("Invalid Israeli ID", reason: Validators::Id.invalid_reason(value))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Validates an Israeli phone number, raising an error if invalid.
|
|
144
|
+
#
|
|
145
|
+
# @param value [String] The phone number to validate
|
|
146
|
+
# @param type [Symbol] Type of phone: :mobile, :landline, :voip, or :any
|
|
147
|
+
# @return [true] Always returns true if valid
|
|
148
|
+
# @raise [Israeli::InvalidPhoneError] if the phone is invalid
|
|
149
|
+
def valid_phone!(value, type: :any)
|
|
150
|
+
return true if valid_phone?(value, type: type)
|
|
151
|
+
|
|
152
|
+
reason = Validators::Phone.invalid_reason(value, type: type)
|
|
153
|
+
raise InvalidPhoneError.new("Invalid Israeli phone number", reason: reason)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Validates an Israeli postal code, raising an error if invalid.
|
|
157
|
+
#
|
|
158
|
+
# @param value [String] The postal code to validate
|
|
159
|
+
# @return [true] Always returns true if valid
|
|
160
|
+
# @raise [Israeli::InvalidPostalCodeError] if the postal code is invalid
|
|
161
|
+
def valid_postal_code!(value)
|
|
162
|
+
return true if valid_postal_code?(value)
|
|
163
|
+
|
|
164
|
+
raise InvalidPostalCodeError.new("Invalid Israeli postal code", reason: Validators::PostalCode.invalid_reason(value))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Validates an Israeli bank account, raising an error if invalid.
|
|
168
|
+
#
|
|
169
|
+
# @param value [String] The bank account to validate
|
|
170
|
+
# @param format [Symbol] Format: :domestic, :iban, or :any
|
|
171
|
+
# @return [true] Always returns true if valid
|
|
172
|
+
# @raise [Israeli::InvalidBankAccountError] if the bank account is invalid
|
|
173
|
+
def valid_bank_account!(value, format: :any)
|
|
174
|
+
return true if valid_bank_account?(value, format: format)
|
|
175
|
+
|
|
176
|
+
reason = Validators::BankAccount.invalid_reason(value, format: format)
|
|
177
|
+
raise InvalidBankAccountError.new("Invalid Israeli bank account", reason: reason)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Parse Methods - return rich result objects
|
|
181
|
+
# These provide more detailed information than simple boolean validation
|
|
182
|
+
|
|
183
|
+
# Parses an Israeli ID number and returns a result object.
|
|
184
|
+
#
|
|
185
|
+
# @param value [String, Integer] The ID number to parse
|
|
186
|
+
# @return [Israeli::IdResult] Result object with validation status and formatting
|
|
187
|
+
#
|
|
188
|
+
# @example
|
|
189
|
+
# result = Israeli.parse_id("123456782")
|
|
190
|
+
# result.valid? # => true
|
|
191
|
+
# result.formatted # => "123456782"
|
|
192
|
+
#
|
|
193
|
+
# @example Invalid ID
|
|
194
|
+
# result = Israeli.parse_id("123456789")
|
|
195
|
+
# result.valid? # => false
|
|
196
|
+
# result.reason # => :invalid_checksum
|
|
197
|
+
def parse_id(value)
|
|
198
|
+
digits = Sanitizer.digits_only(value)
|
|
199
|
+
normalized = digits&.rjust(9, "0")
|
|
200
|
+
valid = Validators::Id.valid?(value)
|
|
201
|
+
reason = valid ? nil : Validators::Id.invalid_reason(value)
|
|
202
|
+
|
|
203
|
+
IdResult.new(
|
|
204
|
+
original: value,
|
|
205
|
+
normalized: normalized,
|
|
206
|
+
valid: valid,
|
|
207
|
+
reason: reason
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Parses an Israeli phone number and returns a result object.
|
|
212
|
+
#
|
|
213
|
+
# @param value [String] The phone number to parse
|
|
214
|
+
# @return [Israeli::PhoneResult] Result object with type detection and formatting
|
|
215
|
+
#
|
|
216
|
+
# @example
|
|
217
|
+
# result = Israeli.parse_phone("0501234567")
|
|
218
|
+
# result.valid? # => true
|
|
219
|
+
# result.type # => :mobile
|
|
220
|
+
# result.mobile? # => true
|
|
221
|
+
# result.formatted(style: :dashed) # => "050-123-4567"
|
|
222
|
+
def parse_phone(value)
|
|
223
|
+
normalized = Sanitizer.normalize_phone(value)
|
|
224
|
+
valid = Validators::Phone.valid?(value)
|
|
225
|
+
phone_type = Validators::Phone.detect_type(value)
|
|
226
|
+
reason = valid ? nil : Validators::Phone.invalid_reason(value)
|
|
227
|
+
|
|
228
|
+
PhoneResult.new(
|
|
229
|
+
original: value,
|
|
230
|
+
normalized: normalized,
|
|
231
|
+
valid: valid,
|
|
232
|
+
reason: reason,
|
|
233
|
+
type: phone_type
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Parses an Israeli postal code and returns a result object.
|
|
238
|
+
#
|
|
239
|
+
# @param value [String] The postal code to parse
|
|
240
|
+
# @return [Israeli::PostalCodeResult] Result object with validation and formatting
|
|
241
|
+
#
|
|
242
|
+
# @example
|
|
243
|
+
# result = Israeli.parse_postal_code("2610101")
|
|
244
|
+
# result.valid? # => true
|
|
245
|
+
# result.formatted # => "2610101"
|
|
246
|
+
# result.formatted(style: :spaced) # => "26101 01"
|
|
247
|
+
def parse_postal_code(value)
|
|
248
|
+
digits = Sanitizer.digits_only(value)
|
|
249
|
+
valid = Validators::PostalCode.valid?(value)
|
|
250
|
+
reason = valid ? nil : Validators::PostalCode.invalid_reason(value)
|
|
251
|
+
|
|
252
|
+
PostalCodeResult.new(
|
|
253
|
+
original: value,
|
|
254
|
+
normalized: digits,
|
|
255
|
+
valid: valid,
|
|
256
|
+
reason: reason
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Parses an Israeli bank account and returns a result object.
|
|
261
|
+
#
|
|
262
|
+
# @param value [String] The bank account to parse
|
|
263
|
+
# @return [Israeli::BankAccountResult] Result object with format detection
|
|
264
|
+
#
|
|
265
|
+
# @example
|
|
266
|
+
# result = Israeli.parse_bank_account("4985622815429")
|
|
267
|
+
# result.valid? # => true
|
|
268
|
+
# result.domestic? # => true
|
|
269
|
+
# result.formatted # => "49-856-22815429"
|
|
270
|
+
def parse_bank_account(value)
|
|
271
|
+
valid = Validators::BankAccount.valid?(value)
|
|
272
|
+
reason = valid ? nil : Validators::BankAccount.invalid_reason(value)
|
|
273
|
+
detected_format = detect_bank_format(value) if valid
|
|
274
|
+
|
|
275
|
+
BankAccountResult.new(
|
|
276
|
+
original: value,
|
|
277
|
+
normalized: normalize_bank_value(value, detected_format),
|
|
278
|
+
valid: valid,
|
|
279
|
+
reason: reason,
|
|
280
|
+
format: detected_format
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
private
|
|
285
|
+
|
|
286
|
+
def detect_bank_format(value)
|
|
287
|
+
digits = Sanitizer.digits_only(value)
|
|
288
|
+
normalized = value.to_s.gsub(/\s/, "").upcase
|
|
289
|
+
|
|
290
|
+
return :domestic if Validators::BankAccount.valid_domestic?(digits)
|
|
291
|
+
return :iban if Validators::BankAccount.valid_iban?(normalized)
|
|
292
|
+
|
|
293
|
+
nil
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def normalize_bank_value(value, detected_format)
|
|
297
|
+
case detected_format
|
|
298
|
+
when :iban then value.to_s.gsub(/\s/, "").upcase
|
|
299
|
+
else Sanitizer.digits_only(value)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
93
302
|
end
|
|
94
303
|
end
|
|
95
304
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: israeli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dpaluy
|
|
@@ -34,6 +34,7 @@ files:
|
|
|
34
34
|
- lib/israeli/errors.rb
|
|
35
35
|
- lib/israeli/luhn.rb
|
|
36
36
|
- lib/israeli/railtie.rb
|
|
37
|
+
- lib/israeli/result.rb
|
|
37
38
|
- lib/israeli/sanitizer.rb
|
|
38
39
|
- lib/israeli/validators/bank_account.rb
|
|
39
40
|
- lib/israeli/validators/id.rb
|
|
@@ -65,7 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
65
66
|
- !ruby/object:Gem::Version
|
|
66
67
|
version: '0'
|
|
67
68
|
requirements: []
|
|
68
|
-
rubygems_version:
|
|
69
|
+
rubygems_version: 3.6.9
|
|
69
70
|
specification_version: 4
|
|
70
71
|
summary: Validation utilities for Israeli identifiers (ID, phone, postal code, bank
|
|
71
72
|
account).
|