nextmail 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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +183 -0
  3. data/lib/nextmail/api_paths.rb +447 -0
  4. data/lib/nextmail/client.rb +81 -0
  5. data/lib/nextmail/constants.rb +1621 -0
  6. data/lib/nextmail/credentials.rb +53 -0
  7. data/lib/nextmail/decode.rb +34 -0
  8. data/lib/nextmail/defaults.rb +114 -0
  9. data/lib/nextmail/endpoint.rb +144 -0
  10. data/lib/nextmail/errors.rb +121 -0
  11. data/lib/nextmail/http.rb +172 -0
  12. data/lib/nextmail/image_crop.rb +38 -0
  13. data/lib/nextmail/languages.rb +42 -0
  14. data/lib/nextmail/pagination.rb +63 -0
  15. data/lib/nextmail/protocol.rb +221 -0
  16. data/lib/nextmail/raw_body.rb +83 -0
  17. data/lib/nextmail/request_path.rb +50 -0
  18. data/lib/nextmail/resource.rb +109 -0
  19. data/lib/nextmail/resources/account.rb +155 -0
  20. data/lib/nextmail/resources/addresses.rb +38 -0
  21. data/lib/nextmail/resources/analytics.rb +19 -0
  22. data/lib/nextmail/resources/app_host.rb +23 -0
  23. data/lib/nextmail/resources/audiences.rb +95 -0
  24. data/lib/nextmail/resources/automations.rb +125 -0
  25. data/lib/nextmail/resources/billing.rb +76 -0
  26. data/lib/nextmail/resources/bookings.rb +100 -0
  27. data/lib/nextmail/resources/branding.rb +24 -0
  28. data/lib/nextmail/resources/broadcasts.rb +102 -0
  29. data/lib/nextmail/resources/calendar.rb +153 -0
  30. data/lib/nextmail/resources/chats.rb +62 -0
  31. data/lib/nextmail/resources/connected_mailboxes.rb +35 -0
  32. data/lib/nextmail/resources/connectors.rb +75 -0
  33. data/lib/nextmail/resources/contacts.rb +197 -0
  34. data/lib/nextmail/resources/dns_connections.rb +23 -0
  35. data/lib/nextmail/resources/domains.rb +264 -0
  36. data/lib/nextmail/resources/drafts.rb +36 -0
  37. data/lib/nextmail/resources/emails.rb +120 -0
  38. data/lib/nextmail/resources/encryption.rb +19 -0
  39. data/lib/nextmail/resources/events.rb +22 -0
  40. data/lib/nextmail/resources/exports.rb +27 -0
  41. data/lib/nextmail/resources/files.rb +82 -0
  42. data/lib/nextmail/resources/follow_ups.rb +21 -0
  43. data/lib/nextmail/resources/forms.rb +112 -0
  44. data/lib/nextmail/resources/imports.rb +106 -0
  45. data/lib/nextmail/resources/keys.rb +126 -0
  46. data/lib/nextmail/resources/knowledge.rb +137 -0
  47. data/lib/nextmail/resources/labels.rb +43 -0
  48. data/lib/nextmail/resources/languages.rb +11 -0
  49. data/lib/nextmail/resources/me.rb +57 -0
  50. data/lib/nextmail/resources/members.rb +100 -0
  51. data/lib/nextmail/resources/out_of_office.rb +28 -0
  52. data/lib/nextmail/resources/provider_imports.rb +48 -0
  53. data/lib/nextmail/resources/roles.rb +39 -0
  54. data/lib/nextmail/resources/rules.rb +73 -0
  55. data/lib/nextmail/resources/security.rb +39 -0
  56. data/lib/nextmail/resources/segments.rb +31 -0
  57. data/lib/nextmail/resources/senders.rb +19 -0
  58. data/lib/nextmail/resources/sending.rb +23 -0
  59. data/lib/nextmail/resources/settings.rb +16 -0
  60. data/lib/nextmail/resources/signatures.rb +41 -0
  61. data/lib/nextmail/resources/subscriptions.rb +55 -0
  62. data/lib/nextmail/resources/support.rb +11 -0
  63. data/lib/nextmail/resources/suppressions.rb +43 -0
  64. data/lib/nextmail/resources/tasks.rb +95 -0
  65. data/lib/nextmail/resources/temp_mail.rb +67 -0
  66. data/lib/nextmail/resources/templates.rb +153 -0
  67. data/lib/nextmail/resources/threads.rb +218 -0
  68. data/lib/nextmail/resources/tools.rb +35 -0
  69. data/lib/nextmail/resources/tracking.rb +72 -0
  70. data/lib/nextmail/resources/webhooks.rb +170 -0
  71. data/lib/nextmail/resources/workspaces.rb +40 -0
  72. data/lib/nextmail/results.rb +33 -0
  73. data/lib/nextmail/transport.rb +272 -0
  74. data/lib/nextmail/update_notice.rb +69 -0
  75. data/lib/nextmail/version.rb +5 -0
  76. data/lib/nextmail/webhook.rb +82 -0
  77. data/lib/nextmail/wire.rb +102 -0
  78. data/lib/nextmail.rb +126 -0
  79. metadata +122 -0
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nextmail
4
+ module Languages
5
+ module_function
6
+
7
+ def key(value)
8
+ value.to_s.strip.downcase
9
+ end
10
+
11
+ BY_CODE = LANGUAGES.to_h { |language| [key(language[:code]), language] }.freeze
12
+ BY_LABEL = LANGUAGES.to_h { |language| [key(language[:label]), language] }.freeze
13
+ BY_NATIVE = LANGUAGES.to_h { |language| [key(language[:native]), language] }.freeze
14
+ ALIASES = Protocol::LANGUAGE_ALIASES.to_h { |(name, code)| [key(name), key(code)] }.freeze
15
+
16
+ def by_code(code)
17
+ return nil if code.nil? || code == ""
18
+
19
+ BY_CODE[key(code)]
20
+ end
21
+
22
+ def resolve(input)
23
+ return nil if input.nil? || input == ""
24
+
25
+ wanted = key(input)
26
+ return nil if wanted.empty?
27
+ return BY_CODE[ALIASES[wanted]] if ALIASES.key?(wanted)
28
+
29
+ direct = BY_CODE[wanted] || BY_LABEL[wanted] || BY_NATIVE[wanted]
30
+ return direct if direct
31
+
32
+ base = wanted.split(Defaults::LANGUAGE_SUBTAG_PATTERN).first
33
+ return nil if base.nil? || base.empty? || base == wanted
34
+
35
+ BY_CODE[ALIASES.fetch(base, base)]
36
+ end
37
+
38
+ def rtl?(code)
39
+ resolve(code)&.fetch(:rtl, false) == true
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nextmail
4
+ module Pagination
5
+ CURSOR = Protocol::CURSOR_STYLES[:CURSOR]
6
+ PAGE_TOKEN = Protocol::CURSOR_STYLES[:PAGE_TOKEN]
7
+
8
+ module_function
9
+
10
+ def fetch_page(transport, path, query: nil, style: CURSOR, limit: nil, cursor: nil, api_key: nil)
11
+ payload = transport.request(path, query: page_query(query, style, limit, cursor), api_key:)
12
+ envelope = payload.is_a?(Hash) ? payload : {}
13
+ next_cursor = cursor_of(envelope[style[:FIELD].to_sym])
14
+
15
+ Page.new(items: items_of(envelope), has_more: more?(envelope, next_cursor), next_cursor:)
16
+ end
17
+
18
+ def iterate(transport, path, query: nil, style: CURSOR, limit: nil, cursor: nil, api_key: nil)
19
+ walk(cursor) { |current| fetch_page(transport, path, query:, style:, limit:, cursor: current, api_key:) }
20
+ end
21
+
22
+ def collect_all(transport, path, **options)
23
+ iterate(transport, path, **options).to_a
24
+ end
25
+
26
+ def fetch_list(transport, path, query: nil, api_key: nil)
27
+ payload = transport.request(path, query:, api_key:)
28
+ items_of(payload.is_a?(Hash) ? payload : {})
29
+ end
30
+
31
+ def walk(cursor, &fetch)
32
+ Enumerator.new do |yielder|
33
+ current = cursor
34
+
35
+ loop do
36
+ page = fetch.call(current)
37
+ page.items.each { |item| yielder << item }
38
+
39
+ break if !page.has_more || page.next_cursor.nil? || page.next_cursor == current
40
+
41
+ current = page.next_cursor
42
+ end
43
+ end
44
+ end
45
+
46
+ def page_query(query, style, limit, cursor)
47
+ (query || {}).merge(Protocol::QUERY_KEYS[:LIMIT] => limit, style[:PARAM] => cursor)
48
+ end
49
+
50
+ def items_of(envelope)
51
+ envelope[:data].is_a?(Array) ? envelope[:data] : []
52
+ end
53
+
54
+ def more?(envelope, next_cursor)
55
+ flag = envelope[:hasMore]
56
+ (flag == true || flag == false) ? flag : !next_cursor.nil?
57
+ end
58
+
59
+ def cursor_of(value)
60
+ (value.is_a?(String) && !value.empty?) ? value : nil
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nextmail
4
+ module Protocol
5
+ ACCESS_TOKEN_RULES = {RESERVED_PREFIX: "nm_", MAX_LENGTH: 512}.freeze
6
+
7
+ API_KEY_MODES = {LIVE: "live", TEST: "test"}.freeze
8
+
9
+ API_KEY_PREFIXES = {LIVE: "nm_live_", TEST: "nm_test_"}.freeze
10
+
11
+ CONTENT_TYPES = {
12
+ JSON: "application/json",
13
+ CALENDAR: "text/calendar",
14
+ VCARD: "text/vcard",
15
+ OCTET_STREAM: "application/octet-stream"
16
+ }.freeze
17
+
18
+ CURSOR_STYLES = {
19
+ CURSOR: {PARAM: "cursor", FIELD: "nextCursor"}.freeze,
20
+ PAGE_TOKEN: {PARAM: "pageToken", FIELD: "nextPageToken"}.freeze
21
+ }.freeze
22
+
23
+ ENV_VARS = {
24
+ API_KEY: "NEXTMAIL_API_KEY",
25
+ ACCESS_TOKEN: "NEXTMAIL_ACCESS_TOKEN",
26
+ BASE_URL: "NEXTMAIL_BASE_URL"
27
+ }.freeze
28
+
29
+ ERROR_CODES = {
30
+ INSUFFICIENT_SCOPE: "insufficient_scope",
31
+ CONSOLE_ONLY: "console_only",
32
+ UNRECOGNISED_RESPONSE: "unrecognised_response"
33
+ }.freeze
34
+
35
+ ERROR_TYPE_BY_STATUS = {
36
+ 401 => "authentication_error",
37
+ 403 => "permission_error",
38
+ 404 => "not_found_error",
39
+ 409 => "conflict_error",
40
+ 422 => "validation_error",
41
+ 429 => "rate_limit_error"
42
+ }.freeze
43
+
44
+ HEADER_KEYS = {
45
+ AUTHORIZATION: "Authorization",
46
+ ACCEPT: "Accept",
47
+ CONTENT_TYPE: "Content-Type",
48
+ USER_AGENT: "User-Agent",
49
+ IDEMPOTENCY_KEY: "Idempotency-Key",
50
+ RETRY_AFTER: "retry-after",
51
+ REQUEST_ID: "x-request-id"
52
+ }.freeze
53
+
54
+ HTTP_METHODS = {GET: "GET", POST: "POST", PUT: "PUT", PATCH: "PATCH", DELETE: "DELETE"}.freeze
55
+
56
+ IDEMPOTENCY_KEY_PREFIX = "nm-"
57
+
58
+ IMAGE_CROP_DECIMALS = 6
59
+
60
+ IMAGE_CROP_SEPARATOR = ","
61
+
62
+ LANGUAGE_ALIASES = [
63
+ ["zh", "zh-Hans"].freeze,
64
+ ["zh-cn", "zh-Hans"].freeze,
65
+ ["zh-sg", "zh-Hans"].freeze,
66
+ ["cmn", "zh-Hans"].freeze,
67
+ ["zh-tw", "zh-Hant"].freeze,
68
+ ["zh-mo", "zh-HK"].freeze,
69
+ ["iw", "he"].freeze,
70
+ ["in", "id"].freeze,
71
+ ["ji", "yi"].freeze,
72
+ ["mo", "ro"].freeze,
73
+ ["sh", "sr-Latn"].freeze,
74
+ ["sr-latn-rs", "sr-Latn"].freeze,
75
+ ["prs", "fa-AF"].freeze,
76
+ ["fa-af", "fa-AF"].freeze,
77
+ ["kmr", "ku"].freeze,
78
+ ["pt-pt", "pt"].freeze,
79
+ ["es-mx", "es-419"].freeze,
80
+ ["es-ar", "es-419"].freeze,
81
+ ["es-co", "es-419"].freeze,
82
+ ["es-cl", "es-419"].freeze
83
+ ].freeze
84
+
85
+ LIST_SEPARATOR = ","
86
+
87
+ LOCAL_HOSTS = ["localhost", "::1", "[::1]", "127.0.0.1", "0.0.0.0", "::", "[::]"].freeze
88
+
89
+ LOOPBACK_HOSTNAMES = ["localhost", "::1", "[::1]"].freeze
90
+
91
+ LOOPBACK_IPV4_ADDRESS = "127.0.0.1"
92
+
93
+ LOOPBACK_IPV6_ADDRESS = "[::1]"
94
+
95
+ QUERY_KEYS = {
96
+ LIMIT: "limit",
97
+ CURSOR: "cursor",
98
+ STATUS: "status",
99
+ FROM: "from",
100
+ TO: "to",
101
+ TIMEZONE: "timezone",
102
+ ENABLED: "enabled",
103
+ RULE_ID: "ruleId",
104
+ THREAD_ID: "threadId",
105
+ FOLDER: "folder",
106
+ QUERY: "query",
107
+ LABEL_IDS: "labelIds",
108
+ OPENED: "opened",
109
+ CLICKED: "clicked",
110
+ DAYS: "days",
111
+ MINUTES: "minutes",
112
+ GRAIN: "grain",
113
+ OFFSET_MINUTES: "offsetMinutes",
114
+ INCLUDE_MACHINE: "includeMachine",
115
+ REASSIGN_TO: "reassignTo",
116
+ SOURCE: "source",
117
+ ADDRESS: "address",
118
+ DIRECTION: "direction",
119
+ FILTER: "filter",
120
+ SEARCH: "search",
121
+ SORT: "sort",
122
+ PAGE: "page",
123
+ PAGE_SIZE: "pageSize",
124
+ VERSION: "version",
125
+ TRACKED: "tracked",
126
+ Q: "q",
127
+ AUDIENCE_IDS: "audienceIds",
128
+ AUDIENCE_ID: "audienceId",
129
+ BROADCAST_ID: "broadcastId",
130
+ BROADCAST_IDS: "broadcastIds",
131
+ EMAIL: "email",
132
+ WITHOUT_EMAIL: "withoutEmail",
133
+ BLOCKED: "blocked",
134
+ DATE_FROM: "dateFrom",
135
+ DATE_TO: "dateTo",
136
+ FROM_CONTACTS: "fromContacts",
137
+ SEMANTIC: "semantic",
138
+ REASON: "reason",
139
+ KIND: "kind",
140
+ ENDPOINT_IDS: "endpointIds",
141
+ KEY_IDS: "keyIds",
142
+ RULE_IDS: "ruleIds",
143
+ SINCE: "since",
144
+ UNTIL: "until",
145
+ FAILED_ONLY: "failedOnly",
146
+ FILENAME: "filename",
147
+ DOMAIN: "domain",
148
+ SCHEDULED_FROM: "scheduledFrom",
149
+ SCHEDULED_TO: "scheduledTo",
150
+ ADDRESSES: "addresses",
151
+ OFFSET: "offset",
152
+ REFRESH: "refresh",
153
+ CONFIRM: "confirm",
154
+ USERNAME: "username",
155
+ SCOPE: "scope",
156
+ LEVEL: "level",
157
+ PINNED: "pinned",
158
+ TITLE: "title",
159
+ ITEM_ID: "itemId",
160
+ SENDS: "sends",
161
+ DOMAINS: "domains",
162
+ PEOPLE: "people",
163
+ AI_ACTIONS: "aiActions",
164
+ INTERVAL: "interval",
165
+ CATEGORY: "category",
166
+ DEEP: "deep",
167
+ PATH: "path",
168
+ STEP_KEY: "stepKey",
169
+ NAME: "name",
170
+ CONNECTED_APPS: "connectedApps",
171
+ TASK_ID: "taskId",
172
+ RUN_ID: "runId",
173
+ VIEW: "view",
174
+ PAGE_ID: "pageId",
175
+ DURATION_MINUTES: "durationMinutes",
176
+ APP: "app",
177
+ CONNECTOR_ID: "connectorId",
178
+ INCLUDE_TEAM: "includeTeam",
179
+ EMAILS: "emails",
180
+ OCCURRENCE_START: "occurrenceStart",
181
+ AUTOMATION_IDS: "automationIds",
182
+ EVENT: "event",
183
+ OUTCOME: "outcome",
184
+ TAG: "tag",
185
+ RECIPIENT: "recipient",
186
+ GROUP_ID: "groupId",
187
+ PAGE_IDS: "pageIds",
188
+ BOOKING_ID: "bookingId",
189
+ SUGGESTED: "suggested",
190
+ CROP: "crop",
191
+ ORDER: "order",
192
+ INCLUDE_REVOKED: "includeRevoked",
193
+ START_INDEX: "startIndex",
194
+ COUNT: "count",
195
+ HIDE_LABELLED: "hideLabelled",
196
+ INACTIVE_DAYS: "inactiveDays",
197
+ MESSAGE_ID: "messageId"
198
+ }.freeze
199
+
200
+ RETRY = {
201
+ STATUSES: [408, 429, 500, 502, 503, 504].freeze,
202
+ METHODS: ["GET"].freeze,
203
+ BASE_BACKOFF_MS: 500,
204
+ MAX_BACKOFF_MS: 8000,
205
+ MAX_HONOURED_RETRY_AFTER_MS: 60000,
206
+ RATE_LIMIT_STATUS: 429
207
+ }.freeze
208
+
209
+ SEALED_ENCRYPTION_FORMATS = ["pgp-mime", "pgp-inline", "smime-encrypted"].freeze
210
+
211
+ UNSPECIFIED_HOSTNAMES = ["0.0.0.0", "::", "[::]"].freeze
212
+
213
+ WEBHOOK_SIGNATURE = {
214
+ VERSION: "v1",
215
+ TIMESTAMP: "t",
216
+ PAIR_SEPARATOR: ",",
217
+ VALUE_SEPARATOR: "=",
218
+ TOLERANCE_SECONDS: 300
219
+ }.freeze
220
+ end
221
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nextmail
4
+ module RawBody
5
+ module_function
6
+
7
+ def read(data)
8
+ case data
9
+ when String then data
10
+ when Pathname then File.binread(data)
11
+ else read_stream(data)
12
+ end
13
+ end
14
+
15
+ def size(data)
16
+ case data
17
+ when String then data.bytesize
18
+ when Pathname then File.size(data)
19
+ else stream_size(data)
20
+ end
21
+ end
22
+
23
+ def slice(data, offset, length)
24
+ case data
25
+ when String then data.byteslice(offset, length)
26
+ when Pathname then File.open(data, "rb") { |file| stream_slice(file, offset, length) }
27
+ else stream_slice(data, offset, length)
28
+ end
29
+ end
30
+
31
+ def content_type(data, explicit)
32
+ return explicit unless explicit.nil? || explicit.to_s.empty?
33
+
34
+ declared = data.respond_to?(:content_type) ? data.content_type : nil
35
+ return declared if declared.is_a?(String) && !declared.empty?
36
+
37
+ name = file_name(data)
38
+ Defaults::CONTENT_TYPES_BY_EXTENSION[File.extname(name).downcase] if name
39
+ end
40
+
41
+ def file_name(data)
42
+ return data.original_filename if data.respond_to?(:original_filename) && data.original_filename
43
+ return File.basename(data) if data.is_a?(Pathname)
44
+
45
+ File.basename(data.path) if data.respond_to?(:path) && data.path
46
+ end
47
+
48
+ def read_stream(data)
49
+ raise ArgumentError, Defaults::MESSAGES[:RAW_BODY_SHAPE] unless data.respond_to?(:read)
50
+
51
+ rewind(data)
52
+ data.read.to_s.b
53
+ end
54
+
55
+ def stream_size(data)
56
+ raise ArgumentError, Defaults::MESSAGES[:RAW_BODY_SHAPE] unless data.respond_to?(:read)
57
+ return data.size if data.respond_to?(:size)
58
+
59
+ data.stat.size
60
+ end
61
+
62
+ def stream_slice(data, offset, length)
63
+ raise ArgumentError, Defaults::MESSAGES[:RAW_BODY_SHAPE] unless data.respond_to?(:read)
64
+
65
+ position(data, offset)
66
+ data.read(length).to_s.b
67
+ end
68
+
69
+ def position(data, offset)
70
+ return data.seek(offset) if data.respond_to?(:seek)
71
+
72
+ rewind(data) if offset.zero?
73
+ rescue Errno::ESPIPE
74
+ nil
75
+ end
76
+
77
+ def rewind(data)
78
+ data.rewind if data.respond_to?(:rewind)
79
+ rescue Errno::ESPIPE
80
+ nil
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nextmail
4
+ module RequestPath
5
+ module_function
6
+
7
+ def fill(pattern, params)
8
+ values = params.transform_keys(&:to_s)
9
+
10
+ pattern.scan(Defaults::PATH_PARAM) { |(name)| assert_segment(values[name]) }
11
+
12
+ pattern
13
+ .gsub(Defaults::OPTIONAL_PATH_PARAM) { optional_segment(values[Regexp.last_match(1)]) }
14
+ .gsub(Defaults::PATH_PARAM) { encode(values[Regexp.last_match(1)]) }
15
+ end
16
+
17
+ def assert_segment(value)
18
+ text = segment_text(value)
19
+
20
+ raise ArgumentError, Defaults::MESSAGES[:EMPTY_SEGMENT] if text.nil? || text.empty?
21
+ raise ArgumentError, "#{text.inspect} #{Defaults::MESSAGES[:DOT_SEGMENT]}" if Defaults::DOT_SEGMENT.match?(text)
22
+ end
23
+
24
+ def segment_text(value)
25
+ case value
26
+ when String, Symbol then utf8(value.to_s)
27
+ when Numeric then Wire.number(value).to_s
28
+ end
29
+ end
30
+
31
+ def utf8(text)
32
+ converted = (text.encoding == Encoding::BINARY) ? text.dup.force_encoding(Encoding::UTF_8) : text.encode(Encoding::UTF_8)
33
+ raise ArgumentError, Defaults::MESSAGES[:SEGMENT_ENCODING] unless converted.valid_encoding?
34
+
35
+ converted
36
+ rescue EncodingError
37
+ raise ArgumentError, Defaults::MESSAGES[:SEGMENT_ENCODING]
38
+ end
39
+
40
+ def optional_segment(value)
41
+ (value.nil? || value == "") ? "" : "/#{encode(value)}"
42
+ end
43
+
44
+ def encode(value)
45
+ segment_text(value).to_s.gsub(Defaults::URI_COMPONENT_UNSAFE) do |character|
46
+ character.bytes.map { |byte| format("%%%02X", byte) }.join
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nextmail
4
+ class Resource
5
+ CURSOR = Pagination::CURSOR
6
+ PAGE_TOKEN = Pagination::PAGE_TOKEN
7
+
8
+ def initialize(transport)
9
+ @transport = transport
10
+ end
11
+
12
+ def inspect
13
+ "#<#{self.class.name}>"
14
+ end
15
+
16
+ private
17
+
18
+ def request(path, **options)
19
+ @transport.request(path, **options)
20
+ end
21
+
22
+ def fill(pattern, **params)
23
+ RequestPath.fill(pattern, params)
24
+ end
25
+
26
+ def payload(body, fields)
27
+ Wire.body(body, fields)
28
+ end
29
+
30
+ def query_params(**values)
31
+ values.to_h { |name, value| [Protocol::QUERY_KEYS.fetch(name.to_s.upcase.to_sym), value] }
32
+ end
33
+
34
+ def joined(values)
35
+ Wire.collection?(values) ? Wire.list(values).join(Protocol::LIST_SEPARATOR) : values
36
+ end
37
+
38
+ def flag(value)
39
+ value&.to_s
40
+ end
41
+
42
+ def instant(value)
43
+ Wire.instant(value)
44
+ end
45
+
46
+ def log_window_query(since, until_value)
47
+ query_params(since: instant(since), until: instant(until_value))
48
+ end
49
+
50
+ def log_request_query(since, until_value, failed_only, statuses, path)
51
+ status = (statuses.nil? || statuses.empty?) ? nil : joined(statuses)
52
+ log_window_query(since, until_value).merge(query_params(failed_only: flag(failed_only), status:, path:))
53
+ end
54
+
55
+ def fetch_page(path, query = nil, style: CURSOR, limit: nil, cursor: nil, api_key: nil)
56
+ Pagination.fetch_page(@transport, path, query:, style:, limit:, cursor:, api_key:)
57
+ end
58
+
59
+ def iterate_pages(path, query = nil, style: CURSOR, limit: nil, cursor: nil, api_key: nil, &)
60
+ each_or_enumerator(Pagination.iterate(@transport, path, query:, style:, limit:, cursor:, api_key:), &)
61
+ end
62
+
63
+ def each_or_enumerator(enumerator, &block)
64
+ block ? enumerator.each(&block) : enumerator
65
+ end
66
+
67
+ def collect_all(path, query = nil, style: CURSOR, limit: nil, cursor: nil, api_key: nil)
68
+ iterate_pages(path, query, style:, limit:, cursor:, api_key:).to_a
69
+ end
70
+
71
+ def fetch_list(path, query = nil, api_key: nil)
72
+ Pagination.fetch_list(@transport, path, query:, api_key:)
73
+ end
74
+
75
+ def raw_content_type(data, content_type)
76
+ RawBody.content_type(data, content_type)
77
+ end
78
+
79
+ def crop_query(crop)
80
+ query_params(crop: ImageCrop.query(crop))
81
+ end
82
+
83
+ def upload_name(data, filename)
84
+ name = (filename.nil? ? RawBody.file_name(data) : filename).to_s.strip
85
+ raise ArgumentError, Defaults::MESSAGES[:FILENAME_REQUIRED] if name.empty?
86
+
87
+ name
88
+ end
89
+
90
+ def optional_upload_name(data, filename)
91
+ filename.nil? ? RawBody.file_name(data) : filename
92
+ end
93
+
94
+ def upload_timeout(timeout = nil)
95
+ at_least(Defaults::UPLOAD_TIMEOUT, timeout)
96
+ end
97
+
98
+ def turn_timeout(timeout = nil)
99
+ at_least(Defaults::CHAT_TURN_TIMEOUT, timeout)
100
+ end
101
+
102
+ def at_least(floor, timeout)
103
+ return timeout unless timeout.nil?
104
+
105
+ base = @transport.timeout
106
+ base.to_f.positive? ? [base, floor].max : base
107
+ end
108
+ end
109
+ end