datacheck-kit 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8a90ccd789ae768ec83c93e6d061133bcf35792d6637a0654e2ca5465aa4835
4
+ data.tar.gz: 37eac4ab3994c91ae600d070cefa36b08928fc160603fac8d94c2d677ce5fdcb
5
+ SHA512:
6
+ metadata.gz: 490776a1f4fce252c8e1036cbf61bac7eae69ad4e32f2535063a21d4d2e271380514a8786f74d465d99dd34880930e019f1a74fec39eb39ee0144a6a763e7637
7
+ data.tar.gz: '0792e1b61db14431d1317ca15eac3ff73928c90935113077b23db87991818a75e28c2286b1154c68a56506c1c46b1dea4f88e1d04d60e60da446c5b655de3b92'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # datacheck-kit
2
+
3
+ A small, zero-dependency toolkit for validating and formatting the kinds of
4
+ data that show up in almost every form or import script: email addresses,
5
+ phone numbers, URLs, IBANs, credit card numbers, postal codes and slugs.
6
+
7
+ It exists because most projects end up copy-pasting the same handful of
8
+ regexes and checksum functions over and over. This gem collects the ones
9
+ worth reusing into one place, with tests and no runtime dependencies.
10
+
11
+ This is a Ruby port of [datacheck-kit](https://www.npmjs.com/package/datacheck-kit)
12
+ (originally written for npm, also on [PyPI](https://pypi.org/project/datacheck-kit/)
13
+ and [Packagist](https://packagist.org/packages/galvaonten-coder/datacheck-kit)) — same
14
+ functionality, adapted to Ruby conventions (`?`-suffixed predicates, `nil`
15
+ instead of other languages' null-likes).
16
+
17
+ Homepage: https://luckymisters.uk
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ gem install datacheck-kit
23
+ ```
24
+
25
+ Or add to your Gemfile:
26
+
27
+ ```ruby
28
+ gem 'datacheck-kit'
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```ruby
34
+ require 'datacheck-kit'
35
+
36
+ DataCheckKit::Email.valid?('user@example.com') # true
37
+ DataCheckKit::Email.normalize(' User@Example.COM ') # 'user@example.com'
38
+
39
+ DataCheckKit::Url.valid?('example.com') # false (no protocol)
40
+ DataCheckKit::Url.normalize('example.com/path/') # 'https://example.com/path/'
41
+
42
+ DataCheckKit::Phone.valid?('+44 20 7123 4567') # true
43
+ DataCheckKit::Phone.format_e164('+44 20 7123 4567') # '+442071234567'
44
+
45
+ DataCheckKit::Iban.valid?('NL91ABNA0417164300') # true
46
+ DataCheckKit::Iban.format('NL91ABNA0417164300') # 'NL91 ABNA 0417 1643 00'
47
+
48
+ DataCheckKit::CreditCard.valid?('4111 1111 1111 1111') # true (Luhn check)
49
+ DataCheckKit::CreditCard.mask('4111111111111111') # '**** **** **** 1111'
50
+
51
+ DataCheckKit::PostalCode.valid?('1011 AB', 'NL') # true
52
+ DataCheckKit::PostalCode.supported_countries # ['AT', 'CH', 'DE', ...]
53
+
54
+ DataCheckKit::Slug.make('Café Müller & Söhne') # 'cafe-muller-sohne'
55
+ ```
56
+
57
+ ## API
58
+
59
+ - `DataCheckKit::Email.valid?`, `.normalize`
60
+ - `DataCheckKit::Url.valid?`, `.normalize`
61
+ - `DataCheckKit::Phone.valid?`, `.format_e164` — E.164-style numbers
62
+ - `DataCheckKit::Iban.valid?`, `.format` — mod-97 (ISO 7064) checksum
63
+ - `DataCheckKit::CreditCard.valid?`, `.mask` — Luhn algorithm
64
+ - `DataCheckKit::PostalCode.valid?`, `.supported_countries` — NL, PL, DE, FR, IT, AT, CH, GB, ES, FI, US
65
+ - `DataCheckKit::Slug.make`
66
+
67
+ ## Testing
68
+
69
+ ```bash
70
+ bundle install
71
+ rake test
72
+ ```
73
+
74
+ ## License
75
+
76
+ MIT — see [LICENSE.txt](./LICENSE.txt).
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataCheckKit
4
+ module CreditCard
5
+ DIGITS_PATTERN = /\A\d{12,19}\z/.freeze
6
+
7
+ # Validates a credit/debit card number using the Luhn checksum algorithm.
8
+ def self.valid?(value)
9
+ return false unless value.is_a?(String)
10
+
11
+ digits = value.gsub(/[\s-]/, '')
12
+ return false unless digits =~ DIGITS_PATTERN
13
+
14
+ sum = 0
15
+ should_double = false
16
+ digits.reverse.each_char do |ch|
17
+ digit = ch.to_i
18
+ if should_double
19
+ digit *= 2
20
+ digit -= 9 if digit > 9
21
+ end
22
+ sum += digit
23
+ should_double = !should_double
24
+ end
25
+ (sum % 10).zero?
26
+ end
27
+
28
+ # Masks a card number, keeping only the last 4 digits visible,
29
+ # e.g. "4111111111111111" -> "**** **** **** 1111".
30
+ def self.mask(value)
31
+ return nil unless value.is_a?(String)
32
+
33
+ digits = value.gsub(/[\s-]/, '')
34
+ return nil unless digits =~ DIGITS_PATTERN
35
+
36
+ last4 = digits[-4..]
37
+ masked_length = digits.length - 4
38
+
39
+ groups = []
40
+ i = 0
41
+ while i < masked_length
42
+ n = [4, masked_length - i].min
43
+ groups << ('*' * n)
44
+ i += n
45
+ end
46
+ groups << last4
47
+
48
+ groups.join(' ')
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataCheckKit
4
+ module Email
5
+ # Pragmatic (not fully RFC 5322-compliant) email check that catches the
6
+ # mistakes that actually show up in real-world form input.
7
+ PATTERN = /\A[^\s@]+@[^\s@]+\.[^\s@]{2,}\z/.freeze
8
+
9
+ # Checks whether +value+ looks like a valid email address.
10
+ def self.valid?(value)
11
+ return false unless value.is_a?(String)
12
+
13
+ trimmed = value.strip
14
+ return false if trimmed.empty? || trimmed.length > 254
15
+ return false if trimmed.include?('..')
16
+
17
+ !!(trimmed =~ PATTERN)
18
+ end
19
+
20
+ # Normalizes an email address: trims whitespace and lower-cases it.
21
+ # Returns +nil+ for input that isn't a plausible email.
22
+ def self.normalize(value)
23
+ return nil unless valid?(value)
24
+
25
+ value.strip.downcase
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataCheckKit
4
+ module Iban
5
+ # Expected IBAN length per country, ISO 3166-1 alpha-2 keyed.
6
+ LENGTHS = {
7
+ 'AT' => 20, 'BE' => 16, 'CH' => 21, 'DE' => 22, 'ES' => 24, 'FI' => 18, 'FR' => 27,
8
+ 'GB' => 22, 'IT' => 27, 'NL' => 18, 'PL' => 28, 'PT' => 25, 'IE' => 22, 'LU' => 20
9
+ }.freeze
10
+
11
+ SHAPE_PATTERN = /\A[A-Z]{2}\d{2}[A-Z0-9]+\z/.freeze
12
+
13
+ # Validates an IBAN using the standard mod-97 checksum (ISO 7064).
14
+ def self.valid?(value)
15
+ return false unless value.is_a?(String)
16
+
17
+ iban = value.gsub(/\s+/, '').upcase
18
+ return false unless iban =~ SHAPE_PATTERN
19
+
20
+ country = iban[0, 2]
21
+ expected_length = LENGTHS[country]
22
+ return false if expected_length && iban.length != expected_length
23
+
24
+ # Move the first 4 characters to the end, then convert letters to
25
+ # numbers (A=10, B=11, ... Z=35) and check the result mod 97 == 1.
26
+ rearranged = iban[4..] + iban[0, 4]
27
+ numeric = rearranged.each_char.map { |c| c =~ /[A-Z]/ ? (c.ord - 55).to_s : c }.join
28
+
29
+ numeric.to_i % 97 == 1
30
+ end
31
+
32
+ # Formats an IBAN into groups of 4 characters for display,
33
+ # e.g. "NL91ABNA0417164300" -> "NL91 ABNA 0417 1643 00".
34
+ def self.format(value)
35
+ iban = value.gsub(/\s+/, '').upcase
36
+ iban.scan(/.{1,4}/).join(' ')
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataCheckKit
4
+ module Phone
5
+ SEPARATORS = /[\s\-().]/.freeze
6
+ E164_PATTERN = /\A\+?[1-9]\d{7,14}\z/.freeze
7
+
8
+ # Checks whether +value+ is a plausible international phone number
9
+ # (E.164-style: optional leading +, 8-15 digits).
10
+ def self.valid?(value)
11
+ return false unless value.is_a?(String)
12
+
13
+ digits = value.gsub(SEPARATORS, '')
14
+ !!(digits =~ E164_PATTERN)
15
+ end
16
+
17
+ # Formats a phone number into E.164 form (e.g. "+442071234567").
18
+ # Assumes the number already includes a country code.
19
+ # Returns +nil+ if the input isn't a plausible phone number.
20
+ def self.format_e164(value)
21
+ return nil unless valid?(value)
22
+
23
+ digits = value.gsub(SEPARATORS, '')
24
+ digits.start_with?('+') ? digits : "+#{digits}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataCheckKit
4
+ module PostalCode
5
+ # A pragmatic (not exhaustive) set of postal code patterns for a
6
+ # handful of European countries.
7
+ PATTERNS = {
8
+ 'NL' => /\A\d{4}\s?[A-Z]{2}\z/i,
9
+ 'PL' => /\A\d{2}-\d{3}\z/,
10
+ 'DE' => /\A\d{5}\z/,
11
+ 'FR' => /\A\d{5}\z/,
12
+ 'IT' => /\A\d{5}\z/,
13
+ 'AT' => /\A\d{4}\z/,
14
+ 'CH' => /\A\d{4}\z/,
15
+ 'GB' => /\A[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}\z/i,
16
+ 'ES' => /\A\d{5}\z/,
17
+ 'FI' => /\A\d{5}\z/,
18
+ 'US' => /\A\d{5}(-\d{4})?\z/
19
+ }.freeze
20
+
21
+ # Validates a postal/ZIP code against the pattern for a given
22
+ # ISO 3166-1 alpha-2 country code.
23
+ def self.valid?(value, country_code)
24
+ return false unless value.is_a?(String) && country_code.is_a?(String)
25
+
26
+ pattern = PATTERNS[country_code.upcase]
27
+ return false unless pattern
28
+
29
+ !!(value.strip =~ pattern)
30
+ end
31
+
32
+ # Returns the list of country codes this module has a pattern for.
33
+ def self.supported_countries
34
+ PATTERNS.keys
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataCheckKit
4
+ module Slug
5
+ # Basic Latin diacritic map, enough for common Western/Northern European text.
6
+ DIACRITICS = {
7
+ 'á' => 'a', 'à' => 'a', 'â' => 'a', 'ä' => 'a', 'ã' => 'a', 'å' => 'a',
8
+ 'é' => 'e', 'è' => 'e', 'ê' => 'e', 'ë' => 'e',
9
+ 'í' => 'i', 'ì' => 'i', 'î' => 'i', 'ï' => 'i',
10
+ 'ó' => 'o', 'ò' => 'o', 'ô' => 'o', 'ö' => 'o', 'õ' => 'o',
11
+ 'ú' => 'u', 'ù' => 'u', 'û' => 'u', 'ü' => 'u',
12
+ 'ñ' => 'n', 'ç' => 'c', 'ý' => 'y', 'ß' => 'ss'
13
+ }.freeze
14
+
15
+ # Converts a string into a URL-friendly slug: lower-cases, strips
16
+ # diacritics, and replaces non-alphanumeric runs with a single hyphen.
17
+ def self.make(value)
18
+ return '' unless value.is_a?(String)
19
+
20
+ lowered = value.downcase
21
+ deaccented = lowered.each_char.map { |c| DIACRITICS[c] || c }.join
22
+
23
+ collapsed = deaccented.gsub(/[^a-z0-9]+/, '-')
24
+ collapsed.gsub(/\A-+|-+\z/, '')
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module DataCheckKit
6
+ module Url
7
+ # Checks whether +value+ is a syntactically valid http(s) URL.
8
+ def self.valid?(value)
9
+ return false unless value.is_a?(String)
10
+
11
+ trimmed = value.strip
12
+ return false if trimmed.empty?
13
+
14
+ uri = begin
15
+ URI.parse(trimmed)
16
+ rescue URI::InvalidURIError
17
+ nil
18
+ end
19
+ return false if uri.nil?
20
+
21
+ %w[http https].include?(uri.scheme) && !uri.host.nil?
22
+ end
23
+
24
+ # Normalizes a URL: adds https:// if the protocol is missing,
25
+ # lower-cases the host, and strips a bare trailing slash from the path.
26
+ # Returns +nil+ if the input can't be turned into a valid URL.
27
+ def self.normalize(value)
28
+ return nil unless value.is_a?(String)
29
+
30
+ trimmed = value.strip
31
+ return nil if trimmed.empty?
32
+
33
+ candidate = trimmed =~ %r{\Ahttps?://}i ? trimmed : "https://#{trimmed}"
34
+
35
+ uri = begin
36
+ URI.parse(candidate)
37
+ rescue URI::InvalidURIError
38
+ nil
39
+ end
40
+ return nil if uri.nil? || !%w[http https].include?(uri.scheme) || uri.host.nil?
41
+
42
+ uri.host = uri.host.downcase
43
+ uri.path = '' if uri.path == '/' && uri.query.nil? && uri.fragment.nil?
44
+
45
+ uri.to_s
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'datacheck-kit/email'
4
+ require_relative 'datacheck-kit/url'
5
+ require_relative 'datacheck-kit/phone'
6
+ require_relative 'datacheck-kit/iban'
7
+ require_relative 'datacheck-kit/credit_card'
8
+ require_relative 'datacheck-kit/postal_code'
9
+ require_relative 'datacheck-kit/slug'
10
+
11
+ # DataCheckKit is a zero-dependency toolkit for validating and formatting
12
+ # everyday data: emails, phone numbers, URLs, IBANs, credit card numbers,
13
+ # postal codes and slugs.
14
+ module DataCheckKit
15
+ VERSION = '1.0.0'
16
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: datacheck-kit
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Malboro
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ description: Validates and formats emails, phone numbers, URLs, IBANs, credit card
27
+ numbers, postal codes and slugs — no runtime dependencies.
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - LICENSE.txt
33
+ - README.md
34
+ - lib/datacheck-kit.rb
35
+ - lib/datacheck-kit/credit_card.rb
36
+ - lib/datacheck-kit/email.rb
37
+ - lib/datacheck-kit/iban.rb
38
+ - lib/datacheck-kit/phone.rb
39
+ - lib/datacheck-kit/postal_code.rb
40
+ - lib/datacheck-kit/slug.rb
41
+ - lib/datacheck-kit/url.rb
42
+ homepage: https://luckymisters.uk
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ homepage_uri: https://luckymisters.uk
47
+ source_code_uri: https://github.com/galvaonten-coder/datacheck-kit-ruby
48
+ rubygems_mfa_required: 'true'
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '2.7'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 4.0.16
64
+ specification_version: 4
65
+ summary: Zero-dependency toolkit for validating and formatting everyday data
66
+ test_files: []