voiceml 0.9.2 → 0.9.3

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -13
  3. data/lib/voiceml/client.rb +53 -13
  4. data/lib/voiceml/errors.rb +15 -2
  5. data/lib/voiceml/models/accounts.rb +47 -3
  6. data/lib/voiceml/models/applications.rb +6 -0
  7. data/lib/voiceml/models/assistants_v1.rb +40 -0
  8. data/lib/voiceml/models/calls.rb +4 -0
  9. data/lib/voiceml/models/common.rb +35 -4
  10. data/lib/voiceml/models/conferences.rb +8 -0
  11. data/lib/voiceml/models/conversations_v1.rb +131 -29
  12. data/lib/voiceml/models/diagnostics.rb +33 -12
  13. data/lib/voiceml/models/exports.rb +58 -0
  14. data/lib/voiceml/models/incoming_phone_numbers.rb +4 -0
  15. data/lib/voiceml/models/media.rb +4 -0
  16. data/lib/voiceml/models/messages.rb +29 -0
  17. data/lib/voiceml/models/messaging_v1.rb +35 -2
  18. data/lib/voiceml/models/monitor_v1.rb +54 -0
  19. data/lib/voiceml/models/notifications.rb +2 -0
  20. data/lib/voiceml/models/outgoing_caller_ids.rb +6 -0
  21. data/lib/voiceml/models/payments.rb +2 -0
  22. data/lib/voiceml/models/pricing.rb +38 -4
  23. data/lib/voiceml/models/queues.rb +8 -0
  24. data/lib/voiceml/models/recordings.rb +5 -1
  25. data/lib/voiceml/models/routes_v2.rb +6 -2
  26. data/lib/voiceml/models/sip.rb +34 -6
  27. data/lib/voiceml/models/sip_registrations.rb +49 -0
  28. data/lib/voiceml/models/siprec.rb +5 -17
  29. data/lib/voiceml/models/streams.rb +4 -16
  30. data/lib/voiceml/models/transcriptions.rb +35 -4
  31. data/lib/voiceml/models/usage_records.rb +48 -0
  32. data/lib/voiceml/models/user_defined_messages.rb +2 -0
  33. data/lib/voiceml/models/voice_v1.rb +29 -19
  34. data/lib/voiceml/request_validator.rb +125 -0
  35. data/lib/voiceml/resources/accounts.rb +75 -0
  36. data/lib/voiceml/resources/applications.rb +3 -1
  37. data/lib/voiceml/resources/assistants_v1.rb +53 -77
  38. data/lib/voiceml/resources/base.rb +138 -16
  39. data/lib/voiceml/resources/calls.rb +34 -49
  40. data/lib/voiceml/resources/conferences.rb +26 -5
  41. data/lib/voiceml/resources/conversations_v1.rb +147 -172
  42. data/lib/voiceml/resources/diagnostics.rb +20 -35
  43. data/lib/voiceml/resources/exports.rb +36 -0
  44. data/lib/voiceml/resources/incoming_phone_numbers.rb +53 -32
  45. data/lib/voiceml/resources/messages.rb +79 -15
  46. data/lib/voiceml/resources/messaging_v1.rb +47 -13
  47. data/lib/voiceml/resources/monitor_v1.rb +68 -0
  48. data/lib/voiceml/resources/notifications.rb +1 -1
  49. data/lib/voiceml/resources/outgoing_caller_ids.rb +77 -0
  50. data/lib/voiceml/resources/pricing.rb +13 -5
  51. data/lib/voiceml/resources/queues.rb +6 -5
  52. data/lib/voiceml/resources/recordings.rb +39 -5
  53. data/lib/voiceml/resources/routes_v2.rb +10 -4
  54. data/lib/voiceml/resources/sip.rb +30 -11
  55. data/lib/voiceml/resources/transcriptions.rb +40 -0
  56. data/lib/voiceml/resources/usage_records.rb +51 -0
  57. data/lib/voiceml/resources/voice_v1.rb +31 -38
  58. data/lib/voiceml/transport.rb +277 -230
  59. data/lib/voiceml/version.rb +1 -1
  60. data/lib/voiceml.rb +14 -15
  61. metadata +18 -6
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'base'
4
+
3
5
  require_relative '../models/assistants_v1'
4
6
 
