telnyx 0.0.1

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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +4 -0
  3. data/.github/ISSUE_TEMPLATE.md +5 -0
  4. data/.gitignore +9 -0
  5. data/.rubocop.yml +32 -0
  6. data/.rubocop_todo.yml +50 -0
  7. data/.travis.yml +42 -0
  8. data/CHANGELOG.md +2 -0
  9. data/CONTRIBUTORS +0 -0
  10. data/Gemfile +40 -0
  11. data/Guardfile +8 -0
  12. data/LICENSE +22 -0
  13. data/README.md +173 -0
  14. data/Rakefile +28 -0
  15. data/VERSION +1 -0
  16. data/bin/telnyx-console +16 -0
  17. data/lib/telnyx.rb +151 -0
  18. data/lib/telnyx/api_operations/create.rb +12 -0
  19. data/lib/telnyx/api_operations/delete.rb +13 -0
  20. data/lib/telnyx/api_operations/list.rb +29 -0
  21. data/lib/telnyx/api_operations/nested_resource.rb +63 -0
  22. data/lib/telnyx/api_operations/request.rb +57 -0
  23. data/lib/telnyx/api_operations/save.rb +103 -0
  24. data/lib/telnyx/api_resource.rb +69 -0
  25. data/lib/telnyx/available_phone_number.rb +9 -0
  26. data/lib/telnyx/errors.rb +166 -0
  27. data/lib/telnyx/event.rb +9 -0
  28. data/lib/telnyx/list_object.rb +155 -0
  29. data/lib/telnyx/message.rb +9 -0
  30. data/lib/telnyx/messaging_phone_number.rb +10 -0
  31. data/lib/telnyx/messaging_profile.rb +32 -0
  32. data/lib/telnyx/messaging_sender_id.rb +12 -0
  33. data/lib/telnyx/messaging_short_code.rb +10 -0
  34. data/lib/telnyx/number_order.rb +11 -0
  35. data/lib/telnyx/number_reservation.rb +11 -0
  36. data/lib/telnyx/public_key.rb +7 -0
  37. data/lib/telnyx/singleton_api_resource.rb +24 -0
  38. data/lib/telnyx/telnyx_client.rb +545 -0
  39. data/lib/telnyx/telnyx_object.rb +521 -0
  40. data/lib/telnyx/telnyx_response.rb +50 -0
  41. data/lib/telnyx/util.rb +328 -0
  42. data/lib/telnyx/version.rb +5 -0
  43. data/lib/telnyx/webhook.rb +66 -0
  44. data/telnyx.gemspec +25 -0
  45. data/test/api_stub_helpers.rb +1 -0
  46. data/test/openapi/README.md +9 -0
  47. data/test/telnyx/api_operations_test.rb +85 -0
  48. data/test/telnyx/api_resource_test.rb +293 -0
  49. data/test/telnyx/available_phone_number_test.rb +14 -0
  50. data/test/telnyx/errors_test.rb +23 -0
  51. data/test/telnyx/list_object_test.rb +244 -0
  52. data/test/telnyx/message_test.rb +19 -0
  53. data/test/telnyx/messaging_phone_number_test.rb +33 -0
  54. data/test/telnyx/messaging_profile_test.rb +70 -0
  55. data/test/telnyx/messaging_sender_id_test.rb +46 -0
  56. data/test/telnyx/messaging_short_code_test.rb +33 -0
  57. data/test/telnyx/number_order_test.rb +39 -0
  58. data/test/telnyx/number_reservation_test.rb +12 -0
  59. data/test/telnyx/public_key_test.rb +13 -0
  60. data/test/telnyx/telnyx_client_test.rb +631 -0
  61. data/test/telnyx/telnyx_object_test.rb +497 -0
  62. data/test/telnyx/telnyx_response_test.rb +49 -0
  63. data/test/telnyx/util_test.rb +380 -0
  64. data/test/telnyx/webhook_test.rb +108 -0
  65. data/test/telnyx_mock.rb +78 -0
  66. data/test/telnyx_test.rb +40 -0
  67. data/test/test_data.rb +149 -0
  68. data/test/test_helper.rb +73 -0
  69. metadata +162 -0
