itu-e164 0.1.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: '0469c614c83a4f49532e5f80645b5a44bf33b873c2bb7332852fd909ef421882'
4
+ data.tar.gz: 4ec2f4e91a83688670affd7d08dbe77908d615304783d286018ffe6bac43ad39
5
+ SHA512:
6
+ metadata.gz: a81387d155baa30b4b8878dc7ab5cd9748305e3e102f90409d614b6030f71b0a3b08aa2ac3caedd477925a3c9331970991209dfca84c45fc2708c3bcc28a62f1
7
+ data.tar.gz: b729eb1db3f931748581ba1d24962d2b9abbdd1e8317fa9a3c843ba50073ff1dae4a9a3249a89d90fdbdc008c7344289c2b149759011706667f08fbefdfd1954
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Add strict canonical validation for the geographic-area structure in
6
+ Recommendation ITU-T E.164 (02/2026).
7
+ - Add a dated ITU geographic country-code assignment snapshot.
8
+ - Return immutable, structured results suitable for existing validation layers.
9
+ - Recognize assigned non-geographic E.164 country codes and reject them with an
10
+ explicit profile-mismatch error.
11
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Olisti contributors
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.
22
+
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # itu-e164
2
+
3
+ `itu-e164` is a dependency-free Ruby utility for strict, auditable validation of
4
+ canonical geographic-area numbers against Recommendation ITU-T E.164 (02/2026).
5
+ It is deliberately not a Rails validator and does not contact a carrier.
6
+
7
+ ## Installation
8
+
9
+ Add the gem to your bundle:
10
+
11
+ ```ruby
12
+ gem "itu-e164"
13
+ ```
14
+
15
+ Then run `bundle install`.
16
+
17
+ ## Usage
18
+
19
+ ```ruby
20
+ require "itu/e164"
21
+
22
+ result = ITU::E164.check("+393331234567")
23
+
24
+ result.conformant? # => true
25
+ result.country_code # => "39"
26
+ result.national_number # => "3331234567"
27
+ result.category # => :geographic
28
+ result.standard_version # => "02/2026"
29
+ result.dataset_version # => "itu-t-e164-2026-07-15"
30
+ ```
31
+
32
+ For a boolean:
33
+
34
+ ```ruby
35
+ ITU::E164.conformant?("+393331234567") # => true
36
+ ```
37
+
38
+ For exception-based control flow:
39
+
40
+ ```ruby
41
+ result = ITU::E164.check!("+393331234567")
42
+
43
+ begin
44
+ ITU::E164.check!("0039 333 123 4567")
45
+ rescue ITU::E164::InvalidNumber => error
46
+ error.result.error_codes # => [:invalid_representation]
47
+ end
48
+ ```
49
+
50
+ The accepted representation is intentionally strict: a `String` containing `+`
51
+ followed by ASCII decimal digits. Spaces, punctuation, `00`, URI parameters, and
52
+ extensions are rejected. Normalize user input before this boundary and store
53
+ extensions separately.
54
+
55
+ ## Existing validation layers
56
+
57
+ The result is designed to be consumed without an Active Record dependency:
58
+
59
+ ```ruby
60
+ result = ITU::E164.check(value)
61
+
62
+ unless result.conformant?
63
+ errors.add(:phone_number, result.errors.map(&:message).join(", "))
64
+ end
65
+ ```
66
+
67
+ With Twilio Verify, use this gem before sending a verification:
68
+
69
+ ```ruby
70
+ result = ITU::E164.check(candidate)
71
+ return validation_failure(result.errors) unless result.conformant?
72
+
73
+ twilio_verify(candidate)
74
+ ```
75
+
76
+ The responsibilities stay separate:
77
+
78
+ - `itu-e164` checks canonical representation, E.164 length and field structure,
79
+ and the assigned geographic country-code snapshot.
80
+ - Twilio Verify checks whether a user controls a reachable endpoint.
81
+
82
+ ## Exact scope
83
+
84
+ For `profile: :geographic` (the only profile in 0.1), a conformant result means:
85
+
86
+ - the input is canonical leading-plus notation;
87
+ - the E.164 digit sequence contains at most 15 digits;
88
+ - it starts with an assigned one-, two-, or three-digit geographic country code;
89
+ - at least one digit remains as the national (significant) number.
90
+
91
+ This is the complete E.164-level structure for the geographic category. E.164
92
+ leaves national destination-code and subscriber-number details to national
93
+ numbering authorities.
94
+
95
+ Accordingly, this gem does **not** claim that:
96
+
97
+ - the national number range is allocated or currently active;
98
+ - the number is mobile, SMS-capable, reachable, or owned by a user;
99
+ - a national trunk prefix was correctly removed;
100
+ - one of E.164's seven non-geographic categories satisfies its service-specific
101
+ numbering recommendation.
102
+
103
+ Assigned non-geographic country codes are recognized, but fail the geographic
104
+ profile with `:profile_mismatch`. Spare or unknown codes fail with
105
+ `:country_code_unassigned`.
106
+
107
+ ## Results and errors
108
+
109
+ `ITU::E164.check` always returns an immutable `ITU::E164::Result`. Invalid input
110
+ has one or more immutable `ITU::E164::ValidationError` values:
111
+
112
+ ```ruby
113
+ result = ITU::E164.check("+80012345678")
114
+
115
+ result.conformant? # => false
116
+ result.category # => :global_service
117
+ result.error_codes # => [:profile_mismatch]
118
+ result.to_h
119
+ ```
120
+
121
+ Current error codes are:
122
+
123
+ - `:not_a_string`
124
+ - `:invalid_representation`
125
+ - `:too_long`
126
+ - `:country_code_unassigned`
127
+ - `:missing_national_number`
128
+ - `:profile_mismatch`
129
+
130
+ Branch on error codes, not English messages.
131
+
132
+ ## Versioning and updates
133
+
134
+ The recommendation version and assignment dataset are exposed separately because
135
+ a stable numbering structure can coexist with changing assignments. See
136
+ [SOURCES.md](SOURCES.md) for provenance and the update procedure.
137
+
138
+ ## License
139
+
140
+ The gem is available under the MIT License.
141
+
data/SOURCES.md ADDED
@@ -0,0 +1,46 @@
1
+ # Normative basis and dataset provenance
2
+
3
+ `itu-e164` separates two things that evolve independently:
4
+
5
+ 1. The numbering-plan structure comes from Recommendation ITU-T E.164 (02/2026).
6
+ 2. Country-code assignment status comes from ITU-T service publications and is
7
+ represented by a dated data snapshot.
8
+
9
+ ## Structure
10
+
11
+ - [Recommendation ITU-T E.164 (02/2026)](https://www.itu.int/rec/T-REC-E.164-202602-I/en)
12
+ - [Recommendation ITU-T E.164.1 (02/2026)](https://www.itu.int/rec/T-REC-E.164.1-202602-I/en)
13
+ - [Recommendation ITU-T E.123](https://www.itu.int/rec/T-REC-E.123/en), for the
14
+ leading-plus international notation accepted by this library
15
+
16
+ The implemented geographic profile follows these structural constraints:
17
+
18
+ - an international E.164 number contains decimal digits and has at most 15 digits;
19
+ - a geographic country code has one, two, or three digits;
20
+ - the national (significant) number occupies the remaining digits;
21
+ - international and national prefixes are not part of the number.
22
+
23
+ ## Assignment snapshot
24
+
25
+ - Dataset identifier: `itu-t-e164-2026-07-15`
26
+ - Snapshot date: 15 July 2026
27
+ - [List of Recommendation ITU-T E.164 assigned country codes, baseline at
28
+ 15 December 2016](https://www.itu.int/dms_pub/itu-t/opb/sp/t-sp-e.164d-2016-pdf-e.pdf)
29
+ - Numbered amendments through [ITU Operational Bulletin No. 1344,
30
+ 15 July 2026](https://www.itu.int/pub/T-SP-OB.1344-2026)
31
+ - [ITU National Numbering Plans index](https://www.itu.int/oth/T0202.aspx?lang=en&parent=T0202),
32
+ cross-checked on 28 July 2026
33
+
34
+ The constants are derived assignment facts, not reproduced recommendation text
35
+ or diagrams.
36
+
37
+ ## Updating the snapshot
38
+
39
+ Review each ITU Operational Bulletin after No. 1344 for amendments to the assigned
40
+ country-code list. Apply additions, returns, re-designations, and status changes to
41
+ `lib/itu/e164/data/v2026.rb`, advance `DATASET_VERSION`, and add regression tests
42
+ for every changed code.
43
+
44
+ Do not infer assignment status from a calling library or a carrier API. Assignment
45
+ status is an ITU-controlled dataset.
46
+
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "data/v2026"
4
+ require_relative "result"
5
+ require_relative "validation_error"
6
+
7
+ module ITU
8
+ module E164
9
+ class Checker
10
+ ASCII_INTERNATIONAL_NUMBER = /\A\+[0-9]+\z/
11
+ MAXIMUM_DIGITS = 15
12
+ SUPPORTED_PROFILES = [:geographic].freeze
13
+ ERROR_MESSAGES = {
14
+ not_a_string: "must be a String",
15
+ invalid_representation: "must be + followed by ASCII decimal digits",
16
+ too_long: "must contain at most 15 digits after +",
17
+ country_code_unassigned: "must start with an assigned geographic country code",
18
+ missing_national_number: "must include a national significant number after the country code",
19
+ profile_mismatch: "belongs to a non-geographic E.164 category"
20
+ }.freeze
21
+
22
+ def initialize(profile:)
23
+ unless SUPPORTED_PROFILES.include?(profile)
24
+ raise ArgumentError, "unsupported profile #{profile.inspect}; expected :geographic"
25
+ end
26
+
27
+ @profile = profile
28
+ end
29
+
30
+ def call(input)
31
+ return result(input:, errors: [error(:not_a_string)]) unless input.is_a?(String)
32
+
33
+ stable_input = immutable(input)
34
+ unless ASCII_INTERNATIONAL_NUMBER.match?(stable_input)
35
+ return result(input: stable_input, errors: [error(:invalid_representation)])
36
+ end
37
+
38
+ digits = immutable(stable_input.delete_prefix("+"))
39
+ errors = []
40
+ errors << error(:too_long) if digits.length > MAXIMUM_DIGITS
41
+
42
+ category, country_code = resolve_country_code(digits)
43
+ national_number = resolve_national_number(digits, category, country_code)
44
+
45
+ if category.nil?
46
+ errors << error(:country_code_unassigned)
47
+ elsif category != @profile
48
+ errors << error(:profile_mismatch)
49
+ elsif national_number.empty?
50
+ errors << error(:missing_national_number)
51
+ end
52
+
53
+ result(
54
+ input: stable_input,
55
+ digits:,
56
+ canonical: errors.empty? ? stable_input : nil,
57
+ category:,
58
+ country_code:,
59
+ national_number:,
60
+ errors:
61
+ )
62
+ end
63
+
64
+ private
65
+
66
+ def resolve_country_code(digits)
67
+ geographic_code = Data::V2026::GEOGRAPHIC_COUNTRY_CODES.find do |code|
68
+ digits.start_with?(code)
69
+ end
70
+ return [:geographic, immutable(geographic_code)] if geographic_code
71
+
72
+ non_geographic_code, category = Data::V2026::NON_GEOGRAPHIC_COUNTRY_CODES.find do |code, _|
73
+ digits.start_with?(code)
74
+ end
75
+ return [category, immutable(non_geographic_code)] if non_geographic_code
76
+
77
+ [nil, nil]
78
+ end
79
+
80
+ def resolve_national_number(digits, category, country_code)
81
+ return nil unless category == :geographic
82
+
83
+ immutable(digits.delete_prefix(country_code))
84
+ end
85
+
86
+ def result(input:, errors:, **attributes)
87
+ defaults = {
88
+ input:,
89
+ profile: @profile,
90
+ standard_version: E164::STANDARD_VERSION,
91
+ dataset_version: Data::V2026::DATASET_VERSION,
92
+ digits: nil,
93
+ canonical: nil,
94
+ category: nil,
95
+ country_code: nil,
96
+ national_number: nil
97
+ }
98
+
99
+ Result.new(**defaults, **attributes, errors: errors.freeze)
100
+ end
101
+
102
+ def immutable(value)
103
+ value&.dup&.freeze
104
+ end
105
+
106
+ def error(code)
107
+ ValidationError.new(code:, message: ERROR_MESSAGES.fetch(code))
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ITU
4
+ module E164
5
+ module Data
6
+ module V2026
7
+ DATASET_VERSION = "itu-t-e164-2026-07-15"
8
+
9
+ GEOGRAPHIC_COUNTRY_CODES = %w[
10
+ 1 7
11
+ 20 27 30 31 32 33 34 36 39 40 41 43 44 45 46 47 48 49
12
+ 51 52 53 54 55 56 57 58 60 61 62 63 64 65 66 81 82 84 86
13
+ 90 91 92 93 94 95 98
14
+ 211 212 213 216 218 220 221 222 223 224 225 226 227 228 229
15
+ 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
16
+ 245 246 247 248 249 250 251 252 253 254 255 256 257 258 260
17
+ 261 262 263 264 265 266 267 268 269 290 291 297 298 299
18
+ 350 351 352 353 354 355 356 357 358 359 370 371 372 373 374
19
+ 375 376 377 378 380 381 382 383 385 386 387 389
20
+ 420 421 423
21
+ 500 501 502 503 504 505 506 507 508 509
22
+ 590 591 592 593 594 595 596 597 598 599
23
+ 670 672 673 674 675 676 677 678 679 680 681 682 683 685 686
24
+ 687 688 689 690 691 692
25
+ 850 852 853 855 856 880 886
26
+ 960 961 962 963 964 965 966 967 968 970 971 972 973 974 975
27
+ 976 977 992 993 994 995 996 998
28
+ ].freeze
29
+
30
+ NON_GEOGRAPHIC_COUNTRY_CODES = {
31
+ "800" => :global_service,
32
+ "808" => :global_service,
33
+ "870" => :other_global_service,
34
+ "881" => :global_satellite_service,
35
+ "882" => :network,
36
+ "883" => :iot_m2m,
37
+ "979" => :global_service,
38
+ "991" => :trial
39
+ }.freeze
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ITU
4
+ module E164
5
+ class InvalidNumber < ArgumentError
6
+ attr_reader :result
7
+
8
+ def initialize(result)
9
+ @result = result
10
+ super(result.errors.map(&:message).join(", "))
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ITU
4
+ module E164
5
+ Result = ::Data.define(
6
+ :input,
7
+ :profile,
8
+ :standard_version,
9
+ :dataset_version,
10
+ :digits,
11
+ :canonical,
12
+ :category,
13
+ :country_code,
14
+ :national_number,
15
+ :errors
16
+ ) do
17
+ def conformant?
18
+ errors.empty?
19
+ end
20
+
21
+ def failure?
22
+ !conformant?
23
+ end
24
+
25
+ def error_codes
26
+ errors.map(&:code).freeze
27
+ end
28
+
29
+ def to_h
30
+ {
31
+ input:,
32
+ profile:,
33
+ standard_version:,
34
+ dataset_version:,
35
+ digits:,
36
+ canonical:,
37
+ category:,
38
+ country_code:,
39
+ national_number:,
40
+ conformant: conformant?,
41
+ errors: errors.map(&:to_h)
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ITU
4
+ module E164
5
+ ValidationError = ::Data.define(:code, :message) do
6
+ def to_h
7
+ {code:, message:}
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ITU
4
+ module E164
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/itu/e164.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "e164/checker"
4
+ require_relative "e164/invalid_number"
5
+ require_relative "e164/version"
6
+
7
+ module ITU
8
+ module E164
9
+ STANDARD_VERSION = "02/2026"
10
+ PROFILE = :geographic
11
+
12
+ module_function
13
+
14
+ def check(input, profile: PROFILE)
15
+ Checker.new(profile:).call(input)
16
+ end
17
+
18
+ def check!(input, profile: PROFILE)
19
+ result = check(input, profile:)
20
+ raise InvalidNumber, result unless result.conformant?
21
+
22
+ result
23
+ end
24
+
25
+ def conformant?(input, profile: PROFILE)
26
+ check(input, profile:).conformant?
27
+ end
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: itu-e164
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Maurizio De Magnis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.13'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.64'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '2'
51
+ type: :development
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '1.64'
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '2'
61
+ description: |
62
+ A dependency-free Ruby utility that checks canonical geographic-area numbers
63
+ against Recommendation ITU-T E.164 (02/2026) and a dated ITU country-code
64
+ assignment snapshot.
65
+ email: root@olisti.co
66
+ executables: []
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - CHANGELOG.md
71
+ - LICENSE.txt
72
+ - README.md
73
+ - SOURCES.md
74
+ - lib/itu/e164.rb
75
+ - lib/itu/e164/checker.rb
76
+ - lib/itu/e164/data/v2026.rb
77
+ - lib/itu/e164/invalid_number.rb
78
+ - lib/itu/e164/result.rb
79
+ - lib/itu/e164/validation_error.rb
80
+ - lib/itu/e164/version.rb
81
+ homepage: https://source.olisti.co/olisti.co/itu-e164
82
+ licenses:
83
+ - MIT
84
+ metadata:
85
+ rubygems_mfa_required: 'true'
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '3.2'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.4.10
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Strict ITU-T E.164 (02/2026) geographic-number conformance for Ruby
105
+ test_files: []