assinafy 1.5.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +168 -0
- data/LICENSE +19 -0
- data/README.md +463 -0
- data/lib/assinafy/client.rb +245 -0
- data/lib/assinafy/configuration.rb +157 -0
- data/lib/assinafy/errors.rb +83 -0
- data/lib/assinafy/null_logger.rb +9 -0
- data/lib/assinafy/resources/account_resource.rb +267 -0
- data/lib/assinafy/resources/assignment_resource.rb +476 -0
- data/lib/assinafy/resources/auth_resource.rb +251 -0
- data/lib/assinafy/resources/base_resource.rb +250 -0
- data/lib/assinafy/resources/document_resource.rb +912 -0
- data/lib/assinafy/resources/field_resource.rb +274 -0
- data/lib/assinafy/resources/signer_document_resource.rb +222 -0
- data/lib/assinafy/resources/signer_resource.rb +422 -0
- data/lib/assinafy/resources/tag_resource.rb +183 -0
- data/lib/assinafy/resources/template_resource.rb +215 -0
- data/lib/assinafy/resources/user_resource.rb +182 -0
- data/lib/assinafy/resources/webhook_resource.rb +222 -0
- data/lib/assinafy/support/webhook_verifier.rb +129 -0
- data/lib/assinafy/utils.rb +122 -0
- data/lib/assinafy/version.rb +5 -0
- data/lib/assinafy.rb +25 -0
- metadata +195 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Assinafy
|
|
4
|
+
module Resources
|
|
5
|
+
# Authentication and API key management.
|
|
6
|
+
#
|
|
7
|
+
# See https://api.assinafy.com.br/v1/docs#authentication for the full
|
|
8
|
+
# documentation of these endpoints.
|
|
9
|
+
class AuthResource < BaseResource
|
|
10
|
+
# Authenticate with email and password.
|
|
11
|
+
#
|
|
12
|
+
# The returned `access_token` is a JWT that typically expires in one hour. For long-lived
|
|
13
|
+
# back-end integrations, prefer an API key (see #create_api_key) over the access token.
|
|
14
|
+
#
|
|
15
|
+
# @param email [String]
|
|
16
|
+
# @param password [String]
|
|
17
|
+
# @return [Hash] unwrapped payload: { "access_token" => String, "user" => Hash, "accounts" => Array<Hash> }
|
|
18
|
+
# @raise [Assinafy::ApiError] on a non-2xx response
|
|
19
|
+
#
|
|
20
|
+
# @see POST /login
|
|
21
|
+
#
|
|
22
|
+
# @example Request and response
|
|
23
|
+
# resource.login(email: 'user@example.com', password: 'secret')
|
|
24
|
+
# # Request body sent by the SDK:
|
|
25
|
+
# # { "email": "user@example.com", "password": "secret" }
|
|
26
|
+
# #
|
|
27
|
+
# # Returns the unwrapped data payload (envelope { status, message, data } stripped):
|
|
28
|
+
# # {
|
|
29
|
+
# # "access_token" => "access-token-placeholder",
|
|
30
|
+
# # "user" => {
|
|
31
|
+
# # "id" => "user-id", "name" => "Example User",
|
|
32
|
+
# # "email" => "user@example.com", "telephone" => "+15555550100",
|
|
33
|
+
# # "government_id" => "00000000000", "is_email_verified" => false,
|
|
34
|
+
# # "has_accepted_terms" => true, "created_at" => "2023-03-03T11:51:34Z",
|
|
35
|
+
# # "to_be_deleted_at" => nil
|
|
36
|
+
# # },
|
|
37
|
+
# # "accounts" => [
|
|
38
|
+
# # { "id" => "account-id", "name" => "Example Workspace", "roles" => ["owner"],
|
|
39
|
+
# # "is_delete_allowed" => true, "created_at" => "2023-03-03T11:51:34Z" }
|
|
40
|
+
# # ]
|
|
41
|
+
# # }
|
|
42
|
+
def login(email:, password:)
|
|
43
|
+
call('Failed to login') do
|
|
44
|
+
@connection.post('login', body_params(email: email, password: password))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Authenticate with a third-party identity provider token.
|
|
49
|
+
#
|
|
50
|
+
# Currently the only supported provider is `google`. Returns the same shape as #login.
|
|
51
|
+
#
|
|
52
|
+
# @param provider [String] the provider type; currently only `google`
|
|
53
|
+
# @param token [String] provider-issued OAuth/OIDC access or ID token
|
|
54
|
+
# @param has_accepted_terms [Boolean]
|
|
55
|
+
# @return [Hash] unwrapped payload: { "access_token" => String, "user" => Hash, "accounts" => Array<Hash> }
|
|
56
|
+
#
|
|
57
|
+
# @see POST /authentication/social-login
|
|
58
|
+
#
|
|
59
|
+
# @example Request and response
|
|
60
|
+
# resource.social_login(provider: 'google', token: 'provider-token', has_accepted_terms: true)
|
|
61
|
+
# # Request body sent by the SDK:
|
|
62
|
+
# # { "provider": "google", "token": "provider-token", "has_accepted_terms": true }
|
|
63
|
+
# #
|
|
64
|
+
# # Returns the unwrapped data payload (envelope stripped); same shape as #login:
|
|
65
|
+
# # {
|
|
66
|
+
# # "access_token" => "access-token-placeholder",
|
|
67
|
+
# # "user" => { "id" => "user-id", "name" => "Example User", ... },
|
|
68
|
+
# # "accounts" => [
|
|
69
|
+
# # { "id" => "account-id", "name" => "Example Workspace", "roles" => ["owner"],
|
|
70
|
+
# # "is_delete_allowed" => true, "created_at" => "2023-03-03T11:51:34Z" }
|
|
71
|
+
# # ]
|
|
72
|
+
# # }
|
|
73
|
+
def social_login(provider:, token:, has_accepted_terms:)
|
|
74
|
+
call('Failed to login with social provider') do
|
|
75
|
+
@connection.post(
|
|
76
|
+
'authentication/social-login',
|
|
77
|
+
body_params(
|
|
78
|
+
provider: provider,
|
|
79
|
+
token: token,
|
|
80
|
+
has_accepted_terms: has_accepted_terms
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Link a third-party identity provider to the authenticated user's account.
|
|
87
|
+
#
|
|
88
|
+
# @param provider [String] the provider type; currently only `google`
|
|
89
|
+
# @param token [String] provider-issued OAuth/OIDC token
|
|
90
|
+
# @return [nil] the documented success envelope has no `data` payload
|
|
91
|
+
# @see POST /auth/link-social-login
|
|
92
|
+
# @example Link a Google account
|
|
93
|
+
# client.auth.link_social_login(provider: 'google', token: 'provider-token')
|
|
94
|
+
# # Request body sent by the SDK:
|
|
95
|
+
# # { "provider": "google", "token": "provider-token" }
|
|
96
|
+
# # Response: { "status": 200, "message": "Provider linked" }
|
|
97
|
+
# # => nil
|
|
98
|
+
def link_social_login(provider:, token:)
|
|
99
|
+
call('Failed to link social login') do
|
|
100
|
+
@connection.post('auth/link-social-login', body_params(provider: provider, token: token))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Generate a new API key for the authenticated user.
|
|
105
|
+
#
|
|
106
|
+
# The returned key is shown in full only once, here; afterwards #get_api_key returns a masked
|
|
107
|
+
# version. IMPORTANT: generating a new key deletes (invalidates) the previous one. Send the key
|
|
108
|
+
# via the `X-Api-Key` header and never expose it in a front-end application.
|
|
109
|
+
#
|
|
110
|
+
# @param password [String] the user's current password
|
|
111
|
+
# @return [Hash] unwrapped payload: { "api_key" => String } (the new key, in full)
|
|
112
|
+
#
|
|
113
|
+
# @see POST /users/api-keys
|
|
114
|
+
#
|
|
115
|
+
# @example Request and response
|
|
116
|
+
# resource.create_api_key(password: 'secret')
|
|
117
|
+
# # Request body sent by the SDK:
|
|
118
|
+
# # { "password": "secret" }
|
|
119
|
+
# #
|
|
120
|
+
# # Returns the unwrapped data payload (envelope stripped):
|
|
121
|
+
# # { "api_key" => "api-key-created-once" }
|
|
122
|
+
def create_api_key(password:)
|
|
123
|
+
call('Failed to create API key') do
|
|
124
|
+
@connection.post('users/api-keys', body_params(password: password))
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Retrieve the active API key for the authenticated user.
|
|
129
|
+
#
|
|
130
|
+
# For security the key is returned MASKED (only the last 4 characters are visible); the full key
|
|
131
|
+
# is only available once, at #create_api_key time. Returns `nil` if no key has been generated yet.
|
|
132
|
+
# This endpoint works with `X-Api-Key` authentication (verified live), not only a Bearer token.
|
|
133
|
+
#
|
|
134
|
+
# @return [Hash, nil] unwrapped payload: { "api_key" => String } (masked), or nil if no key exists yet
|
|
135
|
+
# @see GET /users/api-keys
|
|
136
|
+
#
|
|
137
|
+
# @example Request and response (key exists)
|
|
138
|
+
# resource.get_api_key
|
|
139
|
+
# # No request body (GET).
|
|
140
|
+
# #
|
|
141
|
+
# # Returns the unwrapped data payload (envelope stripped):
|
|
142
|
+
# # { "api_key" => "************************************************************9Jdr" }
|
|
143
|
+
#
|
|
144
|
+
# @example Response when no key has been generated yet
|
|
145
|
+
# resource.get_api_key # => nil
|
|
146
|
+
def get_api_key
|
|
147
|
+
call('Failed to get API key') do
|
|
148
|
+
@connection.get('users/api-keys')
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
alias api_key get_api_key
|
|
153
|
+
|
|
154
|
+
# Delete the API key of the authenticated user.
|
|
155
|
+
#
|
|
156
|
+
# The SDK ignores the response body and always returns `nil` on success. (The API itself
|
|
157
|
+
# responds with an empty `data` payload.)
|
|
158
|
+
#
|
|
159
|
+
# @return [nil]
|
|
160
|
+
# @see DELETE /users/api-keys
|
|
161
|
+
#
|
|
162
|
+
# @example Request and response
|
|
163
|
+
# resource.delete_api_key
|
|
164
|
+
# # No request body (DELETE).
|
|
165
|
+
# #
|
|
166
|
+
# # Returns nil on success (the API's empty `data` payload is discarded).
|
|
167
|
+
# # => nil
|
|
168
|
+
def delete_api_key
|
|
169
|
+
call_void('Failed to delete API key') do
|
|
170
|
+
@connection.delete('users/api-keys')
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Change the authenticated user's password.
|
|
175
|
+
#
|
|
176
|
+
# @param email [String]
|
|
177
|
+
# @param password [String] current password
|
|
178
|
+
# @param new_password [String] the new password to set
|
|
179
|
+
# @return [Hash] unwrapped payload: { "email" => String }
|
|
180
|
+
#
|
|
181
|
+
# @see PUT /authentication/change-password
|
|
182
|
+
#
|
|
183
|
+
# @example Request and response
|
|
184
|
+
# resource.change_password(email: 'user@example.com', password: 'current-password',
|
|
185
|
+
# new_password: 'new-password')
|
|
186
|
+
# # Request body sent by the SDK:
|
|
187
|
+
# # { "email": "user@example.com", "password": "current-password",
|
|
188
|
+
# # "new_password": "new-password" }
|
|
189
|
+
# #
|
|
190
|
+
# # Returns the unwrapped data payload (envelope stripped):
|
|
191
|
+
# # { "email" => "user@example.com" }
|
|
192
|
+
def change_password(email:, password:, new_password:)
|
|
193
|
+
call('Failed to change password') do
|
|
194
|
+
@connection.put(
|
|
195
|
+
'authentication/change-password',
|
|
196
|
+
body_params(email: email, password: password, new_password: new_password)
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Trigger a password-reset email for the given account.
|
|
202
|
+
#
|
|
203
|
+
# Used when the user forgot their password or has not set one yet. An email with a reset token
|
|
204
|
+
# is sent; pass that token to #reset_password to complete the flow.
|
|
205
|
+
#
|
|
206
|
+
# @param email [String]
|
|
207
|
+
# @return [Hash] unwrapped payload: { "email" => String }
|
|
208
|
+
#
|
|
209
|
+
# @see PUT /authentication/request-password-reset
|
|
210
|
+
#
|
|
211
|
+
# @example Request and response
|
|
212
|
+
# resource.request_password_reset(email: 'user@example.com')
|
|
213
|
+
# # Request body sent by the SDK:
|
|
214
|
+
# # { "email": "user@example.com" }
|
|
215
|
+
# #
|
|
216
|
+
# # Returns the unwrapped data payload (envelope stripped):
|
|
217
|
+
# # { "email" => "user@example.com" }
|
|
218
|
+
def request_password_reset(email:)
|
|
219
|
+
call('Failed to request password reset') do
|
|
220
|
+
@connection.put('authentication/request-password-reset', body_params(email: email))
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Reset the password using the token sent via #request_password_reset.
|
|
225
|
+
#
|
|
226
|
+
# @param email [String]
|
|
227
|
+
# @param new_password [String] the new password to set
|
|
228
|
+
# @param token [String, nil] reset token from the email; omitted from the body when nil
|
|
229
|
+
# @return [Hash] unwrapped payload: { "email" => String }
|
|
230
|
+
#
|
|
231
|
+
# @see PUT /authentication/reset-password
|
|
232
|
+
#
|
|
233
|
+
# @example Request and response
|
|
234
|
+
# resource.reset_password(email: 'user@example.com', new_password: 'new-password',
|
|
235
|
+
# token: 'reset-token')
|
|
236
|
+
# # Request body sent by the SDK (nil token would be omitted by body_params):
|
|
237
|
+
# # { "email": "user@example.com", "token": "reset-token", "new_password": "new-password" }
|
|
238
|
+
# #
|
|
239
|
+
# # Returns the unwrapped data payload (envelope stripped):
|
|
240
|
+
# # { "email" => "user@example.com" }
|
|
241
|
+
def reset_password(email:, new_password:, token: nil)
|
|
242
|
+
call('Failed to reset password') do
|
|
243
|
+
@connection.put(
|
|
244
|
+
'authentication/reset-password',
|
|
245
|
+
body_params(email: email, token: token, new_password: new_password)
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Assinafy
|
|
4
|
+
module Resources
|
|
5
|
+
# Shared plumbing for every resource: a Faraday connection, an optional
|
|
6
|
+
# default account ID, parameter normalisation, response-envelope handling,
|
|
7
|
+
# pagination header parsing, and a single `request`/`call` pipeline that
|
|
8
|
+
# wraps Faraday and Assinafy errors into the SDK's own error hierarchy.
|
|
9
|
+
class BaseResource
|
|
10
|
+
PATH_SEGMENT = /\A[A-Za-z0-9._~-]+\z/
|
|
11
|
+
PAGINATION_HEADERS = {
|
|
12
|
+
current_page: 'x-pagination-current-page',
|
|
13
|
+
per_page: 'x-pagination-per-page',
|
|
14
|
+
total: 'x-pagination-total-count',
|
|
15
|
+
last_page: 'x-pagination-page-count'
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# @param connection [Faraday::Connection]
|
|
19
|
+
# @param default_account_id [String, nil] used by `account_id` when no override is passed
|
|
20
|
+
# @param logger [Logger, nil]
|
|
21
|
+
def initialize(connection, default_account_id = nil, logger = nil)
|
|
22
|
+
@connection = connection
|
|
23
|
+
@default_account_id = default_account_id
|
|
24
|
+
@logger = logger || NullLogger.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def account_id(explicit = nil)
|
|
30
|
+
require_id(explicit.nil? ? @default_account_id : explicit, 'Account ID')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Ensure a required scalar argument is present (not nil/blank).
|
|
34
|
+
# `require_id` is kept as an intention-revealing alias for path IDs.
|
|
35
|
+
def require_present(value, name)
|
|
36
|
+
return value unless value.nil? || value.to_s.strip.empty?
|
|
37
|
+
|
|
38
|
+
raise ValidationError.new("#{name} is required")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def require_id(value, name)
|
|
42
|
+
id = require_present(value, name)
|
|
43
|
+
raise ValidationError.new("#{name} must be a String") unless id.is_a?(String)
|
|
44
|
+
|
|
45
|
+
return id if PATH_SEGMENT.match?(id) && id != '.' && id != '..'
|
|
46
|
+
|
|
47
|
+
raise ValidationError.new("#{name} contains invalid characters")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def require_boolean(value, name)
|
|
51
|
+
return value if [true, false].include?(value)
|
|
52
|
+
|
|
53
|
+
raise ValidationError.new("#{name} must be true or false")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def require_payload(payload, name = 'Payload')
|
|
57
|
+
raise ValidationError.new("#{name} must be a Hash") unless payload.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
payload
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def require_array(value, name)
|
|
63
|
+
return value if value.is_a?(Array) && !value.empty?
|
|
64
|
+
|
|
65
|
+
raise ValidationError.new("#{name} must be a non-empty Array")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def query_params(params)
|
|
69
|
+
Utils.query_params(params)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def body_params(params)
|
|
73
|
+
Utils.body_params(params)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Normalise an upload source into `[buffer, file_name]`. Shared by every
|
|
77
|
+
# multipart upload endpoint (document upload, template create, account
|
|
78
|
+
# logo). Accepts a path String, or a Hash with `:file_path` (path) **or**
|
|
79
|
+
# `:buffer` + `:file_name` (raw bytes).
|
|
80
|
+
#
|
|
81
|
+
# @param source [String, Hash]
|
|
82
|
+
# @param max_bytes [Integer, nil] read at most one byte beyond this limit
|
|
83
|
+
# @return [Array(String, String)] `[buffer, file_name]`
|
|
84
|
+
# @raise [ValidationError] on an unusable source
|
|
85
|
+
def read_source(source, max_bytes: nil)
|
|
86
|
+
case source
|
|
87
|
+
when String
|
|
88
|
+
[read_file(source, max_bytes), File.basename(source)]
|
|
89
|
+
when Hash
|
|
90
|
+
if source[:buffer]
|
|
91
|
+
raise ValidationError.new('file_name is required when uploading a buffer') unless source[:file_name]
|
|
92
|
+
|
|
93
|
+
[source[:buffer], source[:file_name]]
|
|
94
|
+
elsif source[:file_path]
|
|
95
|
+
[read_file(source[:file_path], max_bytes), source[:file_name] || File.basename(source[:file_path])]
|
|
96
|
+
else
|
|
97
|
+
raise ValidationError.new('Invalid upload source: provide :file_path or :buffer')
|
|
98
|
+
end
|
|
99
|
+
else
|
|
100
|
+
raise ValidationError.new('Invalid upload source: provide a path String or a Hash with :file_path/:buffer')
|
|
101
|
+
end
|
|
102
|
+
rescue SystemCallError, ArgumentError, TypeError => e
|
|
103
|
+
raise ValidationError.new("Unable to read upload source: #{e.message}", { cause: e.class.name })
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def read_file(path, max_bytes)
|
|
107
|
+
return File.binread(path) unless max_bytes
|
|
108
|
+
|
|
109
|
+
File.binread(path, max_bytes + 1)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Wrap raw bytes in a Faraday::FilePart for a multipart request body.
|
|
113
|
+
#
|
|
114
|
+
# @param buffer [String] raw file bytes
|
|
115
|
+
# @param file_name [String]
|
|
116
|
+
# @param content_type [String] MIME type (defaults to sniffing the extension)
|
|
117
|
+
# @return [Faraday::FilePart]
|
|
118
|
+
def file_part(buffer, file_name, content_type = nil)
|
|
119
|
+
Faraday::FilePart.new(StringIO.new(buffer), content_type || mime_type_for(file_name), file_name)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def mime_type_for(file_name)
|
|
123
|
+
case File.extname(file_name.to_s).downcase
|
|
124
|
+
when '.pdf' then 'application/pdf'
|
|
125
|
+
when '.png' then 'image/png'
|
|
126
|
+
when '.jpg', '.jpeg' then 'image/jpeg'
|
|
127
|
+
when '.gif' then 'image/gif'
|
|
128
|
+
when '.webp' then 'image/webp'
|
|
129
|
+
when '.svg' then 'image/svg+xml'
|
|
130
|
+
else 'application/octet-stream'
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def http_get(path, params = {})
|
|
135
|
+
@connection.get(path, query_params(params))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def http_post(path, body = nil, params = {})
|
|
139
|
+
@connection.post(path) do |request|
|
|
140
|
+
request.params.update(query_params(params))
|
|
141
|
+
request.body = body unless body.nil?
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def http_put(path, body = nil, params = {})
|
|
146
|
+
@connection.put(path) do |request|
|
|
147
|
+
request.params.update(query_params(params))
|
|
148
|
+
request.body = body unless body.nil?
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def http_patch(path, body = nil, params = {})
|
|
153
|
+
@connection.patch(path) do |request|
|
|
154
|
+
request.params.update(query_params(params))
|
|
155
|
+
request.body = body unless body.nil?
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def http_delete(path, params = {}, body: nil)
|
|
160
|
+
@connection.delete(path) do |request|
|
|
161
|
+
request.params.update(query_params(params))
|
|
162
|
+
request.body = body unless body.nil?
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def call(label)
|
|
167
|
+
Utils.handle_assinafy_response(request(label) { yield }.body)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def call_optional(label)
|
|
171
|
+
call(label) { yield }
|
|
172
|
+
rescue ApiError => e
|
|
173
|
+
raise unless e.status_code == 404
|
|
174
|
+
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def call_void(label)
|
|
179
|
+
call(label) { yield }
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def call_binary(label)
|
|
184
|
+
body = request(label) { yield }.body
|
|
185
|
+
body = Utils.handle_assinafy_response(body) if body.is_a?(Hash)
|
|
186
|
+
return ''.b if body.nil?
|
|
187
|
+
return body.b if body.is_a?(String)
|
|
188
|
+
|
|
189
|
+
raise Assinafy::Error.new("#{label}: expected a binary response", { body_class: body.class.name })
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def call_list(label)
|
|
193
|
+
response = request(label) { yield }
|
|
194
|
+
body = Utils.handle_assinafy_response(response.body)
|
|
195
|
+
result = { data: extract_list_data(body) }
|
|
196
|
+
meta = parse_pagination_meta(response.headers)
|
|
197
|
+
result[:meta] = meta if meta
|
|
198
|
+
result
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def request(label)
|
|
204
|
+
response = yield
|
|
205
|
+
check_status!(response, label)
|
|
206
|
+
response
|
|
207
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError => e
|
|
208
|
+
raise NetworkError.new("#{label}: #{e.message}", { cause: e.class.name })
|
|
209
|
+
rescue Assinafy::Error
|
|
210
|
+
raise
|
|
211
|
+
rescue StandardError => e
|
|
212
|
+
# Preserve the original class for debugging; Ruby keeps the original
|
|
213
|
+
# exception accessible via #cause since we re-raise inside the rescue.
|
|
214
|
+
raise Assinafy::Error.new("#{label}: #{e.message}", { cause: e.class.name })
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def check_status!(response, _label)
|
|
218
|
+
return if (200..299).cover?(response.status)
|
|
219
|
+
|
|
220
|
+
raise ApiError.from_response(response.status, response.body)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def extract_list_data(body)
|
|
224
|
+
case body
|
|
225
|
+
when Array then body
|
|
226
|
+
when Hash then body['data'] || []
|
|
227
|
+
else []
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def parse_pagination_meta(headers)
|
|
232
|
+
return nil unless headers
|
|
233
|
+
|
|
234
|
+
meta = PAGINATION_HEADERS.each_with_object({}) do |(key, header), acc|
|
|
235
|
+
value = to_int(headers[header])
|
|
236
|
+
acc[key] = value if value
|
|
237
|
+
end
|
|
238
|
+
meta.empty? ? nil : meta
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def to_int(value)
|
|
242
|
+
return nil if value.nil?
|
|
243
|
+
|
|
244
|
+
Integer(value)
|
|
245
|
+
rescue ArgumentError, TypeError
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|