5
7
  module VoiceML
@@ -63,6 +65,8 @@ module VoiceML
63
65
  # Fetch returns the AssistantWithToolsAndKnowledge shape (tools + knowledge inline).
64
66
  # ============================================================================
65
67
  class AssistantsV1AssistantsResource
68
+ include V1Pages
69
+ include FieldMapping
66
70
  CREATE_FIELDS = %i[name owner personality_prompt model customer_ai segment_credential].freeze
67
71
  UPDATE_FIELDS = %i[name owner personality_prompt model customer_ai segment_credential].freeze
68
72
 
@@ -81,39 +85,29 @@ module VoiceML
81
85
  def create(name:, **kwargs)
82
86
  kwargs[:name] = name
83
87
  AssistantsV1Assistant.from_hash(
84
- @transport.request(:post, '/v1/Assistants', json: build_json(CREATE_FIELDS, kwargs))
88
+ @transport.request(:post, '/v1/Assistants', json: json_params(CREATE_FIELDS, kwargs))
85
89
  )
86
90
  end
87
91
 
88
92
  def fetch(id)
89
93
  AssistantsV1AssistantWithToolsAndKnowledge.from_hash(
90
- @transport.request(:get, "/v1/Assistants/#{id}")
94
+ @transport.request(:get, "/v1/Assistants/#{encode_path_segment(id)}")
91
95
  )
92
96
  end
93
97
 
94
98
  def update(id, **kwargs)
95
99
  AssistantsV1Assistant.from_hash(
96
- @transport.request(:put, "/v1/Assistants/#{id}", json: build_json(UPDATE_FIELDS, kwargs))
100
+ @transport.request(:put, "/v1/Assistants/#{encode_path_segment(id)}", json: json_params(UPDATE_FIELDS, kwargs))
97
101
  )
98
102
  end
99
103
 
100
104
  def delete(id)
101
- @transport.request(:delete, "/v1/Assistants/#{id}")
105
+ @transport.request(:delete, "/v1/Assistants/#{encode_path_segment(id)}")
102
106
  nil
103
107
  end
104
108
 
105
109
  private
106
110
 
107
- def build_json(keys, kwargs)
108
- out = {}
109
- keys.each do |k|
110
- v = kwargs[k]
111
- next if v.nil?
112
-
113
- out[k.to_s] = v
114
- end
115
- out
116
- end
117
111
  end
118
112
 
119
113
  # ============================================================================
@@ -150,6 +144,8 @@ module VoiceML
150
144
 
151
145
  # /v1/Assistants/{id}/Tools — list attached + attach/detach single Tool.
152
146
  class AssistantsV1AssistantToolsScope
147
+ include V1Pages
148
+ include FieldMapping
153
149
  def initialize(transport, assistant_id)
154
150
  @transport = transport
155
151
  @assistant_id = assistant_id
@@ -159,25 +155,27 @@ module VoiceML
159
155
  params = {}
160
156
  params['PageSize'] = page_size unless page_size.nil?
161
157
  AssistantsV1ToolList.new(
162
- @transport.request(:get, "/v1/Assistants/#{@assistant_id}/Tools", params: params)
158
+ @transport.request(:get, "/v1/Assistants/#{encode_path_segment(@assistant_id)}/Tools", params: params)
163
159
  )
164
160
  end
165
161
 
166
162
  # POST /v1/Assistants/{id}/Tools/{toolId} — 204 No Content on success.
167
163
  def attach(tool_id)
168
- @transport.request(:post, "/v1/Assistants/#{@assistant_id}/Tools/#{tool_id}")
164
+ @transport.request(:post, "/v1/Assistants/#{encode_path_segment(@assistant_id)}/Tools/#{encode_path_segment(tool_id)}")
169
165
  nil
170
166
  end
171
167
 
172
168
  # DELETE /v1/Assistants/{id}/Tools/{toolId} — 204 No Content on success.
173
169
  def detach(tool_id)
174
- @transport.request(:delete, "/v1/Assistants/#{@assistant_id}/Tools/#{tool_id}")
170
+ @transport.request(:delete, "/v1/Assistants/#{encode_path_segment(@assistant_id)}/Tools/#{encode_path_segment(tool_id)}")
175
171
  nil
176
172
  end
177
173
  end
178
174
 
