unisender-ruby 0.1.1 → 0.2.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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -0
  3. data/CONTRIBUTING.md +10 -1
  4. data/README.md +55 -112
  5. data/docs/api.md +395 -210
  6. data/docs/execution.md +89 -0
  7. data/docs/import-recovery.md +41 -0
  8. data/docs/live-contracts.md +34 -0
  9. data/docs/migration-0.2.md +129 -0
  10. data/docs/rails.md +248 -0
  11. data/docs/releases.md +4 -4
  12. data/docs/testing.md +83 -0
  13. data/docs/usage.md +144 -0
  14. data/docs/webhooks.md +41 -0
  15. data/examples/campaign.rb +5 -6
  16. data/examples/campaign_lifecycle.rb +28 -0
  17. data/examples/check_connection.rb +1 -1
  18. data/examples/events.rb +1 -1
  19. data/examples/export.rb +20 -6
  20. data/examples/import.rb +3 -2
  21. data/examples/import_recovery.rb +70 -0
  22. data/examples/quickstart.rb +1 -1
  23. data/examples/subscribe.rb +2 -2
  24. data/lib/unisender/acceptance.rb +104 -0
  25. data/lib/unisender/attachment.rb +47 -0
  26. data/lib/unisender/batch_report.rb +115 -0
  27. data/lib/unisender/batch_summary.rb +87 -0
  28. data/lib/unisender/batches.rb +41 -12
  29. data/lib/unisender/campaign_workflow.rb +72 -0
  30. data/lib/unisender/client.rb +65 -33
  31. data/lib/unisender/client_configuration.rb +50 -0
  32. data/lib/unisender/client_execution.rb +43 -0
  33. data/lib/unisender/client_state.rb +31 -0
  34. data/lib/unisender/client_validation.rb +97 -0
  35. data/lib/unisender/configuration.rb +151 -0
  36. data/lib/unisender/contact_records.rb +251 -0
  37. data/lib/unisender/diagnostic_urls.rb +68 -0
  38. data/lib/unisender/diagnostics.rb +83 -0
  39. data/lib/unisender/download_target.rb +61 -0
  40. data/lib/unisender/download_transfer.rb +43 -0
  41. data/lib/unisender/downloader.rb +99 -0
  42. data/lib/unisender/error_diagnostics.rb +83 -0
  43. data/lib/unisender/errors.rb +125 -8
  44. data/lib/unisender/event_batches.rb +40 -0
  45. data/lib/unisender/event_payload.rb +50 -22
  46. data/lib/unisender/events.rb +21 -5
  47. data/lib/unisender/execution_context.rb +114 -0
  48. data/lib/unisender/form.rb +20 -12
  49. data/lib/unisender/helpers.rb +70 -10
  50. data/lib/unisender/hooks.rb +79 -0
  51. data/lib/unisender/import_cells.rb +62 -0
  52. data/lib/unisender/input_types.rb +30 -0
  53. data/lib/unisender/inputs.rb +262 -0
  54. data/lib/unisender/iteration.rb +15 -0
  55. data/lib/unisender/iterators.rb +60 -0
  56. data/lib/unisender/mail_inputs.rb +37 -0
  57. data/lib/unisender/named_inputs.rb +52 -0
  58. data/lib/unisender/observation.rb +30 -0
  59. data/lib/unisender/operation_contracts.rb +106 -0
  60. data/lib/unisender/operations/contacts_1.rb +4 -3
  61. data/lib/unisender/operations/contacts_2.rb +23 -4
  62. data/lib/unisender/operations/messages_1.rb +7 -4
  63. data/lib/unisender/operations/messages_2.rb +8 -3
  64. data/lib/unisender/operations/messages_3.rb +1 -1
  65. data/lib/unisender/operations/notes_1.rb +1 -1
  66. data/lib/unisender/operations/statistics_1.rb +3 -3
  67. data/lib/unisender/operations/templates_1.rb +2 -2
  68. data/lib/unisender/outcomes.rb +320 -0
  69. data/lib/unisender/page_response.rb +48 -0
  70. data/lib/unisender/paginated_operations.rb +43 -2
  71. data/lib/unisender/pagination.rb +53 -20
  72. data/lib/unisender/persistent_transport.rb +104 -0
  73. data/lib/unisender/rate_limiter.rb +72 -0
  74. data/lib/unisender/record.rb +210 -0
  75. data/lib/unisender/record_rows.rb +69 -0
  76. data/lib/unisender/records.rb +614 -0
  77. data/lib/unisender/request_execution.rb +108 -0
  78. data/lib/unisender/response.rb +13 -10
  79. data/lib/unisender/result_registry.rb +72 -0
  80. data/lib/unisender/results.rb +105 -0
  81. data/lib/unisender/retry_policy.rb +10 -16
  82. data/lib/unisender/serialization.rb +54 -0
  83. data/lib/unisender/statistic_records.rb +129 -0
  84. data/lib/unisender/task_response.rb +85 -0
  85. data/lib/unisender/task_waiter.rb +25 -11
  86. data/lib/unisender/testing/assertions.rb +48 -0
  87. data/lib/unisender/testing/fixtures.rb +24 -0
  88. data/lib/unisender/testing.rb +91 -0
  89. data/lib/unisender/transport.rb +17 -9
  90. data/lib/unisender/validation.rb +11 -9
  91. data/lib/unisender/version.rb +1 -1
  92. data/lib/unisender/webhooks/tokens.rb +38 -0
  93. data/lib/unisender/webhooks.rb +230 -0
  94. data/lib/unisender.rb +42 -0
  95. data/sig/acceptance.rbs +30 -0
  96. data/sig/attachment.rbs +15 -0
  97. data/sig/batch_summary.rbs +38 -0
  98. data/sig/campaign_workflow.rbs +20 -0
  99. data/sig/configuration.rbs +62 -0
  100. data/sig/connection_controls.rbs +55 -0
  101. data/sig/diagnostics.rbs +37 -0
  102. data/sig/domain_records.rbs +96 -0
  103. data/sig/downloads.rbs +53 -0
  104. data/sig/execution.rbs +52 -0
  105. data/sig/execution_context.rbs +46 -0
  106. data/sig/hooks.rbs +18 -0
  107. data/sig/inputs.rbs +84 -0
  108. data/sig/iterators.rbs +11 -0
  109. data/sig/mail_inputs.rbs +8 -0
  110. data/sig/manifest.yaml +11 -0
  111. data/sig/operation_contracts.rbs +28 -0
  112. data/sig/operations.rbs +73 -69
  113. data/sig/outcomes.rbs +90 -0
  114. data/sig/record.rbs +77 -0
  115. data/sig/records.rbs +182 -0
  116. data/sig/serialization.rbs +13 -0
  117. data/sig/tasks.rbs +26 -0
  118. data/sig/testing.rbs +54 -0
  119. data/sig/unisender.rbs +128 -39
  120. data/sig/webhooks.rbs +55 -0
  121. metadata +84 -3
