runearc 0.1.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.
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Runearc
4
+ # Every failure the SDK reports. API errors carry the API's +code+, +message+ and +fix+;
5
+ # failures before a response arrives use +status+ 0 and the codes +network_error+,
6
+ # +timeout+, +missing_api_key+ or, for webhooks, +invalid_signature+ and friends.
7
+ #
8
+ # Rescue +Runearc::Error+ for everything, or one of its subclasses:
9
+ #
10
+ # Error
11
+ # +-- ConfigurationError no API key (status 0)
12
+ # +-- ConnectionError no response arrived (status 0)
13
+ # | +-- TimeoutError
14
+ # +-- WebhookVerificationError not a genuine, fresh Runearc webhook (status 0)
15
+ # +-- ApiError the API answered with an error
16
+ # +-- InvalidRequestError 400
17
+ # +-- AuthenticationError 401
18
+ # +-- LimitReachedError 402
19
+ # +-- PermissionError 403
20
+ # +-- NotFoundError 404
21
+ # +-- ConflictError 409
22
+ # +-- RateLimitError 429
23
+ # +-- ServerError 5xx
24
+ class Error < StandardError
25
+ # @return [Integer] the HTTP status, or 0 when no response arrived
26
+ attr_reader :status
27
+
28
+ # @return [String] a stable machine readable code, such as +"invalid_from"+ or +"rate_limited"+
29
+ attr_reader :code
30
+
31
+ # @return [String, nil] the error kind: +"invalid"+, +"unauthorized"+, +"limit_reached"+, +"forbidden"+,
32
+ # +"not_found"+, +"conflict"+, +"rate_limited"+, +"unavailable"+ or +"internal"+ (derived from the
33
+ # status when the API sends none), or +"connection"+ when no response arrived
34
+ attr_reader :kind
35
+
36
+ # @return [String, nil] what to do about it, when there is something you can do
37
+ attr_reader :fix
38
+
39
+ # @return [Array<Hash>, nil] +invalid_batch+ only: every refused item (+:index+, +:code+, +:message+, +:fix+)
40
+ attr_reader :errors
41
+
42
+ # @return [Integer, nil] seconds the API asked us to wait (+Retry-After+), when it sent one
43
+ attr_reader :retry_after
44
+
45
+ # @param message [String, nil]
46
+ # @param status [Integer]
47
+ # @param code [String]
48
+ # @param kind [String, nil]
49
+ # @param fix [String, nil]
50
+ # @param errors [Array<Hash>, nil]
51
+ # @param retry_after [Integer, nil]
52
+ def initialize(message = nil, status: 0, code: "error", kind: nil, fix: nil, errors: nil, retry_after: nil)
53
+ super(message || code)
54
+ @status = status
55
+ @code = code
56
+ @kind = kind
57
+ @fix = fix
58
+ @errors = errors
59
+ @retry_after = retry_after
60
+ end
61
+
62
+ # @return [Hash] the error as plain data, for logging or a JSON response
63
+ def to_h
64
+ { name: self.class.name, status: status, code: code, kind: kind, message: message, fix: fix,
65
+ errors: errors }.compact
66
+ end
67
+ end
68
+
69
+ # No API key was given and +RUNEARC_API_KEY+ is not set. Code +missing_api_key+.
70
+ class ConfigurationError < Error
71
+ def initialize(message = nil, **fields)
72
+ super(message, code: "missing_api_key", kind: "invalid", **fields)
73
+ end
74
+ end
75
+
76
+ # No response arrived: the network failed. Code +network_error+.
77
+ class ConnectionError < Error
78
+ def initialize(message = nil, **fields)
79
+ super(message, code: "network_error", kind: "connection", **fields)
80
+ end
81
+ end
82
+
83
+ # No response arrived within the per attempt timeout. Code +timeout+.
84
+ class TimeoutError < ConnectionError
85
+ def initialize(message = nil, **fields)
86
+ super(message, code: "timeout", **fields)
87
+ end
88
+ end
89
+
90
+ # Raised by {Webhook.verify} when a payload is not a genuine, fresh Runearc webhook.
91
+ class WebhookVerificationError < Error
92
+ def initialize(message = nil, **fields)
93
+ super(message, kind: "invalid", **fields)
94
+ end
95
+ end
96
+
97
+ # The API answered with an error status.
98
+ class ApiError < Error
99
+ # Builds the error class for a failed response, chosen by its status.
100
+ #
101
+ # @param message [String]
102
+ # @param status [Integer]
103
+ # @return [ApiError]
104
+ def self.build(message, status:, **fields)
105
+ kind = fields.delete(:kind) || default_kind(status)
106
+ class_for(status).new(message, status: status, kind: kind, **fields)
107
+ end
108
+
109
+ def self.class_for(status)
110
+ case status
111
+ when 400 then InvalidRequestError
112
+ when 401 then AuthenticationError
113
+ when 402 then LimitReachedError
114
+ when 403 then PermissionError
115
+ when 404 then NotFoundError
116
+ when 409 then ConflictError
117
+ when 429 then RateLimitError
118
+ when 500.. then ServerError
119
+ else ApiError
120
+ end
121
+ end
122
+
123
+ def self.default_kind(status)
124
+ case status
125
+ when 401 then "unauthorized"
126
+ when 402 then "limit_reached"
127
+ when 403 then "forbidden"
128
+ when 404 then "not_found"
129
+ when 409 then "conflict"
130
+ when 429 then "rate_limited"
131
+ when 503 then "unavailable"
132
+ when 500.. then "internal"
133
+ else "invalid"
134
+ end
135
+ end
136
+
137
+ private_class_method :class_for, :default_kind
138
+ end
139
+
140
+ # 400: the request was refused as invalid.
141
+ class InvalidRequestError < ApiError; end
142
+
143
+ # 401: the API key is missing, wrong or revoked.
144
+ class AuthenticationError < ApiError; end
145
+
146
+ # 402: a plan limit was reached.
147
+ class LimitReachedError < ApiError; end
148
+
149
+ # 403: the key may not do this.
150
+ class PermissionError < ApiError; end
151
+
152
+ # 404: no such resource.
153
+ class NotFoundError < ApiError; end
154
+
155
+ # 409: the request conflicts with the current state, for example a reused idempotency key.
156
+ class ConflictError < ApiError; end
157
+
158
+ # 429: rate limited. {Error#retry_after} has the wait the API asked for.
159
+ class RateLimitError < ApiError; end
160
+
161
+ # 5xx: the API failed.
162
+ class ServerError < ApiError; end
163
+ end
@@ -0,0 +1,396 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Runearc
4
+ # @api private
5
+ class Resource
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ private
11
+
12
+ def call(method, path, **options)
13
+ @client.request(method, path, **options)
14
+ end
15
+
16
+ def seg(value)
17
+ Support.segment(value)
18
+ end
19
+
20
+ # `scheduled_at` may be a Time, an RFC 3339 string or the relative form (`in 1 hour`).
21
+ def email_body(email)
22
+ return email unless email.key?(:scheduled_at)
23
+
24
+ email.merge(scheduled_at: Support.time(email[:scheduled_at]))
25
+ end
26
+ end
27
+
28
+ # Sending, reading, scheduling and canceling email.
29
+ #
30
+ # Every method also takes +request_timeout:+ (seconds); the ones that change something take
31
+ # +idempotency_key:+ where noted. +deliver+ and +batch+ get a random key when you give none.
32
+ class Emails < Resource
33
+ # Sends one email (or schedules it with +scheduled_at+). Named +deliver+ because +send+ is taken in Ruby.
34
+ #
35
+ # @param email [Hash] the email as the API takes it: +:from+, +:to+, +:subject+, and optionally +:cc+,
36
+ # +:bcc+, +:reply_to+, +:html+, +:text+, +:headers+, +:attachments+, +:tags+ and +:scheduled_at+
37
+ # (a +Time+, an RFC 3339 string with an offset, or +"in 1 hour"+; at most 30 days ahead)
38
+ # @param idempotency_key [String, nil] makes the request safe to repeat: the API returns the first
39
+ # result for the same key for 24 hours
40
+ # @param request_timeout [Numeric, nil]
41
+ # @return [Hash] +{ id: "eml_..." }+
42
+ def deliver(idempotency_key: nil, request_timeout: nil, **email)
43
+ call(:post, "/v1/emails", body: email_body(email), sends_mail: true,
44
+ idempotency_key: idempotency_key, request_timeout: request_timeout)
45
+ end
46
+
47
+ # Sends up to 100 emails in one request.
48
+ #
49
+ # @param emails [Array<Hash>] each item has the shape of {#deliver}
50
+ # @param batch_validation [String, Symbol, nil] +:strict+ (default): one invalid item refuses the whole
51
+ # batch with +invalid_batch+. +:permissive+: valid items send, invalid ones come back in +:errors+.
52
+ # @param idempotency_key [String, nil]
53
+ # @param request_timeout [Numeric, nil]
54
+ # @return [Hash] +{ data: [{ id: }], errors: [...] }+ (+errors+ only when permissive and some were refused)
55
+ def batch(emails, batch_validation: nil, idempotency_key: nil, request_timeout: nil)
56
+ headers = batch_validation ? { "x-batch-validation" => batch_validation.to_s } : nil
57
+ call(:post, "/v1/emails/batch", body: emails.map { |e| email_body(e) }, headers: headers, sends_mail: true,
58
+ idempotency_key: idempotency_key, request_timeout: request_timeout)
59
+ end
60
+
61
+ # @param id [String] an email id (+eml_...+)
62
+ # @return [Hash] the email with +:status+, +:last_event+, +:recipients+, +:html+, +:text+
63
+ def get(id, request_timeout: nil)
64
+ call(:get, "/v1/emails/#{seg(id)}", request_timeout: request_timeout)
65
+ end
66
+
67
+ # @param limit [Integer, nil] 1 to 100, default 50
68
+ # @param cursor [String, nil] the +:next_cursor+ of the previous page
69
+ # @return [Hash] +{ data: [...], next_cursor: }+; +next_cursor+ is nil on the last page
70
+ def list(limit: nil, cursor: nil, request_timeout: nil)
71
+ call(:get, "/v1/emails", query: { limit: limit, cursor: cursor }, request_timeout: request_timeout)
72
+ end
73
+
74
+ # Changes when a scheduled email sends.
75
+ #
76
+ # @param id [String]
77
+ # @param scheduled_at [Time, String] a +Time+, an RFC 3339 string with an offset, or +"in 1 hour"+
78
+ # @return [Hash] +{ object: "email", id: }+
79
+ def update(id, scheduled_at:, request_timeout: nil)
80
+ call(:patch, "/v1/emails/#{seg(id)}", body: { scheduled_at: Support.time(scheduled_at) },
81
+ request_timeout: request_timeout)
82
+ end
83
+
84
+ # Shorthand for +update(id, scheduled_at: ...)+.
85
+ #
86
+ # @return [Hash]
87
+ def reschedule(id, scheduled_at, request_timeout: nil)
88
+ update(id, scheduled_at: scheduled_at, request_timeout: request_timeout)
89
+ end
90
+
91
+ # Cancels a scheduled email.
92
+ #
93
+ # @return [Hash] +{ object: "email", id: }+
94
+ def cancel(id, request_timeout: nil)
95
+ call(:post, "/v1/emails/#{seg(id)}/cancel", idempotent: true, request_timeout: request_timeout)
96
+ end
97
+ end
98
+
99
+ # The addresses you can send from.
100
+ class Senders < Resource
101
+ # @return [Hash] +{ data: [{ address:, domain:, verified:, ... }] }+
102
+ def list(request_timeout: nil)
103
+ call(:get, "/v1/senders", request_timeout: request_timeout)
104
+ end
105
+ end
106
+
107
+ # Inboxes receive mail. Give one to an agent.
108
+ class Inboxes < Resource
109
+ # @param local_part [String, nil] random when nil
110
+ # @param display_name [String, nil]
111
+ # @param domain [String, nil] a custom domain of the workspace; the shared domain when nil
112
+ # @return [Hash] the inbox
113
+ def create(local_part: nil, display_name: nil, domain: nil, request_timeout: nil)
114
+ body = { local_part: local_part, display_name: display_name, domain: domain }.compact
115
+ call(:post, "/v1/inboxes", body: body, request_timeout: request_timeout)
116
+ end
117
+
118
+ # @return [Hash] +{ data: [...] }+
119
+ def list(request_timeout: nil)
120
+ call(:get, "/v1/inboxes", request_timeout: request_timeout)
121
+ end
122
+
123
+ # @param id [String] an inbox id (+ibx_...+)
124
+ # @return [Hash]
125
+ def get(id, request_timeout: nil)
126
+ call(:get, "/v1/inboxes/#{seg(id)}", request_timeout: request_timeout)
127
+ end
128
+
129
+ # @return [nil]
130
+ def delete(id, request_timeout: nil)
131
+ call(:delete, "/v1/inboxes/#{seg(id)}", request_timeout: request_timeout)
132
+ end
133
+ end
134
+
135
+ # Messages of an inbox: read, wait for, send and reply.
136
+ class Messages < Resource
137
+ # Messages of an inbox, newest first.
138
+ #
139
+ # @param inbox_id [String]
140
+ # @param limit [Integer, nil] 1 to 100, default 50
141
+ # @param cursor [String, nil]
142
+ # @return [Hash] +{ data: [...], next_cursor: }+
143
+ def list(inbox_id, limit: nil, cursor: nil, request_timeout: nil)
144
+ call(:get, "/v1/inboxes/#{seg(inbox_id)}/messages", query: { limit: limit, cursor: cursor },
145
+ request_timeout: request_timeout)
146
+ end
147
+
148
+ # @param id [String] a message id (+msg_...+)
149
+ # @return [Hash]
150
+ def get(id, request_timeout: nil)
151
+ call(:get, "/v1/messages/#{seg(id)}", request_timeout: request_timeout)
152
+ end
153
+
154
+ # The message as received (RFC 822).
155
+ #
156
+ # @return [String]
157
+ def raw(id, request_timeout: nil)
158
+ call(:get, "/v1/messages/#{seg(id)}/raw", text: true, request_timeout: request_timeout)
159
+ end
160
+
161
+ # Blocks until a matching message arrives (up to +timeout+ seconds, max 120) and returns it
162
+ # with the codes and links found in it. Returns nil when nothing arrived in time; calling
163
+ # again is safe.
164
+ #
165
+ # @param inbox_id [String]
166
+ # @param from [String, nil] case insensitive substring of the sender address, such as +"github.com"+
167
+ # @param subject [String, nil] case insensitive substring of the subject
168
+ # @param to [String, nil] a recipient address, matched exactly
169
+ # @param timeout [Integer] seconds to wait, 1 to 120 (default 30)
170
+ # @param after [Time, String, nil] only messages received after this time; defaults to when the wait starts
171
+ # @param request_timeout [Numeric, nil] defaults to +timeout+ plus 15 seconds
172
+ # @return [Hash, nil] +{ message:, codes:, links: }+
173
+ def wait(inbox_id, from: nil, subject: nil, to: nil, timeout: 30, after: nil, request_timeout: nil)
174
+ call(:get, "/v1/inboxes/#{seg(inbox_id)}/wait",
175
+ query: { from: from, subject: subject, to: to, timeout: timeout, after: after },
176
+ request_timeout: request_timeout || (timeout + 15))
177
+ rescue ApiError => e
178
+ raise unless e.code == "wait_timeout"
179
+ end
180
+
181
+ # Sends a new message from an inbox. Named +deliver+ because +send+ is taken in Ruby.
182
+ #
183
+ # @param inbox_id [String]
184
+ # @param message [Hash] +:to+, +:subject+, and optionally +:cc+, +:bcc+, +:text+, +:html+, +:reply_to+,
185
+ # +:headers+, +:attachments+
186
+ # @param idempotency_key [String, nil]
187
+ # @return [Hash] the message
188
+ def deliver(inbox_id, idempotency_key: nil, request_timeout: nil, **message)
189
+ call(:post, "/v1/inboxes/#{seg(inbox_id)}/messages", body: message, sends_mail: true,
190
+ idempotency_key: idempotency_key,
191
+ request_timeout: request_timeout)
192
+ end
193
+
194
+ # Replies in the thread of a message.
195
+ #
196
+ # @param message_id [String]
197
+ # @param reply [Hash] +:text+, +:html+, +:reply_all+, +:headers+, +:attachments+
198
+ # @param idempotency_key [String, nil]
199
+ # @return [Hash] the reply message
200
+ def reply(message_id, idempotency_key: nil, request_timeout: nil, **reply)
201
+ call(:post, "/v1/messages/#{seg(message_id)}/reply", body: reply, sends_mail: true,
202
+ idempotency_key: idempotency_key,
203
+ request_timeout: request_timeout)
204
+ end
205
+ end
206
+
207
+ # Conversation threads.
208
+ class Threads < Resource
209
+ # @param inbox_id [String]
210
+ # @return [Hash] +{ data: [...] }+
211
+ def list(inbox_id, request_timeout: nil)
212
+ call(:get, "/v1/inboxes/#{seg(inbox_id)}/threads", request_timeout: request_timeout)
213
+ end
214
+
215
+ # @param id [String] a thread id
216
+ # @return [Hash] the thread with +:messages+
217
+ def get(id, request_timeout: nil)
218
+ call(:get, "/v1/threads/#{seg(id)}", request_timeout: request_timeout)
219
+ end
220
+ end
221
+
222
+ # The shared support inbox view across every inbox you can read.
223
+ class Conversations < Resource
224
+ # @param status [String, nil] +"open"+, +"closed"+, +"snoozed"+ or +"all"+
225
+ # @param assignee [String, nil] +"me"+, +"none"+ or a user id
226
+ # @param inbox_id [String, nil]
227
+ # @param q [String, nil] search text
228
+ # @param cursor [String, nil]
229
+ # @param limit [Integer, nil]
230
+ # @return [Hash] +{ data: [...], next_cursor: }+
231
+ def list(status: nil, assignee: nil, inbox_id: nil, q: nil, cursor: nil, limit: nil, request_timeout: nil)
232
+ query = { status: status, assignee: assignee, inbox_id: inbox_id, q: q, cursor: cursor, limit: limit }
233
+ call(:get, "/v1/conversations", query: query, request_timeout: request_timeout)
234
+ end
235
+
236
+ # @return [Hash] +{ open:, assigned_to_me:, unassigned:, by_inbox: }+
237
+ def counts(request_timeout: nil)
238
+ call(:get, "/v1/conversations/counts", request_timeout: request_timeout)
239
+ end
240
+
241
+ # Changes a conversation. Only the fields you pass are sent, and an explicit nil is sent as null.
242
+ #
243
+ # @param id [String] the thread id
244
+ # @param fields [Hash] +status:+ (+"open"+ or +"closed"+), +assignee_user_id:+ (nil unassigns),
245
+ # +snoozed_until:+ (a +Time+ or RFC 3339 string; nil wakes the conversation)
246
+ # @return [Hash] the conversation
247
+ def update(id, request_timeout: nil, **fields)
248
+ body = fields.key?(:snoozed_until) ? fields.merge(snoozed_until: Support.time(fields[:snoozed_until])) : fields
249
+ call(:patch, "/v1/conversations/#{seg(id)}", body: body, request_timeout: request_timeout)
250
+ end
251
+
252
+ # Marks the conversation read.
253
+ #
254
+ # @return [nil]
255
+ def read(id, request_timeout: nil)
256
+ call(:post, "/v1/conversations/#{seg(id)}/read", idempotent: true, request_timeout: request_timeout)
257
+ end
258
+
259
+ # Marks the conversation unread.
260
+ #
261
+ # @return [nil]
262
+ def unread(id, request_timeout: nil)
263
+ call(:delete, "/v1/conversations/#{seg(id)}/read", request_timeout: request_timeout)
264
+ end
265
+ end
266
+
267
+ # Sending and receiving domains.
268
+ class Domains < Resource
269
+ # @param name [String] such as +"acme.com"+
270
+ # @return [Hash] the domain with the DNS +:records+ to publish and +:next_step+
271
+ def create(name:, request_timeout: nil)
272
+ call(:post, "/v1/domains", body: { name: name }, request_timeout: request_timeout)
273
+ end
274
+
275
+ # @return [Hash] +{ data: [...] }+
276
+ def list(request_timeout: nil)
277
+ call(:get, "/v1/domains", request_timeout: request_timeout)
278
+ end
279
+
280
+ # @param id [String] a domain id (+dom_...+) or the domain name
281
+ # @return [Hash]
282
+ def get(id, request_timeout: nil)
283
+ call(:get, "/v1/domains/#{seg(id)}", request_timeout: request_timeout)
284
+ end
285
+
286
+ # @return [nil]
287
+ def delete(id, request_timeout: nil)
288
+ call(:delete, "/v1/domains/#{seg(id)}", request_timeout: request_timeout)
289
+ end
290
+
291
+ # Checks the DNS records now and returns the domain with each record's status.
292
+ #
293
+ # @return [Hash]
294
+ def verify(id, request_timeout: nil)
295
+ call(:post, "/v1/domains/#{seg(id)}/verify", idempotent: true, request_timeout: request_timeout)
296
+ end
297
+ end
298
+
299
+ # Webhook endpoints and their deliveries.
300
+ class Webhooks < Resource
301
+ # @param url [String]
302
+ # @param event_types [Array<String>, nil] empty or nil means every event type
303
+ # @param inbox_id [String, nil] nil means every inbox in the workspace
304
+ # @return [Hash] the webhook with its signing +:secret+ (+whsec_...+), shown once
305
+ def create(url:, event_types: nil, inbox_id: nil, request_timeout: nil)
306
+ body = { url: url, event_types: event_types, inbox_id: inbox_id }.compact
307
+ call(:post, "/v1/webhooks", body: body, request_timeout: request_timeout)
308
+ end
309
+
310
+ # @return [Hash] +{ data: [...] }+
311
+ def list(request_timeout: nil)
312
+ call(:get, "/v1/webhooks", request_timeout: request_timeout)
313
+ end
314
+
315
+ # @return [nil]
316
+ def delete(id, request_timeout: nil)
317
+ call(:delete, "/v1/webhooks/#{seg(id)}", request_timeout: request_timeout)
318
+ end
319
+
320
+ # @param id [String] the webhook id
321
+ # @param limit [Integer, nil]
322
+ # @return [Hash] +{ data: [...] }+
323
+ def deliveries(id, limit: nil, request_timeout: nil)
324
+ call(:get, "/v1/webhooks/#{seg(id)}/deliveries", query: { limit: limit }, request_timeout: request_timeout)
325
+ end
326
+
327
+ # Delivers the event of a past delivery again.
328
+ #
329
+ # @param id [String] the webhook id
330
+ # @param delivery_id [String]
331
+ # @return [Hash] the new delivery
332
+ def replay(id, delivery_id, request_timeout: nil)
333
+ call(:post, "/v1/webhooks/#{seg(id)}/deliveries/#{seg(delivery_id)}/replay", request_timeout: request_timeout)
334
+ end
335
+
336
+ # Same as {Webhook.verify}.
337
+ #
338
+ # @return [Hash] the event
339
+ def verify(**kwargs)
340
+ Webhook.verify(**kwargs)
341
+ end
342
+ end
343
+
344
+ # API keys.
345
+ class Keys < Resource
346
+ # @param name [String]
347
+ # @param permissions [String, nil] +"full"+ (default), +"sending"+ or +"inbox"+
348
+ # @param inbox_id [String, nil] inbox keys: the one inbox
349
+ # @param domain_id [String, nil] sending keys: limit to one domain (id or name)
350
+ # @return [Hash] +{ key:, secret: }+; the +secret+ is shown once
351
+ def create(name:, permissions: nil, inbox_id: nil, domain_id: nil, request_timeout: nil)
352
+ body = { name: name, permissions: permissions, inbox_id: inbox_id, domain_id: domain_id }.compact
353
+ call(:post, "/v1/keys", body: body, request_timeout: request_timeout)
354
+ end
355
+
356
+ # @return [Hash] +{ data: [...] }+
357
+ def list(request_timeout: nil)
358
+ call(:get, "/v1/keys", request_timeout: request_timeout)
359
+ end
360
+
361
+ # Revokes the key at once.
362
+ #
363
+ # @return [nil]
364
+ def delete(id, request_timeout: nil)
365
+ call(:delete, "/v1/keys/#{seg(id)}", request_timeout: request_timeout)
366
+ end
367
+ end
368
+
369
+ # Addresses mail is not sent to.
370
+ class Suppressions < Resource
371
+ # @return [Hash] +{ data: [{ address:, reason:, created_at: }] }+
372
+ def list(request_timeout: nil)
373
+ call(:get, "/v1/suppressions", request_timeout: request_timeout)
374
+ end
375
+
376
+ # Lets mail go to the address again.
377
+ #
378
+ # @param address [String]
379
+ # @return [nil]
380
+ def delete(address, request_timeout: nil)
381
+ call(:delete, "/v1/suppressions/#{seg(address)}", request_timeout: request_timeout)
382
+ end
383
+ end
384
+
385
+ # The event log.
386
+ class Events < Resource
387
+ # Events, oldest first. Pass the returned +:next_after+ as +after:+ to continue.
388
+ #
389
+ # @param after [String, nil] an event id
390
+ # @param limit [Integer, nil]
391
+ # @return [Hash] +{ data: [...], next_after: }+
392
+ def list(after: nil, limit: nil, request_timeout: nil)
393
+ call(:get, "/v1/events", query: { after: after, limit: limit }, request_timeout: request_timeout)
394
+ end
395
+ end
396
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Runearc
6
+ # @api private
7
+ module Support
8
+ # A +Time+ becomes the RFC 3339 string the API wants; anything else is returned as given.
9
+ def self.time(value)
10
+ value.is_a?(Time) ? value.getutc.iso8601(3) : value
11
+ end
12
+
13
+ # Percent-encodes one path segment.
14
+ def self.segment(value)
15
+ value.to_s.b.gsub(/[^A-Za-z0-9\-_.~]/n) { |byte| format("%%%02X", byte.ord) }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Runearc
4
+ VERSION = "0.1.0"
5
+ end