179
175
  # /v1/Assistants/{id}/Knowledge — list attached + attach/detach single Knowledge.
180
176
  class AssistantsV1AssistantKnowledgeScope
177
+ include V1Pages
178
+ include FieldMapping
181
179
  def initialize(transport, assistant_id)
182
180
  @transport = transport
183
181
  @assistant_id = assistant_id
@@ -187,25 +185,27 @@ module VoiceML
187
185
  params = {}
188
186
  params['PageSize'] = page_size unless page_size.nil?
189
187
  AssistantsV1KnowledgeList.new(
190
- @transport.request(:get, "/v1/Assistants/#{@assistant_id}/Knowledge", params: params)
188
+ @transport.request(:get, "/v1/Assistants/#{encode_path_segment(@assistant_id)}/Knowledge", params: params)
191
189
  )
192
190
  end
193
191
 
194
192
  # POST /v1/Assistants/{id}/Knowledge/{knowledgeId} — 204 No Content on success.
195
193
  def attach(knowledge_id)
196
- @transport.request(:post, "/v1/Assistants/#{@assistant_id}/Knowledge/#{knowledge_id}")
194
+ @transport.request(:post, "/v1/Assistants/#{encode_path_segment(@assistant_id)}/Knowledge/#{encode_path_segment(knowledge_id)}")
197
195
  nil
198
196
  end
199
197
 
200
198
  # DELETE /v1/Assistants/{id}/Knowledge/{knowledgeId} — 204 No Content on success.
201
199
  def detach(knowledge_id)
202
- @transport.request(:delete, "/v1/Assistants/#{@assistant_id}/Knowledge/#{knowledge_id}")
200
+ @transport.request(:delete, "/v1/Assistants/#{encode_path_segment(@assistant_id)}/Knowledge/#{encode_path_segment(knowledge_id)}")
203
201
  nil
204
202
  end
205
203
  end
206
204
 
207
205
  # /v1/Assistants/{id}/Feedbacks — list + create.
208
206
  class AssistantsV1AssistantFeedbacksScope
207
+ include V1Pages
208
+ include FieldMapping
209
209
  CREATE_FIELDS = %i[session_id message_id score text].freeze
210
210
 
211
211
  def initialize(transport, assistant_id)
@@ -217,34 +217,26 @@ module VoiceML
217
217
  params = {}
218
218
  params['PageSize'] = page_size unless page_size.nil?
219
219
  AssistantsV1FeedbackList.new(
220
- @transport.request(:get, "/v1/Assistants/#{@assistant_id}/Feedbacks", params: params)
220
+ @transport.request(:get, "/v1/Assistants/#{encode_path_segment(@assistant_id)}/Feedbacks", params: params)
221
221
  )
222
222
  end
223
223
 
224
224
  def create(session_id:, **kwargs)
225
225
  kwargs[:session_id] = session_id
226
226
  AssistantsV1Feedback.from_hash(
227
- @transport.request(:post, "/v1/Assistants/#{@assistant_id}/Feedbacks",
228
- json: build_json(CREATE_FIELDS, kwargs))
227
+ @transport.request(:post, "/v1/Assistants/#{encode_path_segment(@assistant_id)}/Feedbacks",
228
+ json: json_params(CREATE_FIELDS, kwargs))
229
229
  )
230
230
  end
231
231
 
232
232
  private
233
233
 
234
- def build_json(keys, kwargs)
235
- out = {}
236
- keys.each do |k|
237
- v = kwargs[k]
238
- next if v.nil?
239
-
240
- out[k.to_s] = v
241
- end
242
- out
243
- end
244
234
  end
245
235
 
246
236
  # /v1/Assistants/{id}/Messages — send (create) a message; returns the model's reply.
247
237
  class AssistantsV1AssistantMessagesScope
238
+ include V1Pages
239
+ include FieldMapping
248
240
  CREATE_FIELDS = %i[identity body session_id webhook mode].freeze
249
241
 
250
242
  def initialize(transport, assistant_id)
@@ -256,23 +248,13 @@ module VoiceML
256
248
  kwargs[:identity] = identity
257
249
  kwargs[:body] = body
