string_validator 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 +22 -0
- data/LICENSE.txt +1 -1
- data/README.md +19 -1
- data/lib/string_validator/sanitizers.rb +62 -8
- data/lib/string_validator/validators.rb +165 -4
- data/lib/string_validator/version.rb +1 -1
- data/lib/string_validator.rb +6 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab042a16d657aecd6e9eef8521ae05d391ae43b16e2a1abcb3846849856c5ffe
|
|
4
|
+
data.tar.gz: 3caf3d2eab31fd3db17e89597b9d320fb6a10e9278057212f7007d00653279d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66733280c4024e9b2aa059aa7dd3df6a768a8b8adb6af3ae728a7abd21ffa373eb97c85249bc93ee5f6734dc32bbf80266c9c4eccdc57b55d32a49a31319868c
|
|
7
|
+
data.tar.gz: '0053390b7e49c6ab06b4ea316b30dc91f4041d1f17e33121c72310156942b55d6f56a4d8ca9341d8be9478091b6e2f5daabdf5c05b9a6ec27b7ef7837bb9fc1e'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Validators: `is_port?`, `is_iso8601?`, `is_date?`, `is_data_uri?`, `is_sem_ver?`, `is_mongo_id?`, `is_iban?`, `is_postal_code?`, `is_jwt?`
|
|
8
|
+
- `is_iban?`: ISO 13616 IBAN with mod-97 check; optional `locale:` (ISO 3166-1 alpha-2) to restrict country
|
|
9
|
+
- `is_postal_code?`: format check by `locale:` (US, GB, CA, DE, FR, IN, NL, ES, IT, AU, JP, BR, PL, CH, AT, BE, SE, NO, DK, FI)
|
|
10
|
+
- `is_jwt?`: three-part base64url structure and valid JSON in header/payload (no signature verification)
|
|
11
|
+
- Sanitizer: `normalize_email` (strip + lowercase, with optional `lowercase_domain: true`)
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- `whitelist` now supports character ranges (e.g. `"a-c0-9"` for letters a–c and digits 0–9)
|
|
16
|
+
- Fixed indentation in `is_url?` for clarity
|
|
17
|
+
|
|
18
|
+
### Error handling
|
|
19
|
+
|
|
20
|
+
- **`StringValidator::InvalidValidatorError`** — raised by `valid?` when the validator name is unknown
|
|
21
|
+
- **`valid?`** — checks that the validator exists before calling (no more `NoMethodError` on typos)
|
|
22
|
+
- **Validators** — return `false` on bad input: non-String, `nil` seed/comparison/values, and rescue `ArgumentError`, `TypeError`, `Encoding::InvalidByteSequenceError` where parsing or encoding can fail (`is_alpha?`, `is_alphanumeric?`, `contains?`, `equals?`, `is_in?`, `is_iso8601?`, `is_date?`, `is_iban?`, `is_postal_code?`, `is_jwt?`)
|
|
23
|
+
- **Sanitizers** — return the original value on non-String or on error; `trim`, `blacklist`, `whitelist` handle `nil` chars; `to_int`/`to_float` rescue `TypeError`; all sanitizers rescue encoding/argument errors where applicable
|
|
24
|
+
|
|
3
25
|
## [0.1.0] - 2025-02-16
|
|
4
26
|
|
|
5
27
|
### Added
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -68,6 +68,15 @@ StringValidator.is_mac_address?("01:02:03:04:05:ab") # => true
|
|
|
68
68
|
StringValidator.is_credit_card?("4111111111111111") # => true (Luhn check)
|
|
69
69
|
StringValidator.is_md5?("d41d8cd98f00b204e9800998ecf8427e") # => true
|
|
70
70
|
StringValidator.is_hexadecimal?("0a1f") # => true
|
|
71
|
+
StringValidator.is_port?("443") # => true
|
|
72
|
+
StringValidator.is_iso8601?("2024-01-15") # => true
|
|
73
|
+
StringValidator.is_date?("2024-01-15") # => true
|
|
74
|
+
StringValidator.is_data_uri?("data:image/png;base64,...") # => true
|
|
75
|
+
StringValidator.is_sem_ver?("1.2.3") # => true
|
|
76
|
+
StringValidator.is_mongo_id?("507f1f77bcf86cd799439011") # => true
|
|
77
|
+
StringValidator.is_iban?("GB82WEST12345698765432") # => true (optional locale: "GB")
|
|
78
|
+
StringValidator.is_postal_code?("12345", locale: "US") # => true (US, GB, CA, DE, IN, etc.)
|
|
79
|
+
StringValidator.is_jwt?("eyJhbGc...") # => true (structure only; no signature check)
|
|
71
80
|
|
|
72
81
|
StringValidator.contains?("hello world", "world") # => true
|
|
73
82
|
StringValidator.equals?("foo", "foo") # => true
|
|
@@ -93,6 +102,7 @@ StringValidator.to_boolean("false") # => false
|
|
|
93
102
|
StringValidator.to_boolean("yes", strict: false) # => true
|
|
94
103
|
StringValidator.to_int("42") # => 42
|
|
95
104
|
StringValidator.to_float("3.14") # => 3.14
|
|
105
|
+
StringValidator.normalize_email(" User@Example.COM ") # => "user@example.com"
|
|
96
106
|
|
|
97
107
|
StringValidator.unescape("<tag>") # => "<tag>"
|
|
98
108
|
```
|
|
@@ -126,13 +136,21 @@ end
|
|
|
126
136
|
|
|
127
137
|
### Safe validation helper
|
|
128
138
|
|
|
129
|
-
|
|
139
|
+
`valid?` enforces that the first argument is a String and that the validator name exists:
|
|
130
140
|
|
|
131
141
|
```ruby
|
|
132
142
|
StringValidator.valid?("user@example.com", :is_email?) # => true
|
|
133
143
|
StringValidator.valid?(nil, :is_email?) # => raises NotStringError
|
|
144
|
+
StringValidator.valid?("x", :not_a_validator) # => raises InvalidValidatorError
|
|
134
145
|
```
|
|
135
146
|
|
|
147
|
+
**Error classes** (all inherit from `StringValidator::Error`):
|
|
148
|
+
|
|
149
|
+
- `StringValidator::NotStringError` — input to `valid?` is not a String
|
|
150
|
+
- `StringValidator::InvalidValidatorError` — unknown validator name passed to `valid?`
|
|
151
|
+
|
|
152
|
+
**Robustness:** Validators return `false` (and sanitizers return the original value) when given non-strings, `nil` where it would cause errors, or when parsing/encoding fails, so you can pass user input safely without rescuing.
|
|
153
|
+
|
|
136
154
|
## Supported Ruby
|
|
137
155
|
|
|
138
156
|
Ruby 3.0+.
|
|
@@ -14,25 +14,37 @@ module StringValidator
|
|
|
14
14
|
def escape(str)
|
|
15
15
|
return str unless str.is_a?(String)
|
|
16
16
|
str.gsub(/[&<>"'\/]/, HTML_ESCAPE)
|
|
17
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError
|
|
18
|
+
str
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def trim(str, chars = nil)
|
|
20
22
|
return str unless str.is_a?(String)
|
|
21
|
-
if chars
|
|
22
|
-
str.
|
|
23
|
-
else
|
|
24
|
-
str.strip
|
|
23
|
+
if chars.nil? || chars == ""
|
|
24
|
+
return str.strip
|
|
25
25
|
end
|
|
26
|
+
chars_str = chars.to_s
|
|
27
|
+
str.gsub(/\A[#{Regexp.escape(chars_str)}]+|[#{Regexp.escape(chars_str)}]+\z/, "")
|
|
28
|
+
rescue TypeError, ArgumentError, Encoding::InvalidByteSequenceError
|
|
29
|
+
str
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
def blacklist(str, chars)
|
|
29
33
|
return str unless str.is_a?(String)
|
|
34
|
+
return str if chars.nil?
|
|
35
|
+
return str unless chars.is_a?(String)
|
|
30
36
|
str.delete(chars)
|
|
37
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError
|
|
38
|
+
str
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
def whitelist(str, chars)
|
|
34
42
|
return str unless str.is_a?(String)
|
|
35
|
-
str
|
|
43
|
+
return str if chars.nil?
|
|
44
|
+
allowed = expand_whitelist_chars(chars)
|
|
45
|
+
str.each_char.select { |c| allowed.include?(c) }.join
|
|
46
|
+
rescue TypeError, ArgumentError, Encoding::InvalidByteSequenceError
|
|
47
|
+
str
|
|
36
48
|
end
|
|
37
49
|
|
|
38
50
|
def strip_low(str, keep_new_lines: false)
|
|
@@ -42,6 +54,8 @@ module StringValidator
|
|
|
42
54
|
else
|
|
43
55
|
str.each_char.select { |c| c.ord >= 32 }.join
|
|
44
56
|
end
|
|
57
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError
|
|
58
|
+
str
|
|
45
59
|
end
|
|
46
60
|
|
|
47
61
|
def to_boolean(str, strict: true)
|
|
@@ -52,19 +66,21 @@ module StringValidator
|
|
|
52
66
|
return true if !strict && %w[yes y].include?(s)
|
|
53
67
|
return false if !strict && %w[no n].include?(s)
|
|
54
68
|
str
|
|
69
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError
|
|
70
|
+
str
|
|
55
71
|
end
|
|
56
72
|
|
|
57
73
|
def to_int(str, radix: 10)
|
|
58
74
|
return str unless str.is_a?(String)
|
|
59
|
-
Integer(str, radix)
|
|
60
|
-
rescue ArgumentError
|
|
75
|
+
Integer(str, radix.to_i)
|
|
76
|
+
rescue ArgumentError, TypeError
|
|
61
77
|
str
|
|
62
78
|
end
|
|
63
79
|
|
|
64
80
|
def to_float(str)
|
|
65
81
|
return str unless str.is_a?(String)
|
|
66
82
|
Float(str)
|
|
67
|
-
rescue ArgumentError
|
|
83
|
+
rescue ArgumentError, TypeError
|
|
68
84
|
str
|
|
69
85
|
end
|
|
70
86
|
|
|
@@ -77,6 +93,44 @@ module StringValidator
|
|
|
77
93
|
.gsub(""", '"')
|
|
78
94
|
.gsub("'", "'")
|
|
79
95
|
.gsub("/", "/")
|
|
96
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError
|
|
97
|
+
str
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def normalize_email(str, lowercase_domain: true)
|
|
101
|
+
return str unless str.is_a?(String)
|
|
102
|
+
s = str.strip
|
|
103
|
+
return s if s.empty?
|
|
104
|
+
local, at, domain = s.rpartition("@")
|
|
105
|
+
return s if at != "@" || local.empty? || domain.empty?
|
|
106
|
+
local = local.downcase
|
|
107
|
+
domain = domain.downcase if lowercase_domain
|
|
108
|
+
"#{local}@#{domain}"
|
|
109
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError
|
|
110
|
+
str
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def expand_whitelist_chars(chars)
|
|
116
|
+
return "" unless chars.is_a?(String)
|
|
117
|
+
set = +""
|
|
118
|
+
i = 0
|
|
119
|
+
while i < chars.length
|
|
120
|
+
if i + 2 < chars.length && chars[i + 1] == "-"
|
|
121
|
+
from = chars[i].ord
|
|
122
|
+
to = chars[i + 2].ord
|
|
123
|
+
from, to = to, from if from > to
|
|
124
|
+
set << (from..to).map(&:chr).join
|
|
125
|
+
i += 3
|
|
126
|
+
else
|
|
127
|
+
set << chars[i]
|
|
128
|
+
i += 1
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
set
|
|
132
|
+
rescue RangeError, ArgumentError, Encoding::InvalidByteSequenceError
|
|
133
|
+
""
|
|
80
134
|
end
|
|
81
135
|
end
|
|
82
136
|
end
|
|
@@ -3,12 +3,52 @@
|
|
|
3
3
|
require "uri"
|
|
4
4
|
require "json"
|
|
5
5
|
require "base64"
|
|
6
|
+
require "date"
|
|
7
|
+
require "time"
|
|
6
8
|
|
|
7
9
|
module StringValidator
|
|
8
10
|
module Validators
|
|
9
11
|
# RFC 5322 simplified email regex (validator.js compatible)
|
|
10
12
|
EMAIL_REGEX = /\A[^\s@]+@[^\s@]+\.[^\s@]+\z/
|
|
11
13
|
|
|
14
|
+
# IBAN length per ISO 3166-1 alpha-2 country code
|
|
15
|
+
IBAN_LENGTHS = {
|
|
16
|
+
"AD" => 24, "AE" => 23, "AL" => 28, "AT" => 20, "AZ" => 28, "BA" => 20, "BE" => 16,
|
|
17
|
+
"BG" => 22, "BH" => 22, "BR" => 29, "BY" => 28, "CH" => 21, "CR" => 22, "CY" => 28,
|
|
18
|
+
"CZ" => 24, "DE" => 22, "DK" => 18, "DO" => 28, "EE" => 20, "ES" => 24, "FI" => 18,
|
|
19
|
+
"FO" => 18, "FR" => 27, "GB" => 22, "GE" => 22, "GI" => 23, "GL" => 18, "GR" => 27,
|
|
20
|
+
"HR" => 21, "HU" => 28, "IE" => 22, "IL" => 23, "IS" => 26, "IT" => 27, "JO" => 30,
|
|
21
|
+
"KW" => 30, "KZ" => 20, "LB" => 28, "LI" => 21, "LT" => 20, "LU" => 20, "LV" => 21,
|
|
22
|
+
"MC" => 27, "MD" => 24, "ME" => 22, "MK" => 19, "MT" => 31, "MU" => 30, "NL" => 18,
|
|
23
|
+
"NO" => 15, "PK" => 24, "PL" => 28, "PS" => 29, "PT" => 25, "QA" => 29, "RO" => 24,
|
|
24
|
+
"RS" => 22, "SA" => 24, "SE" => 24, "SI" => 19, "SK" => 24, "SM" => 27, "TL" => 23,
|
|
25
|
+
"TN" => 24, "TR" => 26, "UA" => 29, "VA" => 22, "VG" => 24, "XK" => 20
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Postal code regex by locale (ISO 3166-1 alpha-2). Format only, not existence check.
|
|
29
|
+
POSTAL_CODE_PATTERNS = {
|
|
30
|
+
"US" => /\A\d{5}(-\d{4})?\z/,
|
|
31
|
+
"GB" => /\A[A-Z]{1,2}\d[A-Z\d]?\s*\d[ABD-HJLNP-UW-Z]{2}\z/i,
|
|
32
|
+
"CA" => /\A[ABCEGHJKLMNPRSTVXY]\d[A-Z]\s*\d[A-Z]\d\z/i,
|
|
33
|
+
"DE" => /\A\d{5}\z/,
|
|
34
|
+
"FR" => /\A\d{5}\z/,
|
|
35
|
+
"IN" => /\A\d{6}\z/,
|
|
36
|
+
"NL" => /\A\d{4}\s*[A-Z]{2}\z/i,
|
|
37
|
+
"ES" => /\A\d{5}\z/,
|
|
38
|
+
"IT" => /\A\d{5}\z/,
|
|
39
|
+
"AU" => /\A\d{4}\z/,
|
|
40
|
+
"JP" => /\A\d{3}-?\d{4}\z/,
|
|
41
|
+
"BR" => /\A\d{5}-?\d{3}\z/,
|
|
42
|
+
"PL" => /\A\d{2}-?\d{3}\z/,
|
|
43
|
+
"CH" => /\A\d{4}\z/,
|
|
44
|
+
"AT" => /\A\d{4}\z/,
|
|
45
|
+
"BE" => /\A\d{4}\z/,
|
|
46
|
+
"SE" => /\A\d{3}\s*\d{2}\z/,
|
|
47
|
+
"NO" => /\A\d{4}\z/,
|
|
48
|
+
"DK" => /\A\d{4}\z/,
|
|
49
|
+
"FI" => /\A\d{5}\z/
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
12
52
|
def is_email?(str, allow_display_name: false, require_tld: true)
|
|
13
53
|
return false unless str.is_a?(String)
|
|
14
54
|
s = str.strip
|
|
@@ -29,9 +69,9 @@ module StringValidator
|
|
|
29
69
|
uri = ::URI.parse(s)
|
|
30
70
|
return false if uri.host.nil? && uri.opaque.nil?
|
|
31
71
|
if require_tld && uri.host && !uri.host.include?(".")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
return false
|
|
73
|
+
end
|
|
74
|
+
true
|
|
35
75
|
rescue ::URI::InvalidURIError
|
|
36
76
|
false
|
|
37
77
|
end
|
|
@@ -69,12 +109,16 @@ module StringValidator
|
|
|
69
109
|
return false unless str.is_a?(String)
|
|
70
110
|
return str.match?(/\A[a-zA-Z]+\z/) if locale.to_s.downcase.start_with?("en")
|
|
71
111
|
str.match?(/\A\p{L}+\z/)
|
|
112
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError
|
|
113
|
+
false
|
|
72
114
|
end
|
|
73
115
|
|
|
74
116
|
def is_alphanumeric?(str, locale: "en-US")
|
|
75
117
|
return false unless str.is_a?(String)
|
|
76
118
|
return str.match?(/\A[a-zA-Z0-9]+\z/) if locale.to_s.downcase.start_with?("en")
|
|
77
119
|
str.match?(/\A[\p{L}0-9]+\z/)
|
|
120
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError
|
|
121
|
+
false
|
|
78
122
|
end
|
|
79
123
|
|
|
80
124
|
def is_numeric?(str, no_symbols: false)
|
|
@@ -170,21 +214,29 @@ module StringValidator
|
|
|
170
214
|
|
|
171
215
|
def contains?(str, seed, ignore_case: false, min_occurrences: 1)
|
|
172
216
|
return false unless str.is_a?(String)
|
|
217
|
+
return false if seed.nil?
|
|
218
|
+
se = seed.to_s
|
|
173
219
|
s = str
|
|
174
220
|
s = s.downcase if ignore_case
|
|
175
|
-
se =
|
|
221
|
+
se = se.downcase if ignore_case
|
|
176
222
|
count = s.scan(Regexp.escape(se)).size
|
|
177
223
|
count >= min_occurrences
|
|
224
|
+
rescue TypeError, ArgumentError
|
|
225
|
+
false
|
|
178
226
|
end
|
|
179
227
|
|
|
180
228
|
def equals?(str, comparison)
|
|
181
229
|
return false unless str.is_a?(String)
|
|
182
230
|
str == comparison.to_s
|
|
231
|
+
rescue TypeError
|
|
232
|
+
false
|
|
183
233
|
end
|
|
184
234
|
|
|
185
235
|
def is_in?(str, values)
|
|
186
236
|
return false unless str.is_a?(String)
|
|
187
237
|
Array(values).map(&:to_s).include?(str)
|
|
238
|
+
rescue TypeError, ArgumentError
|
|
239
|
+
false
|
|
188
240
|
end
|
|
189
241
|
|
|
190
242
|
def is_credit_card?(str, provider: nil)
|
|
@@ -212,8 +264,117 @@ module StringValidator
|
|
|
212
264
|
str.match?(/\A[0-9a-fA-F]+\z/)
|
|
213
265
|
end
|
|
214
266
|
|
|
267
|
+
def is_port?(str)
|
|
268
|
+
return false unless str.is_a?(String)
|
|
269
|
+
return false unless str.match?(/\A\d+\z/)
|
|
270
|
+
n = str.to_i
|
|
271
|
+
n >= 0 && n <= 65_535
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def is_iso8601?(str)
|
|
275
|
+
return false unless str.is_a?(String)
|
|
276
|
+
# Date only: validate with Date to reject e.g. 2024-13-01
|
|
277
|
+
if str.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
278
|
+
::Date.iso8601(str)
|
|
279
|
+
return true
|
|
280
|
+
end
|
|
281
|
+
# Allow "YYYY-MM-DD HH:MM:SS" by normalizing to use T
|
|
282
|
+
s = str.sub(/\A(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)\z/, '\1T\2')
|
|
283
|
+
::Time.iso8601(s)
|
|
284
|
+
true
|
|
285
|
+
rescue ArgumentError, TypeError, Encoding::InvalidByteSequenceError
|
|
286
|
+
false
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def is_date?(str, format: nil)
|
|
290
|
+
return false unless str.is_a?(String)
|
|
291
|
+
if format
|
|
292
|
+
::Date.strptime(str, format.to_s)
|
|
293
|
+
else
|
|
294
|
+
::Date.parse(str)
|
|
295
|
+
end
|
|
296
|
+
true
|
|
297
|
+
rescue ArgumentError, TypeError, Encoding::InvalidByteSequenceError
|
|
298
|
+
false
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def is_data_uri?(str)
|
|
302
|
+
return false unless str.is_a?(String)
|
|
303
|
+
str.match?(/\Adata:([a-zA-Z0-9]+\/[a-zA-Z0-9+.+-]+)?;base64,[A-Za-z0-9+\/=]+\z/) ||
|
|
304
|
+
str.match?(/\Adata:([a-zA-Z0-9]+\/[a-zA-Z0-9+.+-]+)?(;[a-zA-Z0-9-]+=[a-zA-Z0-9-]+)*,(%[0-9a-fA-F]{2}|[a-zA-Z0-9!\$&'*+.^_`|~-])*\z/)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def is_sem_ver?(str)
|
|
308
|
+
return false unless str.is_a?(String)
|
|
309
|
+
str.match?(/\A\d+\.\d+\.\d+(?:-[0-9a-zA-Z.-]+)?(?:\+[0-9a-zA-Z.-]+)?\z/)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def is_mongo_id?(str)
|
|
313
|
+
return false unless str.is_a?(String)
|
|
314
|
+
str.match?(/\A[0-9a-fA-F]{24}\z/)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# IBAN: ISO 13616, mod-97 check. locale = ISO 3166-1 alpha-2 to restrict to that country (optional).
|
|
318
|
+
def is_iban?(str, locale: nil)
|
|
319
|
+
return false unless str.is_a?(String)
|
|
320
|
+
s = str.delete(" ").upcase
|
|
321
|
+
return false unless s.match?(/\A[A-Z]{2}\d{2}[A-Z0-9]+\z/)
|
|
322
|
+
cc = s[0, 2]
|
|
323
|
+
return false unless (expected_len = IBAN_LENGTHS[cc])
|
|
324
|
+
return false if locale && cc != locale.to_s.upcase[0, 2]
|
|
325
|
+
return false unless s.length == expected_len
|
|
326
|
+
iban_mod97_valid?(s)
|
|
327
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError
|
|
328
|
+
false
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Postal code format by locale (ISO 3166-1 alpha-2). Supports: US, GB, CA, DE, FR, IN, NL, etc.
|
|
332
|
+
def is_postal_code?(str, locale: "US")
|
|
333
|
+
return false unless str.is_a?(String)
|
|
334
|
+
s = str.strip
|
|
335
|
+
re = POSTAL_CODE_PATTERNS[locale.to_s.upcase]
|
|
336
|
+
return false unless re
|
|
337
|
+
s.match?(re)
|
|
338
|
+
rescue TypeError, ArgumentError, Encoding::InvalidByteSequenceError
|
|
339
|
+
false
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# JWT structure: three base64url parts (header.payload.signature). Does not verify signature.
|
|
343
|
+
def is_jwt?(str)
|
|
344
|
+
return false unless str.is_a?(String)
|
|
345
|
+
parts = str.split(".", -1)
|
|
346
|
+
return false unless parts.size == 3
|
|
347
|
+
parts.each do |part|
|
|
348
|
+
return false unless part.match?(/\A[A-Za-z0-9_-]+\z/)
|
|
349
|
+
end
|
|
350
|
+
decoded = parts[0, 2].map do |p|
|
|
351
|
+
pad = 4 - (p.length % 4)
|
|
352
|
+
p = p.tr("-_", "+/") + ("=" * pad) if pad != 4
|
|
353
|
+
::Base64.decode64(p)
|
|
354
|
+
rescue ArgumentError
|
|
355
|
+
return false
|
|
356
|
+
end
|
|
357
|
+
::JSON.parse(decoded[0])
|
|
358
|
+
::JSON.parse(decoded[1])
|
|
359
|
+
true
|
|
360
|
+
rescue ::JSON::ParserError, ArgumentError, TypeError, Encoding::InvalidByteSequenceError
|
|
361
|
+
false
|
|
362
|
+
end
|
|
363
|
+
|
|
215
364
|
private
|
|
216
365
|
|
|
366
|
+
def iban_mod97_valid?(s)
|
|
367
|
+
rearranged = s[4..-1] + s[0, 4]
|
|
368
|
+
num_str = rearranged.each_char.map { |c| c =~ /\A[A-Z]\z/ ? (c.ord - 55).to_s : c }.join
|
|
369
|
+
remainder = 0
|
|
370
|
+
num_str.scan(/.{1,7}/) do |chunk|
|
|
371
|
+
remainder = (remainder.to_s + chunk).to_i % 97
|
|
372
|
+
end
|
|
373
|
+
remainder == 1
|
|
374
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError
|
|
375
|
+
false
|
|
376
|
+
end
|
|
377
|
+
|
|
217
378
|
def luhn_valid?(digits)
|
|
218
379
|
sum = 0
|
|
219
380
|
digits.reverse.chars.each_with_index do |c, i|
|
data/lib/string_validator.rb
CHANGED
|
@@ -10,9 +10,14 @@ module StringValidator
|
|
|
10
10
|
|
|
11
11
|
class Error < StandardError; end
|
|
12
12
|
class NotStringError < Error; end
|
|
13
|
+
class InvalidValidatorError < Error; end
|
|
13
14
|
|
|
14
15
|
def self.valid?(str, validator_name, **options)
|
|
15
16
|
raise NotStringError, "input must be a String" unless str.is_a?(String)
|
|
16
|
-
|
|
17
|
+
name = validator_name.to_sym
|
|
18
|
+
raise InvalidValidatorError, "unknown validator: #{name.inspect}" unless respond_to?(name, true)
|
|
19
|
+
public_send(name, str, **options)
|
|
20
|
+
rescue NoMethodError => e
|
|
21
|
+
raise InvalidValidatorError, "unknown validator: #{validator_name.inspect} (#{e.message})"
|
|
17
22
|
end
|
|
18
23
|
end
|