@@ -0,0 +1,614 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unisender
4
+ # Documented identifier record fields.
5
+ class IdentifierRecord < Record
6
+ def id
7
+ identifier!("id")
8
+ end
9
+
10
+ protected
11
+
12
+ def normalize
13
+ super
14
+ @values["id"] = id
15
+ end
16
+ end
17
+
18
+ # Transport metadata and IdentifierRecord field access.
19
+ class IdentifierRecordResponse < ObjectResponse
20
+ def id
21
+ result.id
22
+ end
23
+ end
24
+
25
+ # Documented list fields.
26
+ class List < IdentifierRecord
27
+ def title
28
+ text!("title")
29
+ end
30
+
31
+ protected
32
+
33
+ def normalize
34
+ super
35
+ @values["title"] = title
36
+ end
37
+ end
38
+
39
+ # Transport metadata and List field access.
40
+ class ListResponse < ObjectResponse
41
+ def id
42
+ result.id
43
+ end
44
+
45
+ def title
46
+ result.title
47
+ end
48
+ end
49
+
50
+ # Documented message fields.
51
+ class Message < IdentifierRecord
52
+ def subject
53
+ text("subject")
54
+ end
55
+
56
+ def body
57
+ text("body")
58
+ end
59
+
60
+ def sender_name
61
+ text("sender_name")
62
+ end
63
+
64
+ def sender_email
65
+ text("sender_email")
66
+ end
67
+
68
+ protected
69
+
70
+ def normalize
71
+ super
72
+ @values["subject"] = subject if @values.key?("subject")
73
+ @values["body"] = body if @values.key?("body")
74
+ @values["sender_name"] = sender_name if @values.key?("sender_name")
75
+ @values["sender_email"] = sender_email if @values.key?("sender_email")
76
+ end
77
+ end
78
+
79
+ # Transport metadata and Message field access.
80
+ class MessageResponse < ObjectResponse
81
+ def id
82
+ result.id
83
+ end
84
+
85
+ def subject
86
+ result.subject
87
+ end
88
+
89
+ def body
90
+ result.body
91
+ end
92
+
93
+ def sender_name
94
+ result.sender_name
95
+ end
96
+
97
+ def sender_email
98
+ result.sender_email
99
+ end
100
+ end
101
+
102
+ # Documented campaign fields.
103
+ class Campaign < IdentifierRecord
104
+ def status
105
+ text("status")
106
+ end
107
+
108
+ def subject
109
+ text("subject")
110
+ end
111
+
112
+ protected
113
+
114
+ def normalize
115
+ super
116
+ @values["status"] = status if @values.key?("status")
117
+ @values["subject"] = subject if @values.key?("subject")
118
+ end
119
+ end
120
+
121
+ # Transport metadata and Campaign field access.
122
+ class CampaignResponse < ObjectResponse
123
+ def id
124
+ result.id
125
+ end
126
+
127
+ def status
128
+ result.status
129
+ end
130
+
131
+ def subject
132
+ result.subject
133
+ end
134
+ end
135
+
136
+ # Documented template fields.
137
+ class Template < IdentifierRecord
138
+ def title
139
+ text("title")
140
+ end
141
+
142
+ def subject
143
+ text("subject")
144
+ end
145
+
146
+ def body
147
+ text("body")
148
+ end
149
+
150
+ protected
151
+
152
+ def normalize
153
+ super
154
+ @values["title"] = title if @values.key?("title")
155
+ @values["subject"] = subject if @values.key?("subject")
156
+ @values["body"] = body if @values.key?("body")
157
+ end
158
+ end
159
+
160
+ # Transport metadata and Template field access.
161
+ class TemplateResponse < ObjectResponse
162
+ def id
163
+ result.id
164
+ end
165
+
166
+ def title
167
+ result.title
168
+ end
169
+
170
+ def subject
171
+ result.subject
172
+ end
173
+
174
+ def body
175
+ result.body
176
+ end
177
+ end
178
+
179
+ # Documented field fields.
180
+ class Field < IdentifierRecord
181
+ def name
182
+ text!("name")
183
+ end
184
+
185
+ def type
186
+ text!("type")
187
+ end
188
+
189
+ protected
190
+
191
+ def normalize
192
+ super
193
+ @values["name"] = name
194
+ @values["type"] = type
195
+ end
196
+ end
197
+
198
+ # Transport metadata and Field field access.
199
+ class FieldResponse < ObjectResponse
200
+ def id
201
+ result.id
202
+ end
203
+
204
+ def name
205
+ result.name
206
+ end
207
+
208
+ def type
209
+ result.type
210
+ end
211
+ end
212
+
213
+ # Documented tag fields.
214
+ class Tag < IdentifierRecord
215
+ def name
216
+ text!("name")
217
+ end
218
+
219
+ protected
220
+
221
+ def normalize
222
+ super
223
+ @values["name"] = name
224
+ end
225
+ end
226
+
227
+ # Transport metadata and Tag field access.
228
+ class TagResponse < ObjectResponse
229
+ def id
230
+ result.id
231
+ end
232
+
233
+ def name
234
+ result.name
235
+ end
236
+ end
237
+
238
+ # Documented note fields.
239
+ class Note < IdentifierRecord
240
+ def content
241
+ text!("content")
242
+ end
243
+
244
+ def user_id
245
+ self["user_id"].nil? ? nil : identifier!("user_id")
246
+ end
247
+
248
+ def subscriber_id
249
+ self["subscriber_id"].nil? ? nil : identifier!("subscriber_id")
250
+ end
251
+
252
+ def origin
253
+ text("origin")
254
+ end
255
+
256
+ # rubocop:disable-next Naming/PredicatePrefix -- Retains the documented field name.
257
+ def is_pinned
258
+ value = self["is_pinned"]
259
+ case value
260
+ when true, false, nil then value
261
+ else invalid!("is_pinned")
262
+ end
263
+ end
264
+
265
+ def created_at
266
+ text("created_at")
267
+ end
268
+
269
+ def updated_at
270
+ text("updated_at")
271
+ end
272
+
273
+ def pinned_at
274
+ text("pinned_at")
275
+ end
276
+
277
+ protected
278
+
279
+ def normalize
280
+ super
281
+ @values["content"] = content
282
+ user_id
283
+ subscriber_id
284
+ origin
285
+ is_pinned
286
+ created_at
287
+ updated_at
288
+ pinned_at
289
+ end
290
+ end
291
+
292
+ # Transport metadata and Note field access.
293
+ class NoteResponse < ObjectResponse
294
+ def id
295
+ result.id
296
+ end
297
+
298
+ def content
299
+ result.content
300
+ end
301
+
302
+ def user_id
303
+ result.user_id
304
+ end
305
+
306
+ def subscriber_id
307
+ result.subscriber_id
308
+ end
309
+
310
+ def origin
311
+ result.origin
312
+ end
313
+
314
+ # rubocop:disable-next Naming/PredicatePrefix -- Retains the documented field name.
315
+ def is_pinned
316
+ result.is_pinned
317
+ end
318
+
319
+ def created_at
320
+ result.created_at
321
+ end
322
+
323
+ def updated_at
324
+ result.updated_at
325
+ end
326
+
327
+ def pinned_at
328
+ result.pinned_at
329
+ end
330
+ end
331
+
332
+ # Documented message created fields.
333
+ class MessageCreated < Record
334
+ def message_id
335
+ identifier!("message_id")
336
+ end
337
+
338
+ protected
339
+
340
+ def normalize
341
+ super
342
+ @values["message_id"] = message_id
343
+ end
344
+ end
345
+
346
+ # Transport metadata and MessageCreated field access.
347
+ class MessageCreatedResponse < ObjectResponse
348
+ def message_id
349
+ result.message_id
350
+ end
351
+ end
352
+
353
+ # Documented subscription fields.
354
+ class Subscription < Record
355
+ def person_id
356
+ identifier!("person_id")
357
+ end
358
+
359
+ protected
360
+
361
+ def normalize
362
+ super
363
+ @values["person_id"] = person_id
364
+ end
365
+ end
366
+
367
+ # Transport metadata and Subscription field access.
368
+ class SubscriptionResponse < ObjectResponse
369
+ def person_id
370
+ result.person_id
371
+ end
372
+ end
373
+
374
+ # Documented campaign created fields.
375
+ class CampaignCreated < Record
376
+ def campaign_id
377
+ identifier!("campaign_id")
378
+ end
379
+
380
+ def status
381
+ text!("status")
382
+ end
383
+
384
+ def count
385
+ integer("count")
386
+ end
387
+
388
+ protected
389
+
390
+ def normalize
391
+ super
392
+ @values["campaign_id"] = campaign_id
393
+ @values["status"] = status
394
+ @values["count"] = count if @values.key?("count")
395
+ end
396
+ end
397
+
398
+ # Transport metadata and CampaignCreated field access.
399
+ class CampaignCreatedResponse < ObjectResponse
400
+ def campaign_id
401
+ result.campaign_id
402
+ end
403
+
404
+ def status
405
+ result.status
406
+ end
407
+
408
+ def count
409
+ result.count
410
+ end
411
+ end
412
+
413
+ # Documented status fields.
414
+ class Status < Record
415
+ def status
416
+ text!("status")
417
+ end
418
+
419
+ protected
420
+
421
+ def normalize
422
+ super
423
+ @values["status"] = status
424
+ end
425
+ end
426
+
427
+ # Transport metadata and Status field access.
428
+ class StatusResponse < ObjectResponse
429
+ def status
430
+ result.status
431
+ end
432
+ end
433
+
434
+ # Documented contact count fields.
435
+ class ContactCount < Record
436
+ def count
437
+ integer!("count")
438
+ end
439
+
440
+ protected
441
+
442
+ def normalize
443
+ super
444
+ @values["count"] = count
445
+ end
446
+ end
447
+
448
+ # Transport metadata and ContactCount field access.
449
+ class ContactCountResponse < ObjectResponse
450
+ def count
451
+ result.count
452
+ end
453
+ end
454
+
455
+ # Documented total contacts fields.
456
+ class TotalContacts < Record
457
+ def total
458
+ integer!("total")
459
+ end
460
+
461
+ protected
462
+
463
+ def normalize
464
+ super
465
+ @values["total"] = total
466
+ end
467
+ end
468
+
469
+ # Transport metadata and TotalContacts field access.
470
+ class TotalContactsResponse < ObjectResponse
471
+ def total
472
+ result.total
473
+ end
474
+ end
475
+
476
+ # Documented message version fields.
477
+ class MessageVersion < Record
478
+ def message_id
479
+ identifier!("message_id")
480
+ end
481
+
482
+ def actual_version_id
483
+ identifier!("actual_version_id")
484
+ end
485
+
486
+ protected
487
+
488
+ def normalize
489
+ super
490
+ @values["message_id"] = message_id
491
+ @values["actual_version_id"] = actual_version_id
492
+ end
493
+ end
494
+
495
+ # Transport metadata and MessageVersion field access.
496
+ class MessageVersionResponse < ObjectResponse
497
+ def message_id
498
+ result.message_id
499
+ end
500
+
501
+ def actual_version_id
502
+ result.actual_version_id
503
+ end
504
+ end
505
+
506
+ # Documented web version fields.
507
+ class WebVersion < Record
508
+ def letter_id
509
+ identifier!("letter_id")
510
+ end
511
+
512
+ def web_letter_link
513
+ text!("web_letter_link")
514
+ end
515
+
516
+ protected
517
+
518
+ def normalize
519
+ super
520
+ @values["letter_id"] = letter_id
521
+ @values["web_letter_link"] = web_letter_link
522
+ end
523
+ end
524
+
525
+ # Transport metadata and WebVersion field access.
526
+ class WebVersionResponse < ObjectResponse
527
+ def letter_id
528
+ result.letter_id
529
+ end
530
+
531
+ def web_letter_link
532
+ result.web_letter_link
533
+ end
534
+ end
535
+
536
+ # Documented task result fields.
537
+ class TaskResult < Record
538
+ def task_uuid
539
+ text("task_uuid")
540
+ end
541
+
542
+ def status
543
+ text!("status")
544
+ end
545
+
546
+ def file_to_download
547
+ text("file_to_download")
548
+ end
549
+
550
+ protected
551
+
552
+ def normalize
553
+ super
554
+ @values["task_uuid"] = task_uuid if @values.key?("task_uuid")
555
+ @values["status"] = status
556
+ @values["file_to_download"] = file_to_download if @values.key?("file_to_download")
557
+ end
558
+ end
559
+
560
+ # Transport metadata and TaskResult field access.
561
+ class TaskResultResponse < ObjectResponse
562
+ def task_uuid
563
+ result.task_uuid
564
+ end
565
+
566
+ def status
567
+ result.status
568
+ end
569
+
570
+ def file_to_download
571
+ result.file_to_download
572
+ end
573
+ end
574
+
575
+ # An identifier omitted by legacy empty mutation responses.
576
+ class OptionalIdentifier < Record
577
+ def id
578
+ self["id"].nil? ? nil : identifier!("id")
579
+ end
580
+
581
+ protected
582
+
583
+ def normalize
584
+ id
585
+ end
586
+ end
587
+
588
+ # Transport metadata and an optional mutation identifier.
589
+ class OptionalIdentifierResponse < ObjectResponse
590
+ def id
591
+ result.id
592
+ end
593
+ end
594
+
595
+ # A service notice returned after a test send.
596
+ class MessageNotice < Record
597
+ def message
598
+ text("message")
599
+ end
600
+
601
+ protected
602
+
603
+ def normalize
604
+ message
605
+ end
606
+ end
607
+
608
+ # Transport metadata and a service notice.
609
+ class MessageNoticeResponse < ObjectResponse
610
+ def message
611
+ result.message
612
+ end
613
+ end
614
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unisender
4
+ # Observes the complete named response decode inside each request attempt.
5
+ class Client
6
+ def request(method, params: {})
7
+ perform_request(method, params, named: false) # : Response[json]
8
+ end
9
+
10
+ private
11
+
12
+ # rubocop:disable-next Metrics/MethodLength -- One attempt owns request construction, decoding, and error context.
13
+ def perform_request(method, params, named:)
14
+ uri, headers, body = request_values(method, params)
15
+ attempt = 0
16
+ with_retries(method) do
17
+ attempt += 1
18
+ exchange(method, uri, headers:, body:, attempt:,
19
+ input_count: named ? Acceptance.input_count(method, params) : nil) do |http|
20
+ decode_response(http, method, params, named)
21
+ end
22
+ end
23
+ rescue Error, ValidationError => e
24
+ raise e.with_operation(method), cause: nil
25
+ end
26
+
27
+ def with_retries(method, &)
28
+ # rubocop:disable-next Style/ClassEqualityComparison -- Preserve custom subclass types in Steep's fallback.
29
+ if @retry_policy.class == RetryPolicy
30
+ @retry_policy.call(method, execution: @execution, &)
31
+ else
32
+ @retry_policy.call(method, &)
33
+ end
34
+ end
35
+
36
+ def request_values(method, params)
37
+ validate_request(method, params)
38
+ uri = URI("https://api.unisender.com/#{@language}/api/#{method}")
39
+ headers = { "Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8", "User-Agent" => USER_AGENT }
40
+ body = Form.encode(params.merge("api_key" => @api_key, "format" => "json"))
41
+ [uri, headers, body]
42
+ end
43
+
44
+ def decode_response(http, method, params, named)
45
+ response = Protocol.parse(http, method:)
46
+ return response unless named
47
+
48
+ result = bind_task(method, Results.build(method, response), params)
49
+ observe_acceptance(method, Acceptance.input_count(method, params), result, nil)
50
+ result
51
+ end
52
+
53
+ # rubocop:disable-next Metrics/MethodLength, Metrics/ParameterLists -- Keep attempt metadata with its exchange.
54
+ def exchange(operation, uri, headers:, body:, attempt: 1, input_count: nil)
55
+ check_execution!
56
+ acquire_limit(operation)
57
+ check_execution!
58
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
59
+ response = nil
60
+ begin
61
+ response = post_request(uri, headers, body)
62
+ result = yield response
63
+ rescue StandardError => e
64
+ e.with_http(status: response&.status, request_id: response&.headers&.[]("x-request-id")) if e.is_a?(Error)
65
+ observe_acceptance(operation, input_count, nil, e)
66
+ observe(operation, attempt, started, response, e)
67
+ raise
68
+ end
69
+ observe(operation, attempt, started, response, nil)
70
+ result
71
+ end
72
+
73
+ def acquire_limit(operation)
74
+ limiter = operation == "track_events" ? @configuration.event_rate_limiter : @configuration.rate_limiter
75
+ return unless limiter
76
+
77
+ # rubocop:disable-next Style/ClassEqualityComparison -- Preserve custom subclass types in Steep's fallback.
78
+ if limiter.class == RateLimiter
79
+ limiter.acquire(operation, execution: @execution)
80
+ else
81
+ limiter.acquire(operation)
82
+ end
83
+ end
84
+
85
+ def post_request(uri, headers, body)
86
+ transport = @transport
87
+ case transport
88
+ when Transport
89
+ if [Transport, PersistentTransport].include?(transport.class)
90
+ return transport.post(uri, headers:, body:, execution: @execution)
91
+ end
92
+ end
93
+ transport.post(uri, headers:, body:)
94
+ end
95
+
96
+ def observe(operation, attempt, started, response, error)
97
+ event = Observation.event(operation, attempt, started, response, error)
98
+ Observation.emit(@configuration.instrumentation, event)
99
+ end
100
+
101
+ def validate_request(method, params)
102
+ raise ArgumentError, "Invalid API method name" unless method.match?(%r{\A(?:async/)?[A-Za-z][A-Za-z0-9]*\z})
103
+ return unless params.keys.any? { |key| RESERVED_PARAMETERS.include?(key.to_s) }
104
+
105
+ raise ArgumentError, "api_key and format are managed by the client"
106
+ end
107
+ end
108
+ end