258
250
  AssistantsV1SendMessageResponse.from_hash(
259
- @transport.request(:post, "/v1/Assistants/#{@assistant_id}/Messages",
260
- json: build_json(CREATE_FIELDS, kwargs))
251
+ @transport.request(:post, "/v1/Assistants/#{encode_path_segment(@assistant_id)}/Messages",
252
+ json: json_params(CREATE_FIELDS, kwargs))
261
253
  )
262
254
  end
263
255
 
264
256
  private
265
257
 
266
- def build_json(keys, kwargs)
267
- out = {}
268
- keys.each do |k|
269
- v = kwargs[k]
270
- next if v.nil?
271
-
272
- out[k.to_s] = v
273
- end
274
- out
275
- end
276
258
  end
277
259
 
278
260
  # ============================================================================
@@ -281,6 +263,8 @@ module VoiceML
281
263
  # Fetch returns the ToolWithPolicies shape (policies inline).
282
264
  # ============================================================================
283
265
  class AssistantsV1ToolsResource
266
+ include V1Pages
267
+ include FieldMapping
284
268
  CREATE_FIELDS = %i[name type enabled assistant_id description meta].freeze
285
269
  UPDATE_FIELDS = %i[name type enabled description meta].freeze
286
270
 
@@ -300,37 +284,27 @@ module VoiceML
300
284
  kwargs[:type] = type
301
285
  kwargs[:enabled] = enabled
302
286
  AssistantsV1Tool.from_hash(
303
- @transport.request(:post, '/v1/Tools', json: build_json(CREATE_FIELDS, kwargs))
287
+ @transport.request(:post, '/v1/Tools', json: json_params(CREATE_FIELDS, kwargs))
304
288
  )
305
289
  end
306
290
 
307
291
  def fetch(id)
308
- AssistantsV1ToolWithPolicies.from_hash(@transport.request(:get, "/v1/Tools/#{id}"))
292
+ AssistantsV1ToolWithPolicies.from_hash(@transport.request(:get, "/v1/Tools/#{encode_path_segment(id)}"))
309
293
  end
310
294
 
311
295
  def update(id, **kwargs)
312
296
  AssistantsV1Tool.from_hash(
313
- @transport.request(:put, "/v1/Tools/#{id}", json: build_json(UPDATE_FIELDS, kwargs))
297
+ @transport.request(:put, "/v1/Tools/#{encode_path_segment(id)}", json: json_params(UPDATE_FIELDS, kwargs))
314
298
  )
315
299
  end
316
300
 
317
301
  def delete(id)
318
- @transport.request(:delete, "/v1/Tools/#{id}")
302
+ @transport.request(:delete, "/v1/Tools/#{encode_path_segment(id)}")
319
303
  nil
320
304
  end
321
305
 
322
306
  private
323
307
 
324
- def build_json(keys, kwargs)
325
- out = {}
326
- keys.each do |k|
327
- v = kwargs[k]
328
- next if v.nil?
329
-
330
- out[k.to_s] = v
331
- end
332
- out
333
- end
334
308
  end
335
309
 
336
310
  # ============================================================================
@@ -338,6 +312,8 @@ module VoiceML
338
312
  # `list` accepts an optional `assistant_id:` filter (wire name `AssistantId`).
339
313
  # ============================================================================
340
314
  class AssistantsV1KnowledgeResource
315
+ include V1Pages
316
+ include FieldMapping
341
317
  CREATE_FIELDS = %i[name type assistant_id description embedding_model knowledge_source_details].freeze
342
318
  UPDATE_FIELDS = %i[name type description embedding_model knowledge_source_details].freeze
343
319
 
@@ -356,37 +332,27 @@ module VoiceML
356
332
  kwargs[:name] = name
357
333
  kwargs[:type] = type
358
334
  AssistantsV1Knowledge.from_hash(
359
- @transport.request(:post, '/v1/Knowledge', json: build_json(CREATE_FIELDS, kwargs))
335
+ @transport.request(:post, '/v1/Knowledge', json: json_params(CREATE_FIELDS, kwargs))
360
336
  )
361
337
  end
362
338
 
363
339
  def fetch(id)
364
- AssistantsV1Knowledge.from_hash(@transport.request(:get, "/v1/Knowledge/#{id}"))
340
+ AssistantsV1Knowledge.from_hash(@transport.request(:get, "/v1/Knowledge/#{encode_path_segment(id)}"))
365
341
  end
