dialing_sorcerer 1.0.1-x64-mingw-ucrt
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 +7 -0
- data/.rubocop_todo.yml +65 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +62 -0
- data/LICENSE.txt +21 -0
- data/README.md +255 -0
- data/Rakefile +53 -0
- data/lib/dialing_sorcerer/3.4/dialing_sorcerer.so +0 -0
- data/lib/dialing_sorcerer/phone_number.rb +324 -0
- data/lib/dialing_sorcerer/rspec_matchers.rb +147 -0
- data/lib/dialing_sorcerer/version.rb +14 -0
- data/lib/dialing_sorcerer.rb +43 -0
- data/sig/dialing_sorcerer.rbs +83 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: febc8d8def1a361406aec325b3ee3473bc1887db7c5ea1db1ff0b21357a025a7
|
|
4
|
+
data.tar.gz: f7d7be5fc06276da9ebde885877015a3c2b9832ddc623d09d1798c212de2d60c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: acb19d9fc899192562801383251077f669b9cc1d35ff62f938bde65e439a42dcd9b27084ddf91feea0ad1b9df43e8cc677c0d791fe7f4e35e23e3d721a5fa506
|
|
7
|
+
data.tar.gz: 69729032e463adb192811aa244da836237761f35b5b9860a83a6da73986e1aba678c3f8e1c13db44bb85e543e6f07c4f592fe91ec757ba2b731cfd36d47ed85c
|
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
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]
|
|
Binary file
|
|
@@ -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: x64-mingw-ucrt
|
|
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.so
|
|
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: []
|