dialing_sorcerer 1.0.1-x86_64-darwin

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: f117fe11c90ff4dd3e2b8ac00e7bf80ecaffa84255009827730f16ffc19deb17
4
+ data.tar.gz: 77099ace1146cdd9c1bdbababf7bc10e64d50604efaed8df9d9d37742605de8c
5
+ SHA512:
6
+ metadata.gz: 28b0a188646c9058f95adecefea4b770b07899eef65e9fa4263588b5849cf2e12ebbb2aa813d888ecdf32d42ac38a0553bc788ba7efa54c8889775daaf6c553c
7
+ data.tar.gz: 0b9383cb8d809385922fc6efc909a9413b7677c776d7cff1732c1bb946dcbf3f82a7f35ad9b67cf0c8e87e9cef29b2dbd65b2411f56fcfd9b230a02651bd3208
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,65 @@
1
+ Metrics/AbcSize:
2
+ Max: 40
3
+
4
+ Metrics/MethodLength:
5
+ Max: 30
6
+
7
+ Metrics/ClassLength:
8
+ Max: 350
9
+
10
+ Metrics/ModuleLength:
11
+ Max: 400
12
+
13
+ Metrics/CyclomaticComplexity:
14
+ Max: 15
15
+
16
+ Metrics/PerceivedComplexity:
17
+ Max: 15
18
+
19
+ #---------------------------------------------------------
20
+
21
+ # Allow adding Gems to Gemfile without descriptive comment
22
+ Bundler/GemComment:
23
+ Enabled: false
24
+
25
+ # Ignore missing Copyright notice
26
+ Style/Copyright:
27
+ Enabled: false
28
+
29
+ # Do not complain about missing else clauses
30
+ Style/MissingElse:
31
+ Enabled: false
32
+
33
+ # Allow non symbol hash keys
34
+ Style/StringHashKeys:
35
+ Enabled: false
36
+
37
+ # False positive for save_screenshot method of appium used in Yealink driver
38
+ Lint/Debugger:
39
+ Enabled: false
40
+
41
+ # Disabled to allow rubocop disable directives in source code
42
+ Style/DisableCopsWithinSourceCodeDirective:
43
+ Enabled: false
44
+
45
+ # Disabled to allow passing an option hash instead of keyword parameters
46
+ Style/OptionHash:
47
+ Enabled: false
48
+
49
+ # Disabled to allow OpenStruct conversion of hashes
50
+ Style/OpenStructUse:
51
+ Enabled: false
52
+
53
+ # Disabled to allow formated printout of SIP messagesS
54
+ Style/RedundantFormat:
55
+ Enabled: false
56
+
57
+ # Disabled to allow symbols with numbers for KEYCODE's
58
+ Naming/VariableNumber:
59
+ Enabled: false
60
+
61
+ Naming/PredicateMethod:
62
+ Enabled: false
63
+
64
+ Style/EmptyStringInsideInterpolation:
65
+ Enabled: false
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --type-name-tag generic:Generic
2
+ --default-return void
3
+ --markup markdown
4
+ --markup-provider redcarpet
5
+ --exclude /rbi
6
+ --exclude /sig
data/CHANGELOG.md ADDED
@@ -0,0 +1,62 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [1.0.1] - 2025-09-25
6
+
7
+ ### 🚀 Features
8
+
9
+ - 05063d0 - Added rspec matcher for string input [@armando]
10
+
11
+
12
+ ### 🌀 Miscellaneous Tasks
13
+
14
+ - 9222b90 - Added changelog [@armando]
15
+
16
+ - c7b95e6 - Bump version 1.0.1 [@armando]
17
+
18
+
19
+ ## [1.0.0] - 2025-09-25
20
+
21
+ ### 🚀 Features
22
+
23
+ - 0da4e74 - Added initial setup [@armando]
24
+
25
+ - *(release)* ac150b9 - Pre release preparation [@armando]
26
+
27
+
28
+ ### 🐛 Bug Fixes
29
+
30
+ - *(doc)* 044cf53 - Fixed doc generation with addition of git [@armando]
31
+
32
+ - 7a0b720 - Project url [@armando]
33
+
34
+
35
+ ### 📝 Documentation
36
+
37
+ - 61ef7ab - Added yard docs as gitlab pac [@armando]
38
+
39
+ - dc65f38 - Improved docs [@armando]
40
+
41
+ - 1018107 - Added link to documentation [@armando]
42
+
43
+ - 9e82246 - Url docs documentation [@armando]
44
+
45
+
46
+ ### ⚙️ Testing
47
+
48
+ - 63d05ef - Added benchmarks for parsing [@armando]
49
+
50
+
51
+ ### 🌀 Miscellaneous Tasks
52
+
53
+ - *(rspec)* 966fe95 - Added rspec to pipeline [@armando]
54
+
55
+ - *(testing)* 2e340fa - Added additional test coverage [@armando]
56
+
57
+ - cac73ba - Cleared uneeded file [@armando]
58
+
59
+ - 0cb5d51 - Improved error type to use internal only [@armando]
60
+
61
+
62
+ <!-- generated by git-cliff -->
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 armando
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,255 @@
1
+ # DialSorcerer
2
+
3
+ A Ruby gem for parsing and formatting phone numbers with international support, powered by Rust.
4
+
5
+ DialSorcerer provides comprehensive phone number parsing, validation, and formatting capabilities. It supports international phone number formats, validation of phone number types (mobile, fixed-line, VOIP, etc.), and conversion between different formatting standards including E.164, national, international, and RFC 3966 formats.
6
+
7
+ ## Features
8
+
9
+ - ✨ **International Support**: Parse and format phone numbers from any country
10
+ - 🔍 **Validation**: Validate phone number types (mobile, fixed-line, VOIP, etc.)
11
+ - 📱 **Multiple Formats**: Support for E.164, national, international, and RFC 3966 formats
12
+ - 🚀 **High Performance**: Rust-powered backend for fast processing
13
+ - 🧪 **RSpec Matchers**: Convenient matchers for testing phone number functionality
14
+ - 🛡️ **Type Safety**: Comprehensive error handling and type classification
15
+ - 🌍 **Carrier Information**: Get carrier details when available
16
+ - 📖 **Documentation**: [https://avengers.pages.its-telekom.eu/dial-sorcerer](https://avengers.pages.its-telekom.eu/dial-sorcerer)
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'dialing_sorcerer'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ ```bash
29
+ bundle install
30
+ ```
31
+
32
+ Or install it yourself as:
33
+
34
+ ```bash
35
+ gem install dialing_sorcerer
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### Basic Parsing
41
+
42
+ ```ruby
43
+ require 'dialing_sorcerer'
44
+
45
+ # Parse an international number
46
+ phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
47
+ puts phone.to_e164 # => "+493012345678"
48
+ puts phone.to_national # => "030 12345678"
49
+ puts phone.to_international # => "+49 30 12345678"
50
+ puts phone.country # => "DE"
51
+ puts phone.type # => "FixedLine"
52
+
53
+ # Parse a national number with country code
54
+ phone = DialSorcerer::PhoneNumber.parse("030 12345678", "DE")
55
+ puts phone.country_code # => 49
56
+ ```
57
+
58
+ ### Safe Parsing
59
+
60
+ Use `try_parse` for error handling without exceptions:
61
+
62
+ ```ruby
63
+ result = DialSorcerer::PhoneNumber.try_parse("invalid number")
64
+
65
+ if result.success?
66
+ puts result.phone_number.to_e164
67
+ else
68
+ puts "Error: #{result.error} (#{result.error_type})"
69
+ end
70
+ ```
71
+
72
+ ### Phone Number Types
73
+
74
+ DialSorcerer can identify various phone number types:
75
+
76
+ ```ruby
77
+ mobile = DialSorcerer::PhoneNumber.parse("+49 151 12345678")
78
+ puts mobile.type # => "Mobile"
79
+
80
+ landline = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
81
+ puts landline.type # => "FixedLine"
82
+
83
+ voip = DialSorcerer::PhoneNumber.parse("+49 32 12345678")
84
+ puts voip.type # => "Voip"
85
+ ```
86
+
87
+ ### Formatting Options
88
+
89
+ ```ruby
90
+ phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
91
+
92
+ # Different output formats
93
+ puts phone.to_e164 # => "+493012345678"
94
+ puts phone.to_e164(zero_leading: true) # => "00493012345678"
95
+ puts phone.to_national # => "030 12345678"
96
+ puts phone.to_international # => "+49 30 12345678"
97
+ puts phone.to_rfc3966 # => "tel:+49-30-12345678"
98
+ puts phone.national_number # => "3012345678"
99
+ ```
100
+
101
+ ### Phone Number Comparison
102
+
103
+ ```ruby
104
+ phone1 = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
105
+ phone2 = DialSorcerer::PhoneNumber.parse("030 12345678", "DE")
106
+
107
+ # Strict equality
108
+ puts phone1 == phone2 # => true
109
+
110
+ # Flexible matching (matches various formats)
111
+ puts phone1.equal?("+493012345678") # => true
112
+ puts phone1.equal?("030 12345678") # => true
113
+ puts phone1.equal?("+49 30 12345678") # => true
114
+ ```
115
+
116
+ ### Error Handling
117
+
118
+ ```ruby
119
+ begin
120
+ phone = DialSorcerer::PhoneNumber.parse("123")
121
+ rescue DialSorcerer::ParseError => e
122
+ puts "Failed to parse: #{e.message}"
123
+ end
124
+
125
+ # Or use safe parsing
126
+ result = DialSorcerer::PhoneNumber.try_parse("123")
127
+ puts result.error_type if !result.success? # => :too_short, :too_long, :invalid_country, or :invalid_format
128
+ ```
129
+
130
+ ## Testing with RSpec
131
+
132
+ DialSorcerer includes convenient RSpec matchers:
133
+
134
+ ```ruby
135
+ require 'dialing_sorcerer/rspec_matchers'
136
+
137
+ RSpec.describe "Phone number validation" do
138
+ include DialSorcerer::PhoneNumberMatchers
139
+
140
+ it "validates mobile numbers" do
141
+ phone = DialSorcerer::PhoneNumber.parse("+49 151 12345678")
142
+ expect(phone).to be_mobile
143
+ end
144
+
145
+ it "validates VOIP numbers" do
146
+ voip = DialSorcerer::PhoneNumber.parse("+49 32 12345678")
147
+ expect(voip).to be_voip
148
+ end
149
+
150
+ it "matches MSISDN strings" do
151
+ phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
152
+ expect(phone).to match_msisdn("+493012345678")
153
+ expect(phone).to match_msisdn("030 12345678")
154
+ end
155
+ end
156
+ ```
157
+
158
+ ## API Reference
159
+
160
+ ### DialSorcerer::PhoneNumber
161
+
162
+ #### Class Methods
163
+
164
+ - `parse(number, country = nil, default_country: 'DE')` - Parse a phone number, raises ParseError on failure
165
+ - `try_parse(number, country = nil, default_country: nil)` - Safe parsing, returns ParseResult object
166
+
167
+ #### Instance Methods
168
+
169
+ - `valid?` - Check if phone number is valid
170
+ - `to_e164(zero_leading: false)` - Format as E.164
171
+ - `to_national` - Format as national number
172
+ - `to_international` - Format as international number
173
+ - `to_rfc3966` - Format according to RFC 3966
174
+ - `national_number` - Get national number without country code
175
+ - `country_code` - Get numeric country calling code
176
+ - `country` - Get ISO country code (2-letter)
177
+ - `type` - Get phone number type
178
+ - `carrier` - Get carrier name (when available)
179
+ - `==(other)` - Strict equality comparison
180
+ - `equal?(other)` - Flexible format matching
181
+
182
+ ### Phone Number Types
183
+
184
+ Supported phone number types:
185
+ - `FixedLine` - Traditional landline numbers
186
+ - `Mobile` - Mobile/cellular numbers
187
+ - `FixedLineOrMobile` - Numbers that could be either
188
+ - `TollFree` - Toll-free numbers
189
+ - `PremiumRate` - Premium rate numbers
190
+ - `SharedCost` - Shared cost numbers
191
+ - `PersonalNumber` - Personal numbers
192
+ - `Voip` - Voice over IP numbers
193
+ - `Pager` - Pager numbers
194
+ - `Uan` - Universal Access Numbers
195
+ - `Emergency` - Emergency numbers
196
+ - `Voicemail` - Voicemail access numbers
197
+ - `ShortCode` - Short code numbers
198
+ - `StandardRate` - Standard rate numbers
199
+ - `Carrier` - Carrier-specific numbers
200
+ - `NoInternational` - Numbers not available internationally
201
+ - `Unknown` - Unknown type
202
+
203
+ ## Development
204
+
205
+ Run `rake spec` to run the tests. You can also run `bundle exec irb` for an interactive prompt that will allow you to experiment.
206
+
207
+ ### Requirements
208
+
209
+ - Ruby >= 3.4.0
210
+ - RubyGems >= 3.3.11
211
+ - Rust toolchain (for building the native extension)
212
+
213
+ ### Building
214
+
215
+ The gem includes a Rust extension that provides the core phone number parsing functionality. The extension is built automatically during installation using `rb_sys`.
216
+
217
+ ### Running Tests
218
+
219
+ ```bash
220
+ # Run all tests
221
+ bundle exec rake spec
222
+
223
+ # Run specific test file
224
+ bundle exec rspec spec/phone_number_spec.rb
225
+ ```
226
+
227
+ ### Linting
228
+
229
+ ```bash
230
+ # Check code style
231
+ bundle exec rubocop
232
+
233
+ # Auto-fix issues
234
+ bundle exec rubocop -A
235
+ ```
236
+
237
+ ## Contributing
238
+
239
+ Bug reports and pull requests are welcome on GitLab at https://gitlab01.its-telekom.eu/avengers/dial_sorcerer.
240
+
241
+ 1. Fork the repository
242
+ 2. Create your feature branch (`git checkout -b feature/my-new-feature`)
243
+ 3. Make your changes and add tests
244
+ 4. Ensure all tests pass and code follows style guidelines
245
+ 5. Commit your changes (`git commit -am 'Add some feature'`)
246
+ 6. Push to the branch (`git push origin feature/my-new-feature`)
247
+ 7. Create a new Pull Request
248
+
249
+ ## License
250
+
251
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
252
+
253
+ ## Changelog
254
+
255
+ See [CHANGELOG.md](CHANGELOG.md) for version history and changes.
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rb_sys/extensiontask'
5
+ require 'rspec/core/rake_task'
6
+ require 'rubocop/rake_task'
7
+
8
+ RSpec::Core::RakeTask.new(:spec) do |t|
9
+ t.pattern = Dir.glob('spec/**/*_spec.rb')
10
+ t.rspec_opts = '--format RspecSonarqubeFormatter --out test-report.xml --format documentation'
11
+ end
12
+
13
+ RSpec::Core::RakeTask.new(:spec)
14
+
15
+ task build: :compile
16
+
17
+ GEMSPEC = Gem::Specification.load('dialing_sorcerer.gemspec')
18
+ exttask =
19
+ RbSys::ExtensionTask.new('dialing_sorcerer', GEMSPEC) do |ext|
20
+ ext.lib_dir = 'lib/dialing_sorcerer'
21
+ ext.cross_compile = true
22
+ ext.cross_platform = %w[x64-mingw-ucrt x86_64-linux x86_64-linux-musl x86_64-darwin arm64-darwin]
23
+ end
24
+
25
+ namespace :cargo do
26
+ # Define a Rake task with a description
27
+ desc 'Run cargo fmt to format rust code'
28
+ task :fmt do
29
+ sh 'cargo', 'fmt'
30
+ end
31
+
32
+ desc 'Run cargo test on rust code'
33
+ task :test do
34
+ sh 'cargo test'
35
+ end
36
+ end
37
+
38
+ namespace :compile do
39
+ exttask.cross_platform.each do |plat|
40
+ desc "Build native extension for #{plat} platform (cross compile)"
41
+
42
+ multitask across: plat
43
+ task plat do
44
+ sh 'rb-sys-dock', '-p', plat, '--ruby-versions', '3.4 3.5', '--build'
45
+ end
46
+ end
47
+ end
48
+
49
+ desc 'Build gem but before cross compile rust code'
50
+ task build: :'compile:across'
51
+
52
+ desc 'compile but before run spec and rubocop'
53
+ task default: [:compile, :spec, :rubocop]
@@ -0,0 +1,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dialing_sorcerer'
4
+
5
+ module DialSorcerer
6
+
7
+ # Result object returned by PhoneNumber.try_parse method
8
+ #
9
+ # @example Successful parse
10
+ # result = DialSorcerer::PhoneNumber.try_parse("+49 30 12345678")
11
+ # if result.success?
12
+ # puts result.phone_number.to_e164
13
+ # end
14
+ #
15
+ # @example Failed parse
16
+ # result = DialSorcerer::PhoneNumber.try_parse("invalid")
17
+ # unless result.success?
18
+ # puts "Error: #{result.error} (#{result.error_type})"
19
+ # end
20
+ #
21
+ # @attr [Boolean] success? whether the parsing was successful
22
+ # @attr [PhoneNumber, nil] phone_number the parsed phone number object (nil if parsing failed)
23
+ # @attr [String, nil] error the error message (nil if parsing succeeded)
24
+ # @attr [Symbol, nil] error_type the classified error type (nil if parsing succeeded)
25
+ ParseResult =
26
+ Struct.new(:success?, :phone_number, :error, :error_type) do
27
+ alias_method :success?, :success?
28
+ end
29
+
30
+ # Represents a parsed and validated phone number with various formatting options.
31
+ # This class provides methods to parse, validate, and format phone numbers
32
+ # according to international standards.
33
+ #
34
+ # @example Basic parsing
35
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
36
+ # puts phone.to_e164 # => "+493012345678"
37
+ #
38
+ # @example Parsing with country
39
+ # phone = DialSorcerer::PhoneNumber.parse("030 12345678", "DE")
40
+ # puts phone.country # => "DE"
41
+ #
42
+ # @example Safe parsing
43
+ # result = DialSorcerer::PhoneNumber.try_parse("invalid number")
44
+ # puts result.error if !result.success?
45
+ class PhoneNumber
46
+
47
+ # Parses a phone number string and returns a PhoneNumber object.
48
+ # Raises ParseError if the number is invalid.
49
+ #
50
+ # @param number [String] the phone number to parse
51
+ # @param country [String, nil] the country code for the number (optional)
52
+ # @param default_country [String] the default country code to use ("DE" by default)
53
+ # @return [DialSorcerer::PhoneNumber] the parsed phone number
54
+ # @raise [DialSorcerer::ParseError] if the number cannot be parsed
55
+ #
56
+ # @example Parse international number
57
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
58
+ #
59
+ # @example Parse national number with country
60
+ # phone = DialSorcerer::PhoneNumber.parse("030 12345678", "DE")
61
+ def self.parse(number, country = nil, default_country: 'DE')
62
+ country ||= default_country
63
+ new(DialSorcerer::Internal.parse(number, country&.upcase))
64
+ rescue ArgumentError => e
65
+ raise(DialSorcerer::ParseError, e.message)
66
+ end
67
+
68
+ # Attempts to parse a phone number without raising exceptions.
69
+ # Returns a ParseResult object that indicates success or failure.
70
+ #
71
+ # @param number [String] the phone number to parse
72
+ # @param country [String, nil] the country code for the number (optional)
73
+ # @param default_country [String, nil] the default country code to use
74
+ # @return [ParseResult] result object containing the phone number or error information
75
+ #
76
+ # @example Successful parsing
77
+ # result = DialSorcerer::PhoneNumber.try_parse("+49 30 12345678")
78
+ # puts result.phone_number.to_e164 if result.success?
79
+ #
80
+ # @example Failed parsing
81
+ # result = DialSorcerer::PhoneNumber.try_parse("123")
82
+ # puts result.error unless result.success?
83
+ def self.try_parse(number, country = nil, default_country: nil)
84
+ country ||= default_country
85
+ internal = DialSorcerer::Internal.parse(number, country&.upcase)
86
+ phone = new(internal)
87
+ DialSorcerer::ParseResult.new(true, phone, nil, nil)
88
+ rescue ::ArgumentError => e
89
+ error_type = classify_error(e.message)
90
+ DialSorcerer::ParseResult.new(false, nil, e.message, error_type)
91
+ end
92
+
93
+ # Classifies error messages into specific error types
94
+ # This method analyzes error messages and returns a symbol representing
95
+ # the type of parsing error that occurred.
96
+ #
97
+ # @param message [String] the error message to classify
98
+ # @return [Symbol] the classified error type (:invalid_country, :too_short, :too_long, or :invalid_format)
99
+ #
100
+ # @example Classifying country error
101
+ # DialSorcerer::PhoneNumber.classify_error("Invalid country code") # => :invalid_country
102
+ #
103
+ # @example Classifying length errors
104
+ # DialSorcerer::PhoneNumber.classify_error("Number too short") # => :too_short
105
+ # DialSorcerer::PhoneNumber.classify_error("Number too long") # => :too_long
106
+ #
107
+ # @example Default classification
108
+ # DialSorcerer::PhoneNumber.classify_error("Invalid format") # => :invalid_format
109
+ #
110
+ # @api private
111
+ def self.classify_error(message)
112
+ case message
113
+ when /invalid country/i then :invalid_country
114
+ when /too short/i then :too_short
115
+ when /too long/i then :too_long
116
+ else :invalid_format
117
+ end
118
+ end
119
+
120
+ # Creates a new DialSorcerer::PhoneNumber instance with internal representation
121
+ # @param internal [DialSorcerer::Internal] the internal phone number representation
122
+ # @api private
123
+ def initialize(internal)
124
+ @internal = internal
125
+ end
126
+
127
+ # Checks if the phone number is valid
128
+ #
129
+ # @return [Boolean] true if the phone number is valid
130
+ # @example
131
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
132
+ # phone.valid? # => true
133
+ def valid?
134
+ @internal.valid?
135
+ end
136
+
137
+ # Formats the phone number in E.164 format
138
+ # E.164 formatting has no spaces or decorations, just the country code and number.
139
+ #
140
+ # @param zero_leading [Boolean] whether to use "00" instead of "+" prefix
141
+ # @return [String] the phone number in E.164 format
142
+ #
143
+ # @example Standard E.164 format
144
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
145
+ # phone.to_e164 # => "+493012345678"
146
+ #
147
+ # @example With zero leading
148
+ # phone.to_e164(zero_leading: true) # => "00493012345678"
149
+ def to_e164(zero_leading: false)
150
+ value = @internal.to_e164
151
+ zero_leading ? value.gsub('+', '00') : value
152
+ end
153
+
154
+ # Formats the phone number in national format
155
+ # National formatting has no country code and uses country-dependent formatting.
156
+ #
157
+ # @return [String] the phone number in national format
158
+ # @example
159
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
160
+ # phone.to_national # => "030 12345678"
161
+ def to_national
162
+ @internal.to_national
163
+ end
164
+
165
+ # Formats the phone number in international format
166
+ # International formatting contains country code and uses country-dependent formatting.
167
+ #
168
+ # @return [String] the phone number in international format
169
+ # @example
170
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
171
+ # phone.to_international # => "+49 30 12345678"
172
+ def to_international
173
+ @internal.to_international
174
+ end
175
+
176
+ # Returns the national number without country code
177
+ #
178
+ # @return [String] the national number
179
+ # @example
180
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
181
+ # phone.national_number # => "3012345678"
182
+ def national_number
183
+ @internal.national_number
184
+ end
185
+
186
+ # Formats the phone number according to RFC 3966
187
+ #
188
+ # @return [String] the phone number in RFC 3966 format
189
+ # @example
190
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
191
+ # phone.to_rfc3966 # => "tel:+49-30-12345678"
192
+ def to_rfc3966
193
+ @internal.to_rfc3966
194
+ end
195
+
196
+ # Returns the country calling code
197
+ #
198
+ # @return [Integer] the country calling code
199
+ # @example
200
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
201
+ # phone.country_code # => 49
202
+ def country_code
203
+ @internal.country_code
204
+ end
205
+
206
+ # Returns the ISO country code
207
+ #
208
+ # @return [String] the two-letter ISO country code
209
+ # @example
210
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
211
+ # phone.country # => "DE"
212
+ def country
213
+ @internal.country
214
+ end
215
+
216
+ # Returns the E.164 representation for debugging
217
+ #
218
+ # @return [String] the phone number in E.164 format
219
+ def inspect
220
+ to_e164
221
+ end
222
+
223
+ # Returns the E.164 representation as string
224
+ #
225
+ # @return [String] the phone number in E.164 format
226
+ def to_s
227
+ to_e164
228
+ end
229
+
230
+ # Returns the type of the phone number
231
+ # Types include: FixedLine, Mobile, FixedLineOrMobile, TollFree, PremiumRate,
232
+ # SharedCost, PersonalNumber, Voip, Pager, Uan, Emergency, Voicemail,
233
+ # ShortCode, StandardRate, Carrier, NoInternational, Unknown
234
+ #
235
+ # @return [String] the phone number type
236
+ #
237
+ # @example Getting phone number type
238
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
239
+ # phone.type # => "FixedLine"
240
+ #
241
+ # @example Mobile number
242
+ # mobile = DialSorcerer::PhoneNumber.parse("+49 170 1234567")
243
+ # mobile.type # => "Mobile"
244
+ def type
245
+ @internal.number_type
246
+ end
247
+
248
+ # Compares two phone numbers for equality based on their national number and country
249
+ # Two phone numbers are considered equal if they have the same national number
250
+ # and the same country code.
251
+ #
252
+ # @param other [PhoneNumber] the phone number to compare with
253
+ # @return [Boolean] true if the phone numbers are equal, false otherwise
254
+ #
255
+ # @example Equal phone numbers
256
+ # phone1 = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
257
+ # phone2 = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
258
+ # phone1 == phone2 # => true
259
+ #
260
+ # @example Different phone numbers
261
+ # phone1 = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
262
+ # phone2 = DialSorcerer::PhoneNumber.parse("+49 30 87654321")
263
+ # phone1 == phone2 # => false
264
+ #
265
+ # @example Different countries
266
+ # phone1 = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
267
+ # phone2 = DialSorcerer::PhoneNumber.parse("+43 30 12345678")
268
+ # phone1 == phone2 # => false
269
+ def ==(other)
270
+ return false unless other.is_a?(self.class)
271
+
272
+ national_number == other.national_number && country == other.country
273
+ end
274
+
275
+ # Checks if this phone number matches the given input in any supported format
276
+ # This method is more flexible than == as it compares against various formatted
277
+ # representations of the phone number, ignoring whitespace differences.
278
+ #
279
+ # @param other [PhoneNumber, String, #to_s] the phone number or string to compare with
280
+ # @return [Boolean] true if the input matches any format of this phone number
281
+ #
282
+ # @example Matching E.164 format
283
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
284
+ # phone.equal?("+493012345678") # => true
285
+ #
286
+ # @example Matching national format
287
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
288
+ # phone.equal?("030 12345678") # => true
289
+ #
290
+ # @example Matching with spaces
291
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
292
+ # phone.equal?("+49 30 12345678") # => true
293
+ #
294
+ # @example Non-matching number
295
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
296
+ # phone.equal?("+49 30 87654321") # => false
297
+ def equal?(other)
298
+ return false unless other.is_a?(self.class) || other.respond_to?(:to_s)
299
+
300
+ other = other.to_s unless other.is_a?(self.class)
301
+ [to_rfc3966, national_number, to_international, to_national, to_e164, to_e164(zero_leading: true)].any? do |v|
302
+ v.gsub(/\s/, '') == other.to_s.gsub(/\s/, '')
303
+ end
304
+ end
305
+
306
+ # Returns the carrier name for the phone number, if available
307
+ # Carrier information may not be available for all phone numbers or regions.
308
+ #
309
+ # @return [String, nil] the carrier name, or nil if not available
310
+ #
311
+ # @example Getting carrier information
312
+ # phone = DialSorcerer::PhoneNumber.parse("+49 170 1234567")
313
+ # phone.carrier # => "T-Mobile"
314
+ #
315
+ # @example When carrier is not available
316
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
317
+ # phone.carrier # => nil
318
+ def carrier
319
+ @internal.carrier
320
+ end
321
+
322
+ end
323
+
324
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DialSorcerer
4
+ # RSpec matchers for testing phone numbers
5
+ # These matchers provide convenient ways to test phone number properties in RSpec tests.
6
+ #
7
+ # @example Using the matchers in RSpec
8
+ # RSpec.describe "Phone number validation" do
9
+ # include DialSorcerer::PhoneNumber::Matchers
10
+ #
11
+ # it "validates mobile numbers" do
12
+ # phone = DialSorcerer::PhoneNumber.parse("+49 151 12345678")
13
+ # expect(phone).to be_mobile
14
+ # end
15
+ #
16
+ # it "validates VOIP numbers" do
17
+ # voip = DialSorcerer::PhoneNumber.parse("+49 32 12345678")
18
+ # expect(voip).to be_voip
19
+ # end
20
+ #
21
+ # it "matches MSISDN strings" do
22
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
23
+ # expect(phone).to match_msisdn("+493012345678")
24
+ # end
25
+ # end
26
+ #
27
+ # @since 0.1.0
28
+ class PhoneNumber
29
+ # Provide rspec matchers for DialSorcerer::PhoneNumber
30
+ module Matchers
31
+
32
+ extend ::RSpec::Matchers::DSL
33
+
34
+ # Validates that the given object is a PhoneNumber instance
35
+ # Raises ArgumentError if the object is not a PhoneNumber
36
+ #
37
+ # @param number [Object] the object to validate
38
+ # @raise [ArgumentError] if the object is not a PhoneNumber
39
+ # @api private
40
+ #
41
+ # @example
42
+ # validate(phone_number) # passes if phone_number is a PhoneNumber
43
+ # validate("string") # raises ArgumentError
44
+ def validate(number)
45
+ return number if number.is_a?(DialSorcerer::PhoneNumber)
46
+
47
+ return DialSorcerer::PhoneNumber.parse(number) if number.is_a?(String)
48
+
49
+ raise(
50
+ DialSorcerer::Error,
51
+ "Expected a DialSorcerer::PhoneNumber object or a compatible String, but got #{number.class} <#{number}>."
52
+ )
53
+ rescue DialSorcerer::ParseError
54
+ raise(
55
+ DialSorcerer::Error,
56
+ "Expected a DialSorcerer::PhoneNumber object or a compatible String, but got #{number.class} <#{number}>."
57
+ )
58
+ end
59
+
60
+ # RSpec matcher to test if a phone number is a VOIP number
61
+ #
62
+ # @example Positive assertion
63
+ # expect(phone).to be_voip
64
+ #
65
+ # @example Negative assertion
66
+ # expect(phone).not_to be_voip
67
+ #
68
+ # @example In a test
69
+ # voip_number = DialSorcerer::PhoneNumber.parse("+49 32 12345678")
70
+ # expect(voip_number).to be_voip
71
+ matcher :be_voip do
72
+ match do |number|
73
+ number = validate(number)
74
+ number.type == 'Voip'
75
+ end
76
+
77
+ failure_message do |number|
78
+ "expected MSISDN \"#{number}\" to be a\"VOIP\" number,\n\t\tbut is a \"#{number.type}\" number."
79
+ end
80
+
81
+ failure_message_when_negated do |number|
82
+ "expected MSISDN \"#{number}\" not to be a \"VOIP\" number,\n\t\tbut is a \"#{number.type}\" number."
83
+ end
84
+ end
85
+
86
+ # RSpec matcher to test if a phone number is a mobile number
87
+ #
88
+ # @example Positive assertion
89
+ # expect(phone).to be_mobile
90
+ #
91
+ # @example Negative assertion
92
+ # expect(phone).not_to be_mobile
93
+ #
94
+ # @example In a test
95
+ # mobile_number = DialSorcerer::PhoneNumber.parse("+49 151 12345678")
96
+ # expect(mobile_number).to be_mobile
97
+ matcher :be_mobile do
98
+ match do |number|
99
+ number = validate(number)
100
+ number.type == 'Mobile'
101
+ end
102
+
103
+ failure_message do |number|
104
+ "expected MSISDN \"#{number}\" to be mobile,\n\t\tbut got \"#{number.type}\"."
105
+ end
106
+
107
+ failure_message_when_negated do |number|
108
+ "expected MSISDN \"#{number}\" not to be mobile,\n\t\tbut got \"#{number.type}\"."
109
+ end
110
+ end
111
+
112
+ # RSpec matcher to test if a phone number matches a given MSISDN string
113
+ # Uses the flexible `equal?` method which checks multiple formats
114
+ #
115
+ # @param expected_number [String, PhoneNumber] the expected number to match against
116
+ #
117
+ # @example Positive assertion with E.164 format
118
+ # expect(phone).to match_msisdn("+493012345678")
119
+ #
120
+ # @example Positive assertion with national format
121
+ # expect(phone).to match_msisdn("030 12345678")
122
+ #
123
+ # @example Negative assertion
124
+ # expect(phone).not_to match_msisdn("+441234567890")
125
+ #
126
+ # @example In a test
127
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
128
+ # expect(phone).to match_msisdn("+493012345678")
129
+ # expect(phone).to match_msisdn("030 12345678")
130
+ matcher :match_msisdn do |expected_number|
131
+ match do |number|
132
+ number = validate(number)
133
+ number.equal?(expected_number)
134
+ end
135
+
136
+ failure_message do |number|
137
+ "expected MSISDN to match \"#{number}\",\n\t\tbut got \"#{expected_number}\"."
138
+ end
139
+
140
+ failure_message_when_negated do |number|
141
+ "expected MSISDN not to match \"#{number}\",\n\t\tbut got \"#{expected_number}\"."
142
+ end
143
+ end
144
+
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DialSorcerer
4
+
5
+ # The current version of the DialSorcerer gem
6
+ #
7
+ # @example Getting the version
8
+ # puts DialSorcerer::VERSION # => "0.1.0"
9
+ #
10
+ # @return [String] the semantic version string
11
+ # @since 0.1.0
12
+ VERSION = '1.0.1'
13
+
14
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dialing_sorcerer/phone_number'
4
+ require_relative 'dialing_sorcerer/version'
5
+
6
+ # DialSorcerer is a Ruby gem for parsing and formatting phone numbers.
7
+ # It provides a simple API for validating phone numbers and converting
8
+ # them between different formats.
9
+ #
10
+ # @example Basic usage
11
+ # phone = DialSorcerer::PhoneNumber.parse("+49 30 12345678")
12
+ # puts phone.to_e164 # => "+493012345678"
13
+ # puts phone.to_national # => "030 12345678"
14
+ #
15
+ # @example With country code
16
+ # phone = DialSorcerer::PhoneNumber.parse("030 12345678", "DE")
17
+ # puts phone.country # => "DE"
18
+ #
19
+ # @author DialSorcerer Team
20
+ # @since 0.1.0
21
+ module DialSorcerer
22
+
23
+ # Base error class for all DialSorcerer exceptions
24
+ #
25
+ # @example Rescuing DialSorcerer errors
26
+ # begin
27
+ # phone = DialSorcerer::PhoneNumber.parse("invalid")
28
+ # rescue DialSorcerer::Error => e
29
+ # puts "An error occurred: #{e.message}"
30
+ # end
31
+ class Error < StandardError; end
32
+
33
+ # Error raised when phone number parsing fails
34
+ #
35
+ # @example Handling parse errors
36
+ # begin
37
+ # phone = DialSorcerer::PhoneNumber.parse("123")
38
+ # rescue DialSorcerer::ParseError => e
39
+ # puts "Failed to parse phone number: #{e.message}"
40
+ # end
41
+ class ParseError < ArgumentError; end
42
+
43
+ end
@@ -0,0 +1,83 @@
1
+ module DialSorcerer
2
+ VERSION: "0.1.0"
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class ParseError < StandardError
8
+ end
9
+
10
+ # Result object returned by PhoneNumber.try_parse method
11
+ class ParseResult < Struct[untyped]
12
+ attr_accessor success?: bool
13
+ attr_accessor phone_number: PhoneNumber?
14
+ attr_accessor error: String?
15
+ attr_accessor error_type: Symbol?
16
+
17
+ def initialize: (bool success?, PhoneNumber? phone_number, String? error, Symbol? error_type) -> void
18
+ end
19
+
20
+ # Represents a parsed and validated phone number with various formatting options
21
+ class PhoneNumber
22
+ # Parses a phone number string and returns a PhoneNumber object
23
+ def self.parse: (String number, ?String? country, ?default_country: String) -> PhoneNumber
24
+
25
+ # Attempts to parse a phone number without raising exceptions
26
+ def self.try_parse: (String number, ?String? country, ?default_country: String?) -> ParseResult
27
+
28
+ # Classifies error messages into specific error types
29
+ def self.classify_error: (String message) -> Symbol
30
+
31
+ def initialize: (untyped internal) -> void
32
+
33
+ # Checks if the phone number is valid
34
+ def valid?: () -> bool
35
+
36
+ # Formats the phone number in E.164 format
37
+ def to_e164: (?zero_leading: bool) -> String
38
+
39
+ # Formats the phone number in national format
40
+ def to_national: () -> String
41
+
42
+ # Formats the phone number in international format
43
+ def to_international: () -> String
44
+
45
+ # Returns the national number without country code
46
+ def national_number: () -> String
47
+
48
+ # Formats the phone number according to RFC 3966
49
+ def to_rfc3966: () -> String
50
+
51
+ # Returns the country calling code
52
+ def country_code: () -> Integer
53
+
54
+ # Returns the ISO country code
55
+ def country: () -> String
56
+
57
+ # Returns the E.164 representation for debugging
58
+ def inspect: () -> String
59
+
60
+ # Returns the E.164 representation as string
61
+ def to_s: () -> String
62
+
63
+ # Returns the phone number type
64
+ def type: () -> String
65
+
66
+ # Compares two phone numbers for equality
67
+ def ==: (untyped other) -> bool
68
+
69
+ # Checks if this phone number matches the given input in any supported format
70
+ def equal?: (untyped other) -> bool
71
+
72
+ # Returns the carrier name for the phone number
73
+ def carrier: () -> String?
74
+ end
75
+
76
+ # RSpec matchers for testing phone numbers
77
+ module PhoneNumberMatchers
78
+ extend RSpec::Matchers::DSL
79
+
80
+ # Validates that the given object is a PhoneNumber instance
81
+ def validate: (untyped number) -> void
82
+ end
83
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dialing_sorcerer
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: x86_64-darwin
6
+ authors:
7
+ - Avengers
8
+ - Mateus
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2025-10-31 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: |
15
+ DialSorcerer is a Ruby gem that provides comprehensive phone number parsing, validation,
16
+ and formatting capabilities. It supports international phone number formats, validation
17
+ of phone number types (mobile, fixed-line, VOIP, etc.), and conversion between different
18
+ formatting standards including E.164, national, international, and RFC 3966 formats.
19
+ The gem also includes RSpec matchers for convenient testing of phone number functionality.
20
+ email:
21
+ - mateus@moveaway.de
22
+ executables: []
23
+ extensions: []
24
+ extra_rdoc_files: []
25
+ files:
26
+ - ".rubocop_todo.yml"
27
+ - ".yardopts"
28
+ - CHANGELOG.md
29
+ - LICENSE.txt
30
+ - README.md
31
+ - Rakefile
32
+ - lib/dialing_sorcerer.rb
33
+ - lib/dialing_sorcerer/3.4/dialing_sorcerer.bundle
34
+ - lib/dialing_sorcerer/phone_number.rb
35
+ - lib/dialing_sorcerer/rspec_matchers.rb
36
+ - lib/dialing_sorcerer/version.rb
37
+ - sig/dialing_sorcerer.rbs
38
+ homepage: https://gitlab.its/avengers/dial-sorcerer
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ homepage_uri: https://gitlab.its/avengers/dial-sorcerer
43
+ source_code_uri: https://gitlab.its/avengers/dial-sorcerer.git
44
+ changelog_uri: https://gitlab.its/avengers/dial-sorcerer/main/CHANGELOG.md
45
+ documentation_uri: https://gitlab.its/dial-sorcerer
46
+ bug_tracker_uri: https://gitlab.its/avengers/dial-sorcerer/issues
47
+ rubygems_mfa_required: 'true'
48
+ post_install_message: |
49
+ Thank you for installing DialSorcerer!
50
+
51
+ This gem provides powerful phone number parsing and formatting capabilities.
52
+ Check out the documentation at: https://rubydoc.info/gems/dialing_sorcerer
53
+
54
+ For examples and usage instructions, visit: https://github.com/armando/dialing_sorcerer
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '3.4'
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: 3.5.dev
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 3.3.11
71
+ requirements: []
72
+ rubygems_version: 3.5.23
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: A Ruby gem for parsing and formatting phone numbers with international support.
76
+ test_files: []