366
342
 
367
343
  def update(id, **kwargs)
368
344
  AssistantsV1Knowledge.from_hash(
369
- @transport.request(:put, "/v1/Knowledge/#{id}", json: build_json(UPDATE_FIELDS, kwargs))
345
+ @transport.request(:put, "/v1/Knowledge/#{encode_path_segment(id)}", json: json_params(UPDATE_FIELDS, kwargs))
370
346
  )
371
347
  end
372
348
 
373
349
  def delete(id)
374
- @transport.request(:delete, "/v1/Knowledge/#{id}")
350
+ @transport.request(:delete, "/v1/Knowledge/#{encode_path_segment(id)}")
375
351
  nil
376
352
  end
377
353
 
378
354
  private
379
355
 
380
- def build_json(keys, kwargs)
381
- out = {}
382
- keys.each do |k|
383
- v = kwargs[k]
384
- next if v.nil?
385
-
386
- out[k.to_s] = v
387
- end
388
- out
389
- end
390
356
  end
391
357
 
392
358
  # ============================================================================
@@ -413,6 +379,8 @@ module VoiceML
413
379
 
414
380
  # /v1/Knowledge/{id}/Status — read-only ingestion status snapshot.
415
381
  class AssistantsV1KnowledgeStatusScope
382
+ include V1Pages
383
+ include FieldMapping
416
384
  def initialize(transport, knowledge_id)
417
385
  @transport = transport
418
386
  @knowledge_id = knowledge_id
@@ -420,13 +388,15 @@ module VoiceML
420
388
 
421
389
  def fetch
422
390
  AssistantsV1KnowledgeStatus.from_hash(
423
- @transport.request(:get, "/v1/Knowledge/#{@knowledge_id}/Status")
391
+ @transport.request(:get, "/v1/Knowledge/#{encode_path_segment(@knowledge_id)}/Status")
424
392
  )
425
393
  end
426
394
  end
427
395
 
428
396
  # /v1/Knowledge/{id}/Chunks — paged list of indexed chunks.
429
397
  class AssistantsV1KnowledgeChunksScope
398
+ include V1Pages
399
+ include FieldMapping
430
400
  def initialize(transport, knowledge_id)
431
401
  @transport = transport
432
402
  @knowledge_id = knowledge_id
@@ -436,7 +406,7 @@ module VoiceML
436
406
  params = {}
437
407
  params['PageSize'] = page_size unless page_size.nil?
438
408
  AssistantsV1KnowledgeChunkList.new(
439
- @transport.request(:get, "/v1/Knowledge/#{@knowledge_id}/Chunks", params: params)
409
+ @transport.request(:get, "/v1/Knowledge/#{encode_path_segment(@knowledge_id)}/Chunks", params: params)
440
410
  )
441
411
  end
442
412
  end
@@ -445,6 +415,8 @@ module VoiceML
445
415
  # /v1/Sessions — list + fetch on the top-level Session resource.
446
416
  # ============================================================================
447
417
  class AssistantsV1SessionsResource
418
+ include V1Pages
419
+ include FieldMapping
448
420
  def initialize(transport)
449
421
  @transport = transport
450
422
  end
@@ -456,7 +428,7 @@ module VoiceML
456
428
  end
457
429
 
458
430
  def fetch(id)
459
- AssistantsV1Session.from_hash(@transport.request(:get, "/v1/Sessions/#{id}"))
431
+ AssistantsV1Session.from_hash(@transport.request(:get, "/v1/Sessions/#{encode_path_segment(id)}"))
460
432
  end
461
433
  end
462
434
 
@@ -475,6 +447,8 @@ module VoiceML
475
447
  end
476
448
 
477
449
  class AssistantsV1SessionMessagesScope
450
+ include V1Pages
451
+ include FieldMapping
478
452
  def initialize(transport, session_id)
479
453
  @transport = transport
480
454
  @session_id = session_id
@@ -484,7 +458,7 @@ module VoiceML
484
458
  params = {}
485
459
  params['PageSize'] = page_size unless page_size.nil?
486
460
  AssistantsV1MessageList.new(
487
- @transport.request(:get, "/v1/Sessions/#{@session_id}/Messages", params: params)
461
+ @transport.request(:get, "/v1/Sessions/#{encode_path_segment(@session_id)}/Messages", params: params)
488
462
  )