data/telnyx.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(::File.join(::File.dirname(__FILE__), "lib"))
4
+
5
+ require "telnyx/version"
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "telnyx"
9
+ s.version = Telnyx::VERSION
10
+ s.required_ruby_version = ">= 2.1.0"
11
+ s.summary = "Ruby bindings for the Telnyx API"
12
+ s.description = "Telnyx. See https://www.telnyx.com for details."
13
+ s.author = "Telnyx"
14
+ s.email = "support@telnyx.com"
15
+ s.homepage = "https://developers.telnyx.com/docs/api/ruby"
16
+ s.license = "MIT"
17
+
18
+ s.add_dependency("faraday", "~> 0.13")
19
+ s.add_dependency("net-http-persistent", "~> 3.0")
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- test/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| ::File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+ end
@@ -0,0 +1 @@
1
+ # frozen_string_literal: true
@@ -0,0 +1,9 @@
1
+ ## Using custom OpenAPI specification and fixtures files
2
+
3
+ You can place custom OpenAPI specification and fixtures files in this
4
+ directory. The files must be in JSON format, and must be named `spec3.json`
5
+ and `fixtures3.json` respectively.
6
+
7
+ If those files are present, the test suite will start its own telnyx-mock
8
+ process on a random available port. In order for this to work, `telnyx-mock`
9
+ must be on the `PATH` in the environment used to run the test suite.
@@ -0,0 +1,85 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ require ::File.expand_path("../../test_helper", __FILE__)
5
+
6
+ module Telnyx
7
+ class ApiOperationsTest < Test::Unit::TestCase
8
+ class UpdateableResource < APIResource
9
+ include Telnyx::APIOperations::Save
10
+
11
+ OBJECT_NAME = "updateableresource".freeze
12
+
13
+ def self.protected_fields
14
+ [:protected]
15
+ end
16
+ end
17
+
18
+ context ".update" do
19
+ should "patch the correct parameters to the resource URL" do
20
+ stub_patch = stub_request(:patch, "#{Telnyx.api_base}/v2/messaging_profiles/id")
21
+ .with(body: { foo: "bar" })
22
+ .to_return(body: JSON.generate(data: { record_type: "messaging_profile", foo: "bar" }))
23
+ resource = MessagingProfile.update("id", foo: "bar")
24
+ assert_requested(stub_patch)
25
+ assert_equal("bar", resource.foo)
26
+ end
27
+
28
+ should "error on protected fields" do
29
+ e = assert_raises do
30
+ UpdateableResource.update("id", protected: "bar")
31
+ end
32
+ assert_equal "Cannot update protected field: protected", e.message
33
+ end
34
+ end
35
+
36
+ context ".nested_resource_class_methods" do
37
+ class MainResource < APIResource
38
+ extend Telnyx::APIOperations::NestedResource
39
+ OBJECT_NAME = "mainresource".freeze
40
+ nested_resource_class_methods :nested,
41
+ operations: %i[create retrieve update delete list]
42
+ end
43
+
44
+ should "define a create method" do
45
+ stub_request(:post, "#{Telnyx.api_base}/v2/mainresources/id/nesteds")
46
+ .with(body: { foo: "bar" })
47
+ .to_return(body: JSON.generate(id: "nested_id", object: "nested", foo: "bar"))
48
+ nested_resource = MainResource.create_nested("id", foo: "bar")
49
+ assert_equal "bar", nested_resource.foo
50
+ end
51
+
52
+ should "define a retrieve method" do
53
+ stub_request(:get, "#{Telnyx.api_base}/v2/mainresources/id/nesteds/nested_id")
54
+ .to_return(body: JSON.generate(id: "nested_id", object: "nested", foo: "bar"))
55
+ nested_resource = MainResource.retrieve_nested("id", "nested_id")
56
+ assert_equal "bar", nested_resource.foo
57
+ end
58
+
59
+ should "define an update method" do
60
+ stub_patch = stub_request(:patch, "#{Telnyx.api_base}/v2/mainresources/id/nesteds/nested_id")
61
+ .with(body: { foo: "baz" })
62
+ .to_return(body: JSON.generate(id: "nested_id", object: "nested", foo: "baz"))
63
+ nested_resource = MainResource.update_nested("id", "nested_id", foo: "baz")
64
+ assert_requested(stub_patch)
65
+ assert_equal "baz", nested_resource.foo
66
+ end
67
+
68
+ should "define a delete method" do
69
+ stub_request(:delete, "#{Telnyx.api_base}/v2/mainresources/id/nesteds/nested_id")
70
+ .to_return(body: JSON.generate(id: "nested_id", object: "nested", deleted: true))
71
+ nested_resource = MainResource.delete_nested("id", "nested_id")
72
+ assert_equal true, nested_resource.deleted
73
+ end
74
+
75
+ should "define a list method" do
76
+ stub_get = stub_request(:get, "#{Telnyx.api_base}/v2/mainresources/id/nesteds")
77
+ .to_return(body: JSON.generate(data: [{ record_type: "foo" }]))
78
+ nested_resources = MainResource.list_nesteds("id")
79
+ assert_requested(stub_get)
80
+ assert nested_resources.is_a?(ListObject)
81
+ assert nested_resources.data.is_a?(Array)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,293 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ require ::File.expand_path("../../test_helper", __FILE__)
5
+
6
+ module Telnyx
7
+ class ApiResourceTest < Test::Unit::TestCase
8
+ should "creating a new APIResource should not fetch over the network" do
9
+ Telnyx::MessagingProfile.new("someid")
10
+ assert_not_requested :get, %r{#{Telnyx.api_base}/.*}
11
+ end
12
+
13
+ should "creating a new APIResource from a hash should not fetch over the network" do
14
+ Telnyx::MessagingProfile.construct_from(id: "foo", record_type: "messaging_profile")
15
+ assert_not_requested :get, %r{#{Telnyx.api_base}/.*}
16
+ end
17
+
18
+ should "setting an attribute should not cause a network request" do
19
+ m = Telnyx::MessagingProfile.new("123")
20
+ m.name = "My New Messaging Profile"
21
+ assert_not_requested :get, %r{#{Telnyx.api_base}/.*}
22
+ assert_not_requested :post, %r{#{Telnyx.api_base}/.*}
23
+ end
24
+
25
+ should "accessing id should not issue a fetch" do
26
+ m = Telnyx::MessagingProfile.new("123")
27
+ m.id
28
+ assert_not_requested :get, "#{Telnyx.api_base}/messaging_profiles/123"
29
+ end
30
+
31
+ should "not specifying api credentials should raise an exception" do
32
+ Telnyx.api_key = nil
33
+ assert_raises Telnyx::AuthenticationError do
34
+ Telnyx::MessagingProfile.new("456").refresh
35
+ end
36
+ end
37
+
38
+ should "using a nil api key should raise an exception" do
39
+ assert_raises TypeError do
40
+ Telnyx::MessagingProfile.list({}, nil)
41
+ end
42
+ assert_raises TypeError do
43
+ Telnyx::MessagingProfile.list({}, api_key: nil)
44
+ end
45
+ end
46
+
47
+ should "specifying api credentials containing whitespace should raise an exception" do
48
+ Telnyx.api_key = "key "
49
+ assert_raises Telnyx::AuthenticationError do
50
+ Telnyx::MessagingProfile.new("123").refresh
51
+ end
52
+ end
53
+
54
+ should "get resource URL" do
55
+ m = Telnyx::MessagingProfile.new("123")
56
+ assert_match "/messaging_profiles/123", m.resource_url
57
+ end
58
+
59
+ context "when specifying per-object credentials" do
60
+ context "with no global API key set" do
61
+ should "use the per-object credential when creating" do
62
+ stub_post = stub_request(:post, "#{Telnyx.api_base}/v2/messaging_profiles")
63
+ .with(headers: { "Authorization" => "Bearer super-secret" })
64
+ .to_return(body: JSON.generate(data: messaging_profile_fixture))
65
+
66
+ Telnyx::MessagingProfile.create({ name: "New Messaging Profile" },
67
+ "super-secret")
68
+ assert_requested(stub_post)
69
+ end
70
+ end
71
+
72
+ context "with a global API key set" do
73
+ setup do
74
+ Telnyx.api_key = "global"
75
+ end
76
+
77
+ teardown do
78
+ Telnyx.api_key = nil
79
+ end
80
+
81
+ should "use the per-object credential when creating" do
82
+ stub_post = stub_request(:post, "#{Telnyx.api_base}/v2/messaging_profiles")
83
+ .with(headers: { "Authorization" => "Bearer super-secret" })
84
+ .to_return(body: JSON.generate(data: messaging_profile_fixture))
85
+
86
+ Telnyx::MessagingProfile.create({ name: "New Messaging Profile" },
87
+ "super-secret")
88
+ assert_requested(stub_post)
89
+ end
90
+ end
91
+ end
92
+
93
+ context "with valid credentials" do
94
+ should "urlencode values in GET params" do
95
+ stub_get = stub_request(:get, "#{Telnyx.api_base}/v2/messaging_profiles")
96
+ .with(query: { page: { size: 10 } })
97
+ .to_return(body: JSON.generate(data: [messaging_profile_fixture]))
98
+ messaging_profiles = Telnyx::MessagingProfile.list(page: { size: 10 }).data
99
+ assert messaging_profiles.is_a? Array
100
+ assert_requested(stub_get)
101
+ end
102
+
103
+ should "setting a nil value for a param should exclude that param from the request" do
104
+ stub_get = stub_request(:get, "#{Telnyx.api_base}/v2/messaging_profiles")
105
+ .with(query: { page: { size: 5 }, sad: false })
106
+ .to_return(body: JSON.generate(data: [messaging_profile_fixture]))
107
+ Telnyx::MessagingProfile.list(count: nil, page: { size: 5 }, sad: false)
108
+
109
+ assert_requested(stub_get)
110
+
111
+ Telnyx::MessagingProfile.create(name: nil, foo: "bar")
112
+
113
+ assert_requested(:post, "#{Telnyx.api_base}/v2/messaging_profiles", body: { "foo" => "bar" })
114
+ end
115
+
116
+ should "requesting with a unicode ID should result in a request" do
117
+ stub_request(:get, "#{Telnyx.api_base}/v2/messaging_profiles/%E2%98%83")
118
+ .to_return(body: JSON.generate(make_resource_not_found_error), status: 404)
119
+ mp = Telnyx::MessagingProfile.new("☃")
120
+ assert_raises(Telnyx::ResourceNotFoundError) { mp.refresh }
121
+ end
122
+
123
+ should "requesting with no ID should result in an InvalidRequestError with no request" do
124
+ mp = Telnyx::MessagingProfile.new
125
+ assert_raises(Telnyx::InvalidRequestError) { mp.refresh }
126
+ end
127
+
128
+ should "making a GET request with parameters should have a query string and no body" do
129
+ stub = stub_request(:get, "#{Telnyx.api_base}/v2/messaging_profiles")
130
+ .with(query: { limit: 1 })
131
+ .to_return(body: JSON.generate(data: [messaging_profile_fixture]))
132
+ Telnyx::MessagingProfile.list(limit: 1)
133
+ assert_requested(stub, body: "")
134
+ end
135
+
136
+ should "making a POST request with parameters should have a body and no query string" do
137
+ Telnyx::MessagingProfile.create(name: "New Messaging Profile")
138
+ assert_requested(:post, "#{Telnyx.api_base}/v2/messaging_profiles", body: { name: "New Messaging Profile" })
139
+ end
140
+
141
+ should "loading an object should issue a GET request" do
142
+ mp = Telnyx::MessagingProfile.new("123")
143
+ mp.refresh
144
+ assert_requested(:get, "#{Telnyx.api_base}/v2/messaging_profiles/123")
145
+ end
146
+
147
+ should "using array accessors should be the same as the method interface" do
148
+ mp = Telnyx::MessagingProfile.new("123")
149
+ mp.refresh
150
+ assert_equal mp.created_at, mp[:created_at]
151
+ assert_equal mp.created_at, mp["created_at"]
152
+ mp["created_at"] = 12_345
153
+ assert_equal mp.created_at, 12_345
154
+ assert_requested(:get, "#{Telnyx.api_base}/v2/messaging_profiles/123")
155
+ end
156
+
157
+ should "updating an object should issue a PATCH request with only the changed properties" do
158
+ mp = Telnyx::MessagingProfile.construct_from(messaging_profile_fixture)
159
+ mp.name = "new name"
160
+ mp.save
161
+ assert_requested(:patch, "#{Telnyx.api_base}/v2/messaging_profiles/123", body: { "name" => "new name" })
162
+ end
163
+
164
+ should "updating should merge in returned properties" do
165
+ mp = Telnyx::MessagingProfile.new("123")
166
+ mp.name = "new name"
167
+ mp.save
168
+
169
+ assert_requested(:patch, "#{Telnyx.api_base}/v2/messaging_profiles/123", body: { "name" => "new name" })
170
+ assert mp.simplify_characters
171
+ end
172
+
173
+ should "updating should fail if api_key is overwritten with nil" do
174
+ mp = Telnyx::MessagingProfile.new
175
+ assert_raises TypeError do
176
+ mp.save({}, api_key: nil)
177
+ end
178
+ end
179
+
180
+ should "updating should use the supplied api_key" do
181
+ stub_post = stub_request(:post, "#{Telnyx.api_base}/v2/messaging_profiles")
182
+ .with(headers: { "Authorization" => "Bearer super-secret" })
183
+ .to_return(body: JSON.generate(data: messaging_profile_fixture))
184
+ mp = Telnyx::MessagingProfile.new
185
+ mp.save({ name: "Profile for Messages" }, api_key: "super-secret")
186
+
187
+ assert_requested(stub_post)
188
+ assert_equal "Profile for Messages", mp.name
189
+ end
190
+
191
+ should "deleting should send no props and result in an object that has no props other than `deleted`" do
192
+ mp = Telnyx::MessagingProfile.construct_from(messaging_profile_fixture)
193
+ mp.delete
194
+ assert_requested(:delete, "#{Telnyx.api_base}/v2/messaging_profiles/123", body: "")
195
+ end
196
+
197
+ should "loading all of an APIResource should return an array of recursively instantiated objects" do
198
+ messaging_profiles = Telnyx::MessagingProfile.list.data
199
+
200
+ assert_requested(:get, "#{Telnyx.api_base}/v2/messaging_profiles")
201
+ assert messaging_profiles.is_a? Array
202
+ assert messaging_profiles[0].is_a? Telnyx::MessagingProfile
203
+ end
204
+
205
+ should "save nothing if nothing changes" do
206
+ messaging_profile = Telnyx::MessagingProfile.construct_from(
207
+ id: "123",
208
+ meta: {
209
+ key: "value",
210
+ }
211
+ )
212
+
213
+ messaging_profile.save
214
+ assert_requested(:patch, "#{Telnyx.api_base}/v2/messaging_profiles/123", body: {})
215
+ end
216
+
217
+ should "correctly handle array noops" do
218
+ messaging_profile = Telnyx::MessagingProfile.construct_from(
219
+ id: "myid",
220
+ legal_entity: {
221
+ additional_owners: [{ first_name: "Bob" }],
222
+ },
223
+ currencies_supported: %w[usd cad]
224
+ )
225
+
226
+ messaging_profile.save
227
+ assert_requested(:patch, "#{Telnyx.api_base}/v2/messaging_profiles/myid", body: {})
228
+ end
229
+
230
+ should "correctly handle hash noops" do
231
+ messaging_profile = Telnyx::MessagingProfile.construct_from(
232
+ id: "myid",
233
+ legal_entity: {
234
+ address: { line1: "1 Two Three" },
235
+ }
236
+ )
237
+
238
+ stub_post = stub_request(:patch, "#{Telnyx.api_base}/v2/messaging_profiles/myid")
239
+ .with(body: {})
240
+ .to_return(body: JSON.generate(data: { "id" => "myid" }))
241
+
242
+ messaging_profile.save
243
+ assert_requested(stub_post)
244
+ end
245
+
246
+ should "should create a new resource when an object without an id is saved" do
247
+ messaging_profile = Telnyx::MessagingProfile.construct_from(id: nil, name: nil)
248
+
249
+ messaging_profile.name = "my-messaging-profile"
250
+ messaging_profile.save
251
+
252
+ assert_requested(:post, "#{Telnyx.api_base}/v2/messaging_profiles", body: { name: "my-messaging-profile" })
253
+ end
254
+
255
+ should "set attributes as part of save" do
256
+ messaging_profile = Telnyx::MessagingProfile.construct_from(id: nil,
257
+ name: nil)
258
+
259
+ stub_post = stub_request(:post, "#{Telnyx.api_base}/v2/messaging_profiles")
260
+ .with(body: { name: "telnyx", meta: { key: "value" } })
261
+ .to_return(body: JSON.generate(data: { "id" => "123" }))
262
+
263
+ messaging_profile.save(name: "telnyx", meta: { key: "value" })
264
+ assert_requested(stub_post)
265
+ end
266
+ end
267
+
268
+ @@fixtures = {}
269
+ setup do
270
+ if @@fixtures.empty?
271
+ cache_fixture(:messaging_profile) do
272
+ MessagingProfile.retrieve("123")
273
+ end
274
+ end
275
+ end
276
+
277
+ private
278
+
279
+ def messaging_profile_fixture
280
+ @@fixtures[:messaging_profile]
281
+ end
282
+
283
+ # Expects to retrieve a fixture from telnyx-mock (an API call should be
284
+ # included in the block to yield to) and does very simple memoization.
285
+ def cache_fixture(key)
286
+ return @@fixtures[key] if @@fixtures.key?(key)
287
+
288
+ obj = yield
289
+ @@fixtures[key] = obj.instance_variable_get(:@values).freeze
290
+ @@fixtures[key]
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require ::File.expand_path("../../test_helper", __FILE__)
4
+
5
+ module Telnyx
6
+ class AvailablePhoneNumberTest < Test::Unit::TestCase
7
+ should "be listable" do
8
+ available_phone_numbers = Telnyx::AvailablePhoneNumber.list
9
+ assert_requested :get, "#{Telnyx.api_base}/v2/available_phone_numbers"
10
+ assert available_phone_numbers.data.is_a?(Array)
11
+ assert available_phone_numbers.first.is_a?(Telnyx::AvailablePhoneNumber)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require ::File.expand_path("../../test_helper", __FILE__)
4
+
5
+ module Telnyx
6
+ class TelnyxErrorTest < Test::Unit::TestCase
7
+ context "#to_s" do
8
+ should "convert to string" do
9
+ e = TelnyxError.new([{ "title" => "Missing required attributes" }])
10
+ assert_equal "Missing required attributes Full details: [{\"title\"=>\"Missing required attributes\"}]", e.to_s
11
+
12
+ e = TelnyxError.new([{ "title" => "Missing required attributes" }], http_status: 422)
13
+ assert_equal "(Status 422) Missing required attributes Full details: [{\"title\"=>\"Missing required attributes\"}]", e.to_s
14
+
15
+ e = TelnyxError.new([{ "title" => "Missing required attributes" }], http_status: nil, http_body: nil, json_body: nil, http_headers: { request_id: "request-id" })
16
+ assert_equal "(Request request-id) Missing required attributes Full details: [{\"title\"=>\"Missing required attributes\"}]", e.to_s
17
+
18
+ e = TelnyxError.new([{ "title" => "Missing required attributes" }, { "title" => "Phone number must be in +E.164 format" }], http_status: nil, http_body: nil, json_body: nil, http_headers: { request_id: "request-id" })
19
+ assert_equal "(Request request-id) Missing required attributes plus 1 other error. Full details: [{\"title\"=>\"Missing required attributes\"}, {\"title\"=>\"Phone number must be in +E.164 format\"}]", e.to_s
20
+ end
21
+ end
22
+ end
23
+ end