parse-stack-next 4.5.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 +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "model"
|
|
5
|
+
|
|
6
|
+
# Try to load phonelib for enhanced validation
|
|
7
|
+
begin
|
|
8
|
+
require "phonelib"
|
|
9
|
+
PHONELIB_AVAILABLE = true
|
|
10
|
+
rescue LoadError
|
|
11
|
+
PHONELIB_AVAILABLE = false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module Parse
|
|
15
|
+
# This class provides E.164 phone number validation and formatting for Parse properties.
|
|
16
|
+
# E.164 is the international telephone numbering format that ensures worldwide uniqueness.
|
|
17
|
+
#
|
|
18
|
+
# Format: +[country code][subscriber number]
|
|
19
|
+
# - Must start with +
|
|
20
|
+
# - Country code: 1-3 digits (cannot start with 0)
|
|
21
|
+
# - Subscriber number: remaining digits
|
|
22
|
+
# - Total length: 8-15 digits (including country code)
|
|
23
|
+
#
|
|
24
|
+
# == Enhanced Validation with phonelib
|
|
25
|
+
#
|
|
26
|
+
# For comprehensive phone number validation (including carrier validation, number type
|
|
27
|
+
# detection, and accurate country-specific rules), add the `phonelib` gem to your Gemfile:
|
|
28
|
+
#
|
|
29
|
+
# gem 'phonelib'
|
|
30
|
+
#
|
|
31
|
+
# When phonelib is available, Parse::Phone will use Google's libphonenumber data for:
|
|
32
|
+
# - Accurate validation for all countries and territories
|
|
33
|
+
# - Number type detection (mobile, landline, toll-free, etc.)
|
|
34
|
+
# - Carrier information
|
|
35
|
+
# - Proper formatting per country standards
|
|
36
|
+
#
|
|
37
|
+
# Without phonelib, basic E.164 format validation is used (sufficient for most use cases).
|
|
38
|
+
#
|
|
39
|
+
# @example Basic usage
|
|
40
|
+
# class Contact < Parse::Object
|
|
41
|
+
# property :mobile, :phone
|
|
42
|
+
# property :work_phone, :phone, required: true
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# contact = Contact.new
|
|
46
|
+
# contact.mobile = "+14155551234"
|
|
47
|
+
# contact.mobile.valid? # => true
|
|
48
|
+
# contact.mobile.country_code # => "1"
|
|
49
|
+
# contact.mobile.national # => "4155551234"
|
|
50
|
+
#
|
|
51
|
+
# contact.mobile = "invalid"
|
|
52
|
+
# contact.mobile.valid? # => false
|
|
53
|
+
#
|
|
54
|
+
# contact.mobile = "+1 (415) 555-1234" # Automatically cleaned
|
|
55
|
+
# contact.mobile.to_s # => "+14155551234"
|
|
56
|
+
#
|
|
57
|
+
# @example With phonelib (enhanced features)
|
|
58
|
+
# phone = Parse::Phone.new("+14155551234")
|
|
59
|
+
# phone.phone_type # => :mobile (requires phonelib)
|
|
60
|
+
# phone.carrier # => "Verizon" (requires phonelib)
|
|
61
|
+
# phone.possible? # => true (quick check, requires phonelib)
|
|
62
|
+
#
|
|
63
|
+
# @version 3.0.0
|
|
64
|
+
class Phone
|
|
65
|
+
# E.164 format regex (strict validation for fallback mode)
|
|
66
|
+
# - Starts with +
|
|
67
|
+
# - Country code: 1-3 digits, cannot start with 0
|
|
68
|
+
# - Total digits: 8-15 (E.164 max is 15 digits total including country code)
|
|
69
|
+
E164_REGEX = /\A\+[1-9]\d{6,14}\z/
|
|
70
|
+
|
|
71
|
+
# Regex to strip non-digit characters (except +)
|
|
72
|
+
STRIP_NON_DIGITS = /[^\d+]/
|
|
73
|
+
|
|
74
|
+
class << self
|
|
75
|
+
# Check if phonelib is available for enhanced validation
|
|
76
|
+
# @return [Boolean] true if phonelib gem is loaded
|
|
77
|
+
def phonelib_available?
|
|
78
|
+
PHONELIB_AVAILABLE
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Type casting support for Parse properties.
|
|
82
|
+
# This allows the property system to convert values to Phone instances.
|
|
83
|
+
#
|
|
84
|
+
# @param value [Object] the value to typecast
|
|
85
|
+
# @return [Parse::Phone, nil] the Phone instance or nil
|
|
86
|
+
# @api private
|
|
87
|
+
def typecast(value)
|
|
88
|
+
return nil if value.nil?
|
|
89
|
+
return value if value.is_a?(Parse::Phone)
|
|
90
|
+
Parse::Phone.new(value)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [String] the raw input value
|
|
95
|
+
attr_reader :raw
|
|
96
|
+
|
|
97
|
+
# @return [String] the normalized E.164 formatted number (or nil if invalid input)
|
|
98
|
+
attr_reader :number
|
|
99
|
+
|
|
100
|
+
# Creates a new Phone instance.
|
|
101
|
+
#
|
|
102
|
+
# @overload new(number)
|
|
103
|
+
# @param number [String] a phone number (will be normalized to E.164)
|
|
104
|
+
# @return [Parse::Phone]
|
|
105
|
+
# @overload new(phone)
|
|
106
|
+
# @param phone [Parse::Phone] another Phone instance to copy
|
|
107
|
+
# @return [Parse::Phone]
|
|
108
|
+
#
|
|
109
|
+
# @example
|
|
110
|
+
# Parse::Phone.new("+14155551234")
|
|
111
|
+
# Parse::Phone.new("1-415-555-1234") # Will add + prefix
|
|
112
|
+
# Parse::Phone.new("+1 (415) 555-1234") # Will clean formatting
|
|
113
|
+
def initialize(value)
|
|
114
|
+
@raw = nil
|
|
115
|
+
@number = nil
|
|
116
|
+
@phonelib_phone = nil
|
|
117
|
+
|
|
118
|
+
if value.is_a?(String)
|
|
119
|
+
@raw = value
|
|
120
|
+
@number = normalize(value)
|
|
121
|
+
elsif value.is_a?(Parse::Phone)
|
|
122
|
+
@raw = value.raw
|
|
123
|
+
@number = value.number
|
|
124
|
+
elsif value.respond_to?(:to_s) && !value.nil?
|
|
125
|
+
@raw = value.to_s
|
|
126
|
+
@number = normalize(@raw)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Parse with phonelib if available
|
|
130
|
+
@phonelib_phone = Phonelib.parse(@number) if PHONELIB_AVAILABLE && @number
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Normalize a phone number string to E.164 format.
|
|
134
|
+
# Removes all non-digit characters except leading +.
|
|
135
|
+
#
|
|
136
|
+
# @param value [String] the phone number string
|
|
137
|
+
# @return [String, nil] the normalized number or nil if invalid
|
|
138
|
+
def normalize(value)
|
|
139
|
+
return nil if value.blank?
|
|
140
|
+
|
|
141
|
+
# Remove all non-digit characters except +
|
|
142
|
+
cleaned = value.to_s.gsub(STRIP_NON_DIGITS, "")
|
|
143
|
+
|
|
144
|
+
# If it doesn't start with +, add it
|
|
145
|
+
cleaned = "+#{cleaned}" unless cleaned.start_with?("+")
|
|
146
|
+
|
|
147
|
+
# Return the cleaned value (may still be invalid, but we store it)
|
|
148
|
+
cleaned
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @return [String, nil] the E.164 formatted phone number
|
|
152
|
+
def to_s
|
|
153
|
+
@number
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @return [String, nil] the E.164 formatted phone number for JSON serialization
|
|
157
|
+
def as_json(*args)
|
|
158
|
+
@number
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Check if this phone number is valid E.164 format.
|
|
162
|
+
# When phonelib is available, uses comprehensive validation.
|
|
163
|
+
# Otherwise, uses basic E.164 regex validation.
|
|
164
|
+
#
|
|
165
|
+
# @return [Boolean] true if the phone number is valid
|
|
166
|
+
#
|
|
167
|
+
# @example
|
|
168
|
+
# Parse::Phone.new("+14155551234").valid? # => true
|
|
169
|
+
# Parse::Phone.new("invalid").valid? # => false
|
|
170
|
+
# Parse::Phone.new("+1").valid? # => false (too short)
|
|
171
|
+
def valid?
|
|
172
|
+
return false if @number.blank?
|
|
173
|
+
|
|
174
|
+
if PHONELIB_AVAILABLE && @phonelib_phone
|
|
175
|
+
@phonelib_phone.valid?
|
|
176
|
+
else
|
|
177
|
+
E164_REGEX.match?(@number)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Check if the phone number is possibly valid (quick check).
|
|
182
|
+
# This is faster than full validation and useful for input feedback.
|
|
183
|
+
# Falls back to valid? when phonelib is not available.
|
|
184
|
+
#
|
|
185
|
+
# @return [Boolean] true if the number could be valid
|
|
186
|
+
def possible?
|
|
187
|
+
return false if @number.blank?
|
|
188
|
+
|
|
189
|
+
if PHONELIB_AVAILABLE && @phonelib_phone
|
|
190
|
+
@phonelib_phone.possible?
|
|
191
|
+
else
|
|
192
|
+
valid?
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Check if the phone number is invalid.
|
|
197
|
+
#
|
|
198
|
+
# @return [Boolean] true if the phone number is definitely invalid
|
|
199
|
+
def invalid?
|
|
200
|
+
!valid?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Get the country code portion of the phone number.
|
|
204
|
+
#
|
|
205
|
+
# @return [String, nil] the country code (without +) or nil if invalid
|
|
206
|
+
#
|
|
207
|
+
# @example
|
|
208
|
+
# Parse::Phone.new("+14155551234").country_code # => "1"
|
|
209
|
+
# Parse::Phone.new("+442071234567").country_code # => "44"
|
|
210
|
+
def country_code
|
|
211
|
+
return nil unless valid?
|
|
212
|
+
|
|
213
|
+
if PHONELIB_AVAILABLE && @phonelib_phone
|
|
214
|
+
@phonelib_phone.country_code
|
|
215
|
+
else
|
|
216
|
+
extract_country_code_fallback
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Get the two-letter ISO country code.
|
|
221
|
+
# Requires phonelib for accurate detection.
|
|
222
|
+
#
|
|
223
|
+
# @return [String, nil] the ISO 3166-1 alpha-2 country code (e.g., "US", "GB")
|
|
224
|
+
#
|
|
225
|
+
# @example
|
|
226
|
+
# Parse::Phone.new("+14155551234").country # => "US" (with phonelib)
|
|
227
|
+
def country
|
|
228
|
+
return nil unless PHONELIB_AVAILABLE && @phonelib_phone&.valid?
|
|
229
|
+
@phonelib_phone.country
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Get the national (subscriber) number without country code.
|
|
233
|
+
#
|
|
234
|
+
# @return [String, nil] the national number or nil if invalid
|
|
235
|
+
#
|
|
236
|
+
# @example
|
|
237
|
+
# Parse::Phone.new("+14155551234").national # => "4155551234"
|
|
238
|
+
def national
|
|
239
|
+
return nil unless valid?
|
|
240
|
+
|
|
241
|
+
if PHONELIB_AVAILABLE && @phonelib_phone
|
|
242
|
+
@phonelib_phone.national(false)&.gsub(/\D/, "")
|
|
243
|
+
else
|
|
244
|
+
cc = country_code
|
|
245
|
+
return nil unless cc
|
|
246
|
+
@number[(cc.length + 1)..] # Skip + and country code
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Get the phone number type (mobile, landline, etc.).
|
|
251
|
+
# Requires phonelib for type detection.
|
|
252
|
+
#
|
|
253
|
+
# @return [Symbol, nil] the number type (:mobile, :fixed_line, :toll_free, etc.)
|
|
254
|
+
#
|
|
255
|
+
# @example
|
|
256
|
+
# Parse::Phone.new("+14155551234").phone_type # => :mobile (with phonelib)
|
|
257
|
+
def phone_type
|
|
258
|
+
return nil unless PHONELIB_AVAILABLE && @phonelib_phone&.valid?
|
|
259
|
+
types = @phonelib_phone.types
|
|
260
|
+
types.first if types.any?
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Check if this is a mobile phone number.
|
|
264
|
+
# Requires phonelib for accurate detection.
|
|
265
|
+
#
|
|
266
|
+
# @return [Boolean, nil] true if mobile, false if not, nil if unknown
|
|
267
|
+
def mobile?
|
|
268
|
+
type = phone_type
|
|
269
|
+
return nil if type.nil?
|
|
270
|
+
[:mobile, :fixed_or_mobile].include?(type)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Get the carrier name for this phone number.
|
|
274
|
+
# Requires phonelib and may not be available for all numbers.
|
|
275
|
+
#
|
|
276
|
+
# @return [String, nil] the carrier name or nil
|
|
277
|
+
def carrier
|
|
278
|
+
return nil unless PHONELIB_AVAILABLE && @phonelib_phone&.valid?
|
|
279
|
+
@phonelib_phone.carrier
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Get the geographic area for this phone number.
|
|
283
|
+
# Requires phonelib and may not be available for mobile numbers.
|
|
284
|
+
#
|
|
285
|
+
# @return [String, nil] the geographic area or nil
|
|
286
|
+
def geo_name
|
|
287
|
+
return nil unless PHONELIB_AVAILABLE && @phonelib_phone&.valid?
|
|
288
|
+
@phonelib_phone.geo_name
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Get the country/region name for this phone number's country code.
|
|
292
|
+
#
|
|
293
|
+
# @return [String, nil] the country/region name or nil if unknown
|
|
294
|
+
#
|
|
295
|
+
# @example
|
|
296
|
+
# Parse::Phone.new("+14155551234").country_name # => "United States"
|
|
297
|
+
# Parse::Phone.new("+442071234567").country_name # => "United Kingdom"
|
|
298
|
+
def country_name
|
|
299
|
+
if PHONELIB_AVAILABLE && @phonelib_phone&.valid?
|
|
300
|
+
iso_code = @phonelib_phone.country
|
|
301
|
+
ISO_COUNTRY_NAMES[iso_code] if iso_code
|
|
302
|
+
else
|
|
303
|
+
cc = country_code
|
|
304
|
+
FALLBACK_COUNTRY_NAMES[cc] if cc
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Format the phone number for display.
|
|
309
|
+
# When phonelib is available, uses proper country-specific formatting.
|
|
310
|
+
# Otherwise, provides basic formatted version.
|
|
311
|
+
#
|
|
312
|
+
# @param format [Symbol] :international (default), :national, or :e164
|
|
313
|
+
# @return [String, nil] formatted number or nil if invalid
|
|
314
|
+
#
|
|
315
|
+
# @example
|
|
316
|
+
# Parse::Phone.new("+14155551234").formatted # => "+1 415-555-1234"
|
|
317
|
+
# Parse::Phone.new("+14155551234").formatted(:national) # => "(415) 555-1234"
|
|
318
|
+
def formatted(format = :international)
|
|
319
|
+
return nil unless valid?
|
|
320
|
+
|
|
321
|
+
if PHONELIB_AVAILABLE && @phonelib_phone
|
|
322
|
+
case format
|
|
323
|
+
when :national
|
|
324
|
+
@phonelib_phone.national
|
|
325
|
+
when :e164
|
|
326
|
+
@phonelib_phone.e164
|
|
327
|
+
else
|
|
328
|
+
@phonelib_phone.international
|
|
329
|
+
end
|
|
330
|
+
else
|
|
331
|
+
format_fallback
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Check equality with another phone number.
|
|
336
|
+
#
|
|
337
|
+
# @param other [Parse::Phone, String] the other phone number
|
|
338
|
+
# @return [Boolean] true if the numbers are equal
|
|
339
|
+
def ==(other)
|
|
340
|
+
if other.is_a?(Parse::Phone)
|
|
341
|
+
@number == other.number
|
|
342
|
+
elsif other.is_a?(String)
|
|
343
|
+
@number == normalize(other)
|
|
344
|
+
else
|
|
345
|
+
false
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# @return [Boolean] true if the phone number is blank/nil
|
|
350
|
+
def blank?
|
|
351
|
+
@number.blank?
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# @return [Boolean] true if the phone number is present
|
|
355
|
+
def present?
|
|
356
|
+
!blank?
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Get validation errors for this phone number.
|
|
360
|
+
# Useful for providing user feedback.
|
|
361
|
+
#
|
|
362
|
+
# @return [Array<String>] array of error messages
|
|
363
|
+
def errors
|
|
364
|
+
return [] if valid?
|
|
365
|
+
return ["Phone number is required"] if @number.blank?
|
|
366
|
+
|
|
367
|
+
if PHONELIB_AVAILABLE && @phonelib_phone
|
|
368
|
+
result = []
|
|
369
|
+
# Phonelib uses impossible? for basic length/format check
|
|
370
|
+
if @phonelib_phone.impossible?
|
|
371
|
+
sanitized = @phonelib_phone.sanitized
|
|
372
|
+
result << "Phone number is too short" if sanitized.length < 7
|
|
373
|
+
result << "Phone number is too long" if sanitized.length > 15
|
|
374
|
+
end
|
|
375
|
+
result << "Invalid phone number format" if result.empty?
|
|
376
|
+
result
|
|
377
|
+
else
|
|
378
|
+
["Invalid E.164 phone number format"]
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
private
|
|
383
|
+
|
|
384
|
+
# ISO 3166-1 alpha-2 country codes to names (for phonelib mode)
|
|
385
|
+
ISO_COUNTRY_NAMES = {
|
|
386
|
+
"AF" => "Afghanistan", "AL" => "Albania", "DZ" => "Algeria",
|
|
387
|
+
"AR" => "Argentina", "AU" => "Australia", "AT" => "Austria",
|
|
388
|
+
"BE" => "Belgium", "BR" => "Brazil", "CA" => "Canada",
|
|
389
|
+
"CL" => "Chile", "CN" => "China", "CO" => "Colombia",
|
|
390
|
+
"CZ" => "Czech Republic", "DK" => "Denmark", "EG" => "Egypt",
|
|
391
|
+
"FI" => "Finland", "FR" => "France", "DE" => "Germany",
|
|
392
|
+
"GR" => "Greece", "HU" => "Hungary", "IN" => "India",
|
|
393
|
+
"ID" => "Indonesia", "IR" => "Iran", "IE" => "Ireland",
|
|
394
|
+
"IL" => "Israel", "IT" => "Italy", "JP" => "Japan",
|
|
395
|
+
"KZ" => "Kazakhstan", "KE" => "Kenya", "MY" => "Malaysia",
|
|
396
|
+
"MX" => "Mexico", "MA" => "Morocco", "MM" => "Myanmar",
|
|
397
|
+
"NL" => "Netherlands", "NZ" => "New Zealand", "NG" => "Nigeria",
|
|
398
|
+
"NO" => "Norway", "PK" => "Pakistan", "PH" => "Philippines",
|
|
399
|
+
"PL" => "Poland", "PT" => "Portugal", "RO" => "Romania",
|
|
400
|
+
"RU" => "Russia", "SA" => "Saudi Arabia", "SG" => "Singapore",
|
|
401
|
+
"SK" => "Slovakia", "ZA" => "South Africa", "KR" => "South Korea",
|
|
402
|
+
"ES" => "Spain", "LK" => "Sri Lanka", "SE" => "Sweden",
|
|
403
|
+
"CH" => "Switzerland", "TH" => "Thailand", "TN" => "Tunisia",
|
|
404
|
+
"TR" => "Turkey", "AE" => "UAE", "GB" => "United Kingdom",
|
|
405
|
+
"US" => "United States", "VN" => "Vietnam",
|
|
406
|
+
}.freeze
|
|
407
|
+
|
|
408
|
+
# Fallback country code extraction when phonelib is not available
|
|
409
|
+
FALLBACK_COUNTRY_CODES = %w[
|
|
410
|
+
1 7 20 27 30 31 32 33 34 36 39 40 41 43 44 45 46 47 48 49
|
|
411
|
+
51 52 53 54 55 56 57 58 60 61 62 63 64 65 66 81 82 84 86
|
|
412
|
+
90 91 92 93 94 95 98 212 213 216 218 220 221 222 223 224
|
|
413
|
+
225 226 227 228 229 230 231 232 233 234 235 236 237 238
|
|
414
|
+
239 240 241 242 243 244 245 246 247 248 249 250 251 252
|
|
415
|
+
253 254 255 256 257 258 260 261 262 263 264 265 266 267
|
|
416
|
+
268 269 290 291 297 298 299 350 351 352 353 354 355 356
|
|
417
|
+
357 358 359 370 371 372 373 374 375 376 377 378 379 380
|
|
418
|
+
381 382 383 385 386 387 389 420 421 423 500 501 502 503
|
|
419
|
+
504 505 506 507 508 509 590 591 592 593 594 595 596 597
|
|
420
|
+
598 599 670 672 673 674 675 676 677 678 679 680 681 682
|
|
421
|
+
683 685 686 687 688 689 690 691 692 850 852 853 855 856
|
|
422
|
+
880 886 960 961 962 963 964 965 966 967 968 970 971 972
|
|
423
|
+
973 974 975 976 977 992 993 994 995 996 998
|
|
424
|
+
].freeze
|
|
425
|
+
|
|
426
|
+
# Fallback country names for basic validation mode
|
|
427
|
+
FALLBACK_COUNTRY_NAMES = {
|
|
428
|
+
"1" => "North America",
|
|
429
|
+
"7" => "Russia/Kazakhstan",
|
|
430
|
+
"20" => "Egypt",
|
|
431
|
+
"27" => "South Africa",
|
|
432
|
+
"30" => "Greece",
|
|
433
|
+
"31" => "Netherlands",
|
|
434
|
+
"32" => "Belgium",
|
|
435
|
+
"33" => "France",
|
|
436
|
+
"34" => "Spain",
|
|
437
|
+
"36" => "Hungary",
|
|
438
|
+
"39" => "Italy",
|
|
439
|
+
"40" => "Romania",
|
|
440
|
+
"41" => "Switzerland",
|
|
441
|
+
"43" => "Austria",
|
|
442
|
+
"44" => "United Kingdom",
|
|
443
|
+
"45" => "Denmark",
|
|
444
|
+
"46" => "Sweden",
|
|
445
|
+
"47" => "Norway",
|
|
446
|
+
"48" => "Poland",
|
|
447
|
+
"49" => "Germany",
|
|
448
|
+
"52" => "Mexico",
|
|
449
|
+
"54" => "Argentina",
|
|
450
|
+
"55" => "Brazil",
|
|
451
|
+
"56" => "Chile",
|
|
452
|
+
"57" => "Colombia",
|
|
453
|
+
"60" => "Malaysia",
|
|
454
|
+
"61" => "Australia",
|
|
455
|
+
"62" => "Indonesia",
|
|
456
|
+
"63" => "Philippines",
|
|
457
|
+
"64" => "New Zealand",
|
|
458
|
+
"65" => "Singapore",
|
|
459
|
+
"66" => "Thailand",
|
|
460
|
+
"81" => "Japan",
|
|
461
|
+
"82" => "South Korea",
|
|
462
|
+
"84" => "Vietnam",
|
|
463
|
+
"86" => "China",
|
|
464
|
+
"90" => "Turkey",
|
|
465
|
+
"91" => "India",
|
|
466
|
+
"92" => "Pakistan",
|
|
467
|
+
"93" => "Afghanistan",
|
|
468
|
+
"94" => "Sri Lanka",
|
|
469
|
+
"95" => "Myanmar",
|
|
470
|
+
"98" => "Iran",
|
|
471
|
+
"212" => "Morocco",
|
|
472
|
+
"213" => "Algeria",
|
|
473
|
+
"216" => "Tunisia",
|
|
474
|
+
"234" => "Nigeria",
|
|
475
|
+
"254" => "Kenya",
|
|
476
|
+
"351" => "Portugal",
|
|
477
|
+
"353" => "Ireland",
|
|
478
|
+
"358" => "Finland",
|
|
479
|
+
"420" => "Czech Republic",
|
|
480
|
+
"421" => "Slovakia",
|
|
481
|
+
"966" => "Saudi Arabia",
|
|
482
|
+
"971" => "UAE",
|
|
483
|
+
"972" => "Israel",
|
|
484
|
+
}.freeze
|
|
485
|
+
|
|
486
|
+
def extract_country_code_fallback
|
|
487
|
+
return nil unless @number&.start_with?("+")
|
|
488
|
+
digits = @number[1..]
|
|
489
|
+
|
|
490
|
+
# Try longest match first (3-digit codes)
|
|
491
|
+
[3, 2, 1].each do |len|
|
|
492
|
+
code = digits[0, len]
|
|
493
|
+
return code if FALLBACK_COUNTRY_CODES.include?(code)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Default to first digit for unknown codes
|
|
497
|
+
digits[0, 1]
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def format_fallback
|
|
501
|
+
cc = country_code
|
|
502
|
+
nat = national
|
|
503
|
+
return @number unless cc && nat
|
|
504
|
+
|
|
505
|
+
case cc
|
|
506
|
+
when "1" # North America: +1 415-555-1234
|
|
507
|
+
if nat.length == 10
|
|
508
|
+
"+#{cc} #{nat[0, 3]}-#{nat[3, 3]}-#{nat[6, 4]}"
|
|
509
|
+
else
|
|
510
|
+
@number
|
|
511
|
+
end
|
|
512
|
+
when "44" # UK: +44 20 7123 4567
|
|
513
|
+
"+#{cc} #{nat[0, 2]} #{nat[2, 4]} #{nat[6..]}"
|
|
514
|
+
else
|
|
515
|
+
# Generic: +CC NNNNNNNNNN
|
|
516
|
+
"+#{cc} #{nat}"
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
end
|