489
463
  end
490
464
  end
@@ -493,6 +467,8 @@ module VoiceML
493
467
  # /v1/Policies — read-only list, filterable by ToolId/KnowledgeId.
494
468
  # ============================================================================
495
469
  class AssistantsV1PoliciesResource
470
+ include V1Pages
471
+ include FieldMapping
496
472
  def initialize(transport)
497
473
  @transport = transport
498
474
  end
@@ -1,38 +1,160 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
4
+
3
5
  module VoiceML
6
+ # @api private
7
+ # Kwarg-to-wire-name mapping shared by every resource that takes `**kwargs`.
8
+ # Raises `ArgumentError` for kwargs outside the field map, so a typo
9
+ # (`machine_detecton:`) fails loudly instead of being silently dropped.
10
+ module FieldMapping
11
+ private
12
+
13
+ # Translate snake_case Ruby kwargs to the form/query field names the server
14
+ # expects. `nil` values are dropped; booleans and dates are formatted inside
15
+ # Transport.
16
+ def form_params(map, kwargs)
17
+ unknown = kwargs.keys - map.values
18
+ unless unknown.empty?
19
+ raise ArgumentError, "unknown keyword(s): #{unknown.map(&:inspect).sort.join(', ')}; " \
20
+ "expected one of #{map.values.map(&:inspect).sort.join(', ')}"
21
+ end
22
+
23
+ out = {}
24
+ map.each do |wire_name, ruby_key|
25
+ value = kwargs[ruby_key]
26
+ next if value.nil?
27
+
28
+ out[wire_name] = value
29
+ end
30
+ out
31
+ end
32
+
33
+ # Translate a snake_case kwargs subset for JSON bodies (Assistants v1 keeps
34
+ # snake_case wire names). Same strictness as `form_params`.
35
+ def json_params(keys, kwargs)
36
+ unknown = kwargs.keys - keys
37
+ unless unknown.empty?
38
+ raise ArgumentError, "unknown keyword(s): #{unknown.map(&:inspect).sort.join(', ')}; " \
39
+ "expected one of #{keys.map(&:inspect).sort.join(', ')}"
40
+ end
41
+
42
+ out = {}
43
+ keys.each do |key|
44
+ value = kwargs[key]
45
+ next if value.nil?
46
+
47
+ out[key.to_s] = value
48
+ end
49
+ out
50
+ end
51
+
52
+ # Percent-encode one caller-supplied path segment per RFC 3986: keep the
53
+ # unreserved set (`A-Z a-z 0-9 - . _ ~`) and encode every other byte as
54
+ # `%XX` (UTF-8), so `+` becomes `%2B`, a space `%20`, `/` `%2F`, `?` `%3F`,
55
+ # `#` `%23` and `%` `%25`. Applied once per segment — never to a full path.
56
+ def encode_path_segment(segment)
57
+ segment.to_s.b.gsub(/[^A-Za-z0-9\-._~]/) { |byte| format('%%%02X', byte.ord) }
58
+ end
59
+
60
+ def page_url(link, absolute: false)
61
+ base = URI.parse(@transport.base_url)
62
+ uri = URI.parse(link)
63
+ raise ArgumentError, 'expected an absolute next_page_url' if absolute && !uri.absolute?
64
+
65
+ uri = URI.join("#{base.scheme}://#{base.authority}/", link) unless uri.absolute?
66
+ unless uri.scheme == base.scheme && uri.host == base.host && uri.port == base.port && !uri.userinfo
67
+ raise ArgumentError, 'next page must stay on the configured product origin'
68
+ end
69
+ uri.to_s
70
+ rescue URI::Error, ArgumentError => e
71
+ raise ConnectionError.new("invalid next-page link: #{e.message}", status_code: 0)
72
+ end
73
+ end
74
+
4
75
  # @api private
5
76
  # Mixin holding a `Transport` reference and helpers for AccountSid-scoped pathing.
6
77
  class BaseResource
78
+ include FieldMapping
79
+
7
80
  def initialize(transport)
8
81
  @transport = transport
9
82
  end
10
83
 
