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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 775d9b34a1173bd2bbab00ca4a86f13d8fb340c13ee84d15f5893cdc7d1d6dab
4
- data.tar.gz: 6b872c2ebbd7bd5573854e20f79e5bbd1ae5de265a879f370af625a0a2a00d8a
3
+ metadata.gz: c891a985fc73096b35660dd06031c286ab7bd4bcf4257d1a953ba43d56eb0a8a
4
+ data.tar.gz: 57a1d7fda2b852f9528d802bec9e8676500551cf117ac7349b6b7a221d8d39d4
5
5
  SHA512:
6
- metadata.gz: d5d9bc4d5d2b1e874221c84612307533dfc252a077c2fe5c388cfb55ca4283f39e180656353697ac38e65285a1f700bc1786f37791ff8b401f09e1252255852e
7
- data.tar.gz: de83f4a136c0f53111ba505cd9165c02fac56e044a2e3d9d2819a9d459e91b4a792c50fd6b8090dc2365cc1b12de0f98d3a45225fb3685b0a2088198205c211f
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
@@ -7,7 +7,7 @@ module Israeli
7
7
  #
8
8
  # @example Handling errors
9
9
  # begin
10
- # Israeli.format_id!("invalid")
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.format_id!("abc")
20
- # # => Israeli::InvalidFormatError: ID must contain only digits
21
- class InvalidFormatError < Error; end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Israeli
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.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: 4.0.0
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).