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.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.env.sample +112 -0
  4. data/.env.test +10 -0
  5. data/.github/workflows/ruby.yml +36 -0
  6. data/.gitignore +49 -0
  7. data/.ruby-version +1 -0
  8. data/.solargraph.yml +22 -0
  9. data/CHANGELOG.md +5816 -0
  10. data/Gemfile +30 -0
  11. data/Gemfile.lock +175 -0
  12. data/LICENSE.txt +23 -0
  13. data/Makefile +63 -0
  14. data/README.md +5655 -0
  15. data/Rakefile +573 -0
  16. data/bin/console +38 -0
  17. data/bin/parse-console +136 -0
  18. data/bin/server +17 -0
  19. data/bin/setup +7 -0
  20. data/config/parse-config.json +12 -0
  21. data/docs/TEST_SERVER.md +271 -0
  22. data/docs/_config.yml +1 -0
  23. data/docs/mcp_guide.md +3484 -0
  24. data/docs/mongodb_direct_guide.md +1348 -0
  25. data/docs/mongodb_index_optimization_guide.md +631 -0
  26. data/examples/transaction_example.rb +219 -0
  27. data/lib/parse/acl_scope.rb +728 -0
  28. data/lib/parse/agent/cancellation_token.rb +80 -0
  29. data/lib/parse/agent/constraint_translator.rb +480 -0
  30. data/lib/parse/agent/describe.rb +420 -0
  31. data/lib/parse/agent/errors.rb +133 -0
  32. data/lib/parse/agent/mcp_client.rb +557 -0
  33. data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
  34. data/lib/parse/agent/mcp_rack_app.rb +1143 -0
  35. data/lib/parse/agent/mcp_server.rb +376 -0
  36. data/lib/parse/agent/metadata_audit.rb +259 -0
  37. data/lib/parse/agent/metadata_dsl.rb +733 -0
  38. data/lib/parse/agent/metadata_registry.rb +794 -0
  39. data/lib/parse/agent/pipeline_validator.rb +82 -0
  40. data/lib/parse/agent/prompts.rb +351 -0
  41. data/lib/parse/agent/rate_limiter.rb +158 -0
  42. data/lib/parse/agent/relation_graph.rb +162 -0
  43. data/lib/parse/agent/result_formatter.rb +453 -0
  44. data/lib/parse/agent/tools.rb +5489 -0
  45. data/lib/parse/agent.rb +3249 -0
  46. data/lib/parse/api/aggregate.rb +79 -0
  47. data/lib/parse/api/all.rb +26 -0
  48. data/lib/parse/api/analytics.rb +18 -0
  49. data/lib/parse/api/batch.rb +33 -0
  50. data/lib/parse/api/cloud_functions.rb +58 -0
  51. data/lib/parse/api/config.rb +125 -0
  52. data/lib/parse/api/files.rb +29 -0
  53. data/lib/parse/api/hooks.rb +117 -0
  54. data/lib/parse/api/objects.rb +146 -0
  55. data/lib/parse/api/path_segment.rb +75 -0
  56. data/lib/parse/api/push.rb +20 -0
  57. data/lib/parse/api/schema.rb +49 -0
  58. data/lib/parse/api/server.rb +50 -0
  59. data/lib/parse/api/sessions.rb +24 -0
  60. data/lib/parse/api/users.rb +250 -0
  61. data/lib/parse/atlas_search/index_manager.rb +353 -0
  62. data/lib/parse/atlas_search/result.rb +204 -0
  63. data/lib/parse/atlas_search/search_builder.rb +604 -0
  64. data/lib/parse/atlas_search/session.rb +253 -0
  65. data/lib/parse/atlas_search.rb +995 -0
  66. data/lib/parse/client/authentication.rb +97 -0
  67. data/lib/parse/client/batch.rb +234 -0
  68. data/lib/parse/client/body_builder.rb +240 -0
  69. data/lib/parse/client/caching.rb +203 -0
  70. data/lib/parse/client/logging.rb +293 -0
  71. data/lib/parse/client/profiling.rb +181 -0
  72. data/lib/parse/client/protocol.rb +91 -0
  73. data/lib/parse/client/request.rb +233 -0
  74. data/lib/parse/client/response.rb +208 -0
  75. data/lib/parse/client.rb +1104 -0
  76. data/lib/parse/clp_scope.rb +361 -0
  77. data/lib/parse/live_query/circuit_breaker.rb +256 -0
  78. data/lib/parse/live_query/client.rb +1001 -0
  79. data/lib/parse/live_query/configuration.rb +224 -0
  80. data/lib/parse/live_query/event.rb +115 -0
  81. data/lib/parse/live_query/event_queue.rb +272 -0
  82. data/lib/parse/live_query/health_monitor.rb +214 -0
  83. data/lib/parse/live_query/logging.rb +149 -0
  84. data/lib/parse/live_query/subscription.rb +294 -0
  85. data/lib/parse/live_query.rb +163 -0
  86. data/lib/parse/lookup_rewriter.rb +445 -0
  87. data/lib/parse/model/acl.rb +968 -0
  88. data/lib/parse/model/associations/belongs_to.rb +275 -0
  89. data/lib/parse/model/associations/collection_proxy.rb +435 -0
  90. data/lib/parse/model/associations/has_many.rb +597 -0
  91. data/lib/parse/model/associations/has_one.rb +158 -0
  92. data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
  93. data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
  94. data/lib/parse/model/bytes.rb +62 -0
  95. data/lib/parse/model/classes/audience.rb +262 -0
  96. data/lib/parse/model/classes/installation.rb +363 -0
  97. data/lib/parse/model/classes/job_schedule.rb +153 -0
  98. data/lib/parse/model/classes/job_status.rb +264 -0
  99. data/lib/parse/model/classes/product.rb +75 -0
  100. data/lib/parse/model/classes/push_status.rb +263 -0
  101. data/lib/parse/model/classes/role.rb +751 -0
  102. data/lib/parse/model/classes/session.rb +201 -0
  103. data/lib/parse/model/classes/user.rb +943 -0
  104. data/lib/parse/model/clp.rb +544 -0
  105. data/lib/parse/model/core/actions.rb +1268 -0
  106. data/lib/parse/model/core/builder.rb +139 -0
  107. data/lib/parse/model/core/create_lock.rb +386 -0
  108. data/lib/parse/model/core/describe.rb +382 -0
  109. data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
  110. data/lib/parse/model/core/errors.rb +38 -0
  111. data/lib/parse/model/core/fetching.rb +566 -0
  112. data/lib/parse/model/core/field_guards.rb +220 -0
  113. data/lib/parse/model/core/indexing.rb +382 -0
  114. data/lib/parse/model/core/parse_reference.rb +407 -0
  115. data/lib/parse/model/core/properties.rb +809 -0
  116. data/lib/parse/model/core/querying.rb +491 -0
  117. data/lib/parse/model/core/schema.rb +202 -0
  118. data/lib/parse/model/core/search_indexing.rb +174 -0
  119. data/lib/parse/model/date.rb +88 -0
  120. data/lib/parse/model/email.rb +213 -0
  121. data/lib/parse/model/file.rb +527 -0
  122. data/lib/parse/model/geojson.rb +271 -0
  123. data/lib/parse/model/geopoint.rb +261 -0
  124. data/lib/parse/model/model.rb +260 -0
  125. data/lib/parse/model/object.rb +2068 -0
  126. data/lib/parse/model/phone.rb +520 -0
  127. data/lib/parse/model/pointer.rb +443 -0
  128. data/lib/parse/model/polygon.rb +406 -0
  129. data/lib/parse/model/push.rb +975 -0
  130. data/lib/parse/model/shortnames.rb +8 -0
  131. data/lib/parse/model/time_zone.rb +141 -0
  132. data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
  133. data/lib/parse/model/validations.rb +96 -0
  134. data/lib/parse/mongodb.rb +2300 -0
  135. data/lib/parse/pipeline_security.rb +554 -0
  136. data/lib/parse/query/constraint.rb +198 -0
  137. data/lib/parse/query/constraints.rb +3279 -0
  138. data/lib/parse/query/cursor.rb +434 -0
  139. data/lib/parse/query/n_plus_one_detector.rb +445 -0
  140. data/lib/parse/query/operation.rb +104 -0
  141. data/lib/parse/query/ordering.rb +66 -0
  142. data/lib/parse/query.rb +7028 -0
  143. data/lib/parse/schema/index_migrator.rb +291 -0
  144. data/lib/parse/schema/search_index_migrator.rb +289 -0
  145. data/lib/parse/schema.rb +494 -0
  146. data/lib/parse/stack/generators/rails.rb +40 -0
  147. data/lib/parse/stack/generators/templates/model.erb +51 -0
  148. data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
  149. data/lib/parse/stack/generators/templates/model_role.rb +4 -0
  150. data/lib/parse/stack/generators/templates/model_session.rb +4 -0
  151. data/lib/parse/stack/generators/templates/model_user.rb +11 -0
  152. data/lib/parse/stack/generators/templates/parse.rb +12 -0
  153. data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
  154. data/lib/parse/stack/railtie.rb +18 -0
  155. data/lib/parse/stack/tasks.rb +563 -0
  156. data/lib/parse/stack/version.rb +11 -0
  157. data/lib/parse/stack.rb +455 -0
  158. data/lib/parse/two_factor_auth/user_extension.rb +449 -0
  159. data/lib/parse/two_factor_auth.rb +310 -0
  160. data/lib/parse/webhooks/payload.rb +360 -0
  161. data/lib/parse/webhooks/registration.rb +199 -0
  162. data/lib/parse/webhooks/replay_protection.rb +189 -0
  163. data/lib/parse/webhooks.rb +510 -0
  164. data/lib/parse-stack-next.rb +5 -0
  165. data/lib/parse-stack.rb +5 -0
  166. data/parse-stack-next.gemspec +82 -0
  167. data/parse-stack.png +0 -0
  168. data/scripts/debug-ips.js +35 -0
  169. data/scripts/docker/Dockerfile.parse +13 -0
  170. data/scripts/docker/atlas-init.js +284 -0
  171. data/scripts/docker/docker-compose.atlas.yml +76 -0
  172. data/scripts/docker/docker-compose.test.yml +106 -0
  173. data/scripts/docker/mongo-init.js +21 -0
  174. data/scripts/eval_mcp_with_lm_studio.rb +274 -0
  175. data/scripts/start-parse.sh +90 -0
  176. data/scripts/start_mcp_server.rb +78 -0
  177. data/scripts/test_server_connection.rb +82 -0
  178. 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