84
+ # Fetch the page a list envelope links to via `next_page_uri`, verbatim:
85
+ # the URI (path + query, resolved against the transport's base host) is
86
+ # sent as-is, never re-synthesised from a page counter.
87
+ #
88
+ # @return [Object, nil] the next page's envelope (same class as the given
89
+ # one), or `nil` when the envelope has no next page.
90
+ def next_page(list)
91
+ uri = list.respond_to?(:next_page_uri) ? list.next_page_uri : nil
92
+ return nil if uri.nil? || uri.empty?
93
+
94
+ list.class.new(@transport.request(:get, page_url(uri)))
95
+ end
96
+
97
+ # A redacted dump: never includes credentials via the transport.
98
+ def inspect
99
+ "#<#{self.class.name}>"
100
+ end
101
+ alias to_s inspect
102
+
11
103
  private
12
104
 
13
105
  # Build a URL under `/2010-04-01/Accounts/{AccountSid}/...`. Caller passes path segments
14
- # (e.g. `"Calls"`, sid, `"Recordings"`). Empty segments are skipped; nothing is
15
- # URL-encoded — sids and slugs never need escaping.
106
+ # (e.g. `"Calls"`, sid, `"Recordings"`). Empty segments are skipped; every segment is
107
+ # percent-encoded as one RFC 3986 path segment.
16
108
  #
17
- # As of v0.5.0 every REST endpoint resolves under its `.json` form (Twilio drop-in
18
- # compatibility). Pass `suffix: ''` to opt out — used by `.wav` audio fetches and any
19
- # caller that needs to append a different extension.
109
+ # Every REST endpoint resolves under its `.json` form. Pass `suffix: ''` to opt out —
110
+ # used by `.wav` audio fetches, export downloads and any caller that needs to append a
111
+ # different extension.
20
112
  def path(*parts, suffix: '.json')
21
- tail = parts.compact.reject { |p| p.to_s.empty? }.join('/')
22
- "/2010-04-01/Accounts/#{@transport.account_sid}/#{tail}#{suffix}"
113
+ tail = parts.compact.reject { |p| p.to_s.empty? }.map { |p| encode_path_segment(p) }.join('/')
114
+ "/2010-04-01/Accounts/#{encode_path_segment(@transport.account_sid)}/#{tail}#{suffix}"
23
115
  end
116
+ end
24
117
 
25
- # Translate snake_case Ruby kwargs to the form/query field names the server expects.
26
- # `nil` values are dropped; booleans become "true"/"false" inside Transport.
27
- def form_params(map, kwargs)
28
- out = {}
29
- map.each do |wire_name, ruby_key|
30
- value = kwargs[ruby_key]
31
- next if value.nil?
118
+ # @api private
119
+ # `next_page` / `each_page` support for `/v1` list envelopes, which page via
120
+ # an absolute `meta.next_page_url` instead of a relative `next_page_uri`.
121
+ # The URL is only followed when its scheme and host equal the resource's
122
+ # configured base, so credentials are never sent to a third-party host.
123
+ module V1Pages
124
+ include FieldMapping
125
+ # Follow `meta.next_page_url` from a list envelope.
126
+ #
127
+ # @return [Object, nil] the next page's envelope (same class as the given
128
+ # one), or `nil` when the envelope has no next page.
129
+ def next_page(list)
130
+ url = list.respond_to?(:next_page_url) ? list.next_page_url : nil
131
+ return nil if url.nil? || url.empty?
32
132
 
33
- out[wire_name] = value
133
+ list.class.new(@transport.request(:get, page_url(url, absolute: true)))
134
+ end
135
+
136
+ # Yield every item of every page of a `/v1` list envelope, fetching pages
137
+ # lazily. Returns an Enumerator when called without a block; stopping
138
+ # early makes no further requests.
139
+ def each_page(list, &block)
140
+ return enum_for(:each_page, list) unless block
141
+
142
+ page = list
143
+ loop do
144
+ items = v1_items(page)
145
+ items.each(&block)
146
+ nxt = next_page(page)
147
+ break if nxt.nil?
148
+
149
+ page = nxt
34
150
  end
35
- out
151
+ end
152
+
153
+ private
154
+
155
+ def v1_items(list)
156
+ key = list.to_h.keys.find { |k| list.to_h[k].is_a?(Array) }
157
+ key && list.respond_to?(key) ? list.public_send(key) : []
36
158
  end
37
159
  end
38
160
  end