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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5a08752d7f73d74fa45b4dec5d17815e71bb547c30a1364540923bb10e5649a
4
- data.tar.gz: 75c37d3612370ccefa9504d2b466b0a06a9e618363c2a6d0bd53df43663f4136
3
+ metadata.gz: ab042a16d657aecd6e9eef8521ae05d391ae43b16e2a1abcb3846849856c5ffe
4
+ data.tar.gz: 3caf3d2eab31fd3db17e89597b9d320fb6a10e9278057212f7007d00653279d4
5
5
  SHA512:
6
- metadata.gz: 192ae47895b814e8b8bbbb268009e8f9a6ed77135e8718f6948758fb92eb25bd46317d59ba3b40e04c1c14e44774aeb47e4081cbbeede870063ba51d3213b360
7
- data.tar.gz: 1da02180de5a833956a6ae3a6aae356bc5518240f9a9452a981fb0e4f0adf92a15e61dd30d83975e8d7bc0b01eb0784ce3cf1a5d9f32c851c4a731b3e670a4e0
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
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025
3
+ Copyright (c) 2026
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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("&lt;tag&gt;") # => "<tag>"
98
108
  ```
@@ -126,13 +136,21 @@ end
126
136
 
127
137
  ### Safe validation helper
128
138
 
129
- Raises `StringValidator::NotStringError` if the input is not a String:
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.gsub(/\A[#{Regexp.escape(chars)}]+|[#{Regexp.escape(chars)}]+\z/, "")
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.each_char.select { |c| chars.include?(c) }.join
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("&quot;", '"')
78
94
  .gsub("&#x27;", "'")
79
95
  .gsub("&#x2F;", "/")
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
- return false
33
- end
34
- true
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 = ignore_case ? seed.downcase : seed
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|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StringValidator
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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
- public_send(validator_name, str, **options)
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: string_validator
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
  - Raj Panchal