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,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Assinafy
|
|
4
|
+
# Top-level entry point for the Assinafy Ruby SDK.
|
|
5
|
+
#
|
|
6
|
+
# A Client owns a single Faraday connection (with shared auth headers,
|
|
7
|
+
# timeouts, and User-Agent) and exposes one resource accessor per
|
|
8
|
+
# documented API surface.
|
|
9
|
+
#
|
|
10
|
+
# @example Construct from positional args
|
|
11
|
+
# client = Assinafy::Client.create(ENV['ASSINAFY_API_KEY'], ENV['ASSINAFY_ACCOUNT_ID'])
|
|
12
|
+
#
|
|
13
|
+
# @example Construct from a config Hash (e.g. parsed YAML/JSON)
|
|
14
|
+
# client = Assinafy::Client.from_config(api_key: '...', account_id: '...')
|
|
15
|
+
#
|
|
16
|
+
# @see https://api.assinafy.com.br/v1/docs
|
|
17
|
+
class Client
|
|
18
|
+
# @return [Resources::AuthResource]
|
|
19
|
+
attr_reader :auth
|
|
20
|
+
# @return [Resources::AccountResource]
|
|
21
|
+
attr_reader :accounts
|
|
22
|
+
# @return [Resources::UserResource]
|
|
23
|
+
attr_reader :users
|
|
24
|
+
# @return [Resources::DocumentResource]
|
|
25
|
+
attr_reader :documents
|
|
26
|
+
# @return [Resources::SignerResource]
|
|
27
|
+
attr_reader :signers
|
|
28
|
+
# @return [Resources::SignerDocumentResource]
|
|
29
|
+
attr_reader :signer_documents
|
|
30
|
+
# @return [Resources::AssignmentResource]
|
|
31
|
+
attr_reader :assignments
|
|
32
|
+
# @return [Resources::WebhookResource]
|
|
33
|
+
attr_reader :webhooks
|
|
34
|
+
# @return [Resources::TemplateResource]
|
|
35
|
+
attr_reader :templates
|
|
36
|
+
# @return [Resources::FieldResource]
|
|
37
|
+
attr_reader :fields
|
|
38
|
+
# @return [Resources::TagResource]
|
|
39
|
+
attr_reader :tags
|
|
40
|
+
# @return [Support::WebhookVerifier]
|
|
41
|
+
attr_reader :webhook_verifier
|
|
42
|
+
|
|
43
|
+
# @param api_key [String, nil] sent as `X-Api-Key`
|
|
44
|
+
# @param token [String, nil] legacy session token; sent as
|
|
45
|
+
# `Authorization: Bearer ...` when no `api_key` is given
|
|
46
|
+
# @param account_id [String, nil] default workspace ID for account-scoped
|
|
47
|
+
# resources; those methods document their supported per-call overrides
|
|
48
|
+
# @param base_url [String]
|
|
49
|
+
# @param webhook_secret [String, nil] secret for {Support::WebhookVerifier}
|
|
50
|
+
# @param timeout [Integer] Faraday read/open timeout in seconds
|
|
51
|
+
# @param logger [Logger, nil] receives info-level lifecycle messages
|
|
52
|
+
#
|
|
53
|
+
# @example Build a client and reach a resource accessor (no network call)
|
|
54
|
+
# client = Assinafy::Client.new(api_key: 'example_api_key', account_id: 'account_example')
|
|
55
|
+
# client.documents #=> #<Assinafy::Resources::DocumentResource ...>
|
|
56
|
+
def initialize(api_key: nil, token: nil, account_id: nil,
|
|
57
|
+
base_url: Configuration::DEFAULT_BASE_URL,
|
|
58
|
+
webhook_secret: nil,
|
|
59
|
+
timeout: Configuration::DEFAULT_TIMEOUT,
|
|
60
|
+
logger: nil)
|
|
61
|
+
config = Configuration.new(
|
|
62
|
+
api_key: api_key, token: token, account_id: account_id,
|
|
63
|
+
base_url: base_url, webhook_secret: webhook_secret,
|
|
64
|
+
timeout: timeout, logger: logger
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@connection = build_connection(config)
|
|
68
|
+
@logger = config.logger || NullLogger.new
|
|
69
|
+
|
|
70
|
+
@auth = Resources::AuthResource.new(@connection, nil, @logger)
|
|
71
|
+
@accounts = Resources::AccountResource.new(@connection, account_id, @logger)
|
|
72
|
+
@users = Resources::UserResource.new(@connection, nil, @logger)
|
|
73
|
+
@documents = Resources::DocumentResource.new(@connection, account_id, @logger)
|
|
74
|
+
@signers = Resources::SignerResource.new(@connection, account_id, @logger)
|
|
75
|
+
@signer_documents = Resources::SignerDocumentResource.new(@connection, nil, @logger)
|
|
76
|
+
@assignments = Resources::AssignmentResource.new(@connection, account_id, @logger)
|
|
77
|
+
@webhooks = Resources::WebhookResource.new(@connection, account_id, @logger)
|
|
78
|
+
@templates = Resources::TemplateResource.new(@connection, account_id, @logger)
|
|
79
|
+
@fields = Resources::FieldResource.new(@connection, account_id, @logger)
|
|
80
|
+
@tags = Resources::TagResource.new(@connection, account_id, @logger)
|
|
81
|
+
@webhook_verifier = Support::WebhookVerifier.new(webhook_secret)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Convenience constructor with positional `api_key`/`account_id`.
|
|
85
|
+
#
|
|
86
|
+
# @param api_key [String]
|
|
87
|
+
# @param account_id [String]
|
|
88
|
+
# @param options [Hash] forwarded to {#initialize}
|
|
89
|
+
# @return [Client]
|
|
90
|
+
#
|
|
91
|
+
# @example Construct with positional credentials and an optional webhook secret
|
|
92
|
+
# client = Assinafy::Client.create('example_api_key', 'account_example', webhook_secret: 'gateway_secret')
|
|
93
|
+
# client #=> #<Assinafy::Client ...>
|
|
94
|
+
def self.create(api_key, account_id, **options)
|
|
95
|
+
new(api_key: api_key, account_id: account_id, **options)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Build a Client from a Hash (string or symbol keys).
|
|
99
|
+
# Useful for credentials loaded from YAML/JSON.
|
|
100
|
+
#
|
|
101
|
+
# @param config [Hash]
|
|
102
|
+
# @return [Client]
|
|
103
|
+
#
|
|
104
|
+
# @example Build from a credentials Hash loaded from YAML/JSON (string or symbol keys both work)
|
|
105
|
+
# creds = YAML.load_file('config/assinafy.yml') # { 'api_key' => '...', 'account_id' => '...' }
|
|
106
|
+
# client = Assinafy::Client.from_config(creds)
|
|
107
|
+
# client #=> #<Assinafy::Client ...>
|
|
108
|
+
def self.from_config(config)
|
|
109
|
+
from_hash(config)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Alias of {.from_config} for symmetry with {Configuration.from_hash}.
|
|
113
|
+
#
|
|
114
|
+
# @param config [Hash]
|
|
115
|
+
# @return [Client]
|
|
116
|
+
#
|
|
117
|
+
# @example Build from a symbol-keyed Hash
|
|
118
|
+
# client = Assinafy::Client.from_hash(api_key: 'example_api_key', account_id: 'account_example')
|
|
119
|
+
# client #=> #<Assinafy::Client ...>
|
|
120
|
+
def self.from_hash(config)
|
|
121
|
+
cfg = Configuration.from_hash(config)
|
|
122
|
+
new(
|
|
123
|
+
api_key: cfg.api_key,
|
|
124
|
+
token: cfg.token,
|
|
125
|
+
account_id: cfg.account_id,
|
|
126
|
+
base_url: cfg.base_url,
|
|
127
|
+
webhook_secret: cfg.webhook_secret,
|
|
128
|
+
timeout: cfg.timeout,
|
|
129
|
+
logger: cfg.logger
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# High-level helper that bundles the most common workflow:
|
|
134
|
+
# upload PDF → (optionally wait for metadata) → create signers → create a
|
|
135
|
+
# virtual assignment for them.
|
|
136
|
+
#
|
|
137
|
+
# @param source [String, Hash] see {Resources::DocumentResource#upload}
|
|
138
|
+
# @param signers [Array<Hash>] see {Resources::SignerResource#create}
|
|
139
|
+
# @param message [String, nil]
|
|
140
|
+
# @param wait_for_ready [Boolean] poll until the document is metadata-ready (default true)
|
|
141
|
+
# @param expires_at [String, nil] ISO 8601 expiration for the assignment
|
|
142
|
+
# @param copy_receivers [Array<String>, nil] signer IDs that only receive copies
|
|
143
|
+
# @param account_id [String, nil] override the client default
|
|
144
|
+
# @return [Hash{Symbol=>Object}] `{ document: {Hash}, assignment: {Hash}, signer_ids: [String, ...] }`
|
|
145
|
+
# where `document` is the (unwrapped) document payload, `assignment` is the (unwrapped) virtual
|
|
146
|
+
# assignment, and `signer_ids` lists the IDs of the signers created during the workflow.
|
|
147
|
+
# @note This helper is not transactional. If a later API call fails, an uploaded
|
|
148
|
+
# document or newly created signer may remain and should be cleaned up by the caller.
|
|
149
|
+
#
|
|
150
|
+
# @example Upload a PDF and request a virtual signature from one signer
|
|
151
|
+
# result = client.upload_and_request_signatures(
|
|
152
|
+
# source: '/path/to/contract.pdf',
|
|
153
|
+
# signers: [{ full_name: 'Example Signer', email: 'signer@example.com' }],
|
|
154
|
+
# message: 'Please review and sign'
|
|
155
|
+
# )
|
|
156
|
+
#
|
|
157
|
+
# # Under the hood the SDK uploads the file, then POSTs this JSON body to
|
|
158
|
+
# # POST /documents/{document_id}/assignments (nil optional fields are dropped):
|
|
159
|
+
# # {
|
|
160
|
+
# # "method": "virtual",
|
|
161
|
+
# # "signers": [{ "id": "19e6b92e7895332ed9708535d8c" }],
|
|
162
|
+
# # "message": "Please review and sign"
|
|
163
|
+
# # }
|
|
164
|
+
#
|
|
165
|
+
# # Returned (unwrapped) Hash:
|
|
166
|
+
# result
|
|
167
|
+
# #=> {
|
|
168
|
+
# # document: {
|
|
169
|
+
# # "resource" => "document", "id" => "1032009d72b364f377ff270405cc",
|
|
170
|
+
# # "account_id" => "account_example", "name" => "contract.pdf",
|
|
171
|
+
# # "status" => "metadata_ready",
|
|
172
|
+
# # "artifacts" => { "original" => "https://.../download/original", "thumbnail" => "https://..." },
|
|
173
|
+
# # "tags" => [], "pages" => [{ "id" => "...", "number" => 1, "height" => 1651, "width" => 1275 }]
|
|
174
|
+
# # # ... (see docs for full shape)
|
|
175
|
+
# # },
|
|
176
|
+
# # assignment: {
|
|
177
|
+
# # "resource" => "assignment", "id" => "19e99aa0633e32ac13f845c08db",
|
|
178
|
+
# # "sender_email" => "sender@example.com", "method" => "virtual",
|
|
179
|
+
# # "expires_at" => nil, "message" => "Please review and sign",
|
|
180
|
+
# # "signers" => [{ "id" => "19e6b92e7895332ed9708535d8c", "full_name" => "Example Signer",
|
|
181
|
+
# # "email" => "signer@example.com", "completed" => false, "step" => 1 }],
|
|
182
|
+
# # "copy_receivers" => [], "items" => [{ "id" => "103200a43e372db16f48a6f0f2d4", "completed" => false }],
|
|
183
|
+
# # "summary" => { "signer_count" => 1, "completed_count" => 0 },
|
|
184
|
+
# # "signing_urls" => [{ "signer_id" => "19e6b92e7895332ed9708535d8c", "url" => "https://.../sign/..." }]
|
|
185
|
+
# # # ... (see docs for full shape)
|
|
186
|
+
# # },
|
|
187
|
+
# # signer_ids: ["19e6b92e7895332ed9708535d8c"]
|
|
188
|
+
# # }
|
|
189
|
+
def upload_and_request_signatures(source:, signers:, message: nil,
|
|
190
|
+
wait_for_ready: true, expires_at: nil,
|
|
191
|
+
copy_receivers: nil, account_id: nil)
|
|
192
|
+
unless signers.is_a?(Array) && !signers.empty? && signers.all?(Hash)
|
|
193
|
+
raise ValidationError.new('Signers must be a non-empty Array of Hashes')
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
@logger.info("Starting upload and signature workflow for #{signers.length} signer(s)")
|
|
197
|
+
|
|
198
|
+
upload_opts = account_id.nil? ? {} : { account_id: account_id }
|
|
199
|
+
document = @documents.upload(source, upload_opts)
|
|
200
|
+
document = @documents.wait_until_ready(document['id']) if wait_for_ready
|
|
201
|
+
|
|
202
|
+
signer_ids = signers.map do |signer|
|
|
203
|
+
created = @signers.create(signer, account_id)
|
|
204
|
+
created['id'] || raise(ApiError.new('Signer created but the API returned no ID', 502, created))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
assignment_payload = { method: 'virtual', signers: signer_ids,
|
|
208
|
+
message: message, expires_at: expires_at,
|
|
209
|
+
copy_receivers: copy_receivers }
|
|
210
|
+
assignment = @assignments.create(document['id'], assignment_payload)
|
|
211
|
+
|
|
212
|
+
@logger.info("Upload and signature workflow completed for document #{document['id']}")
|
|
213
|
+
|
|
214
|
+
{ document: document, assignment: assignment, signer_ids: signer_ids }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Expose the underlying Faraday connection (for advanced use cases,
|
|
218
|
+
# such as adding middleware or inspecting headers in tests).
|
|
219
|
+
#
|
|
220
|
+
# @return [Faraday::Connection]
|
|
221
|
+
#
|
|
222
|
+
# @example Inspect the auth header the SDK sends
|
|
223
|
+
# client = Assinafy::Client.new(api_key: 'example_api_key', account_id: 'account_example')
|
|
224
|
+
# client.faraday_connection.headers['X-Api-Key'] #=> "example_api_key"
|
|
225
|
+
def faraday_connection
|
|
226
|
+
@connection
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def build_connection(config)
|
|
232
|
+
Faraday.new(url: config.base_url) do |f|
|
|
233
|
+
f.request :multipart
|
|
234
|
+
f.request :json
|
|
235
|
+
f.response :json, content_type: /\bjson/
|
|
236
|
+
f.options.timeout = config.timeout
|
|
237
|
+
f.options.open_timeout = config.timeout
|
|
238
|
+
f.headers.merge!(config.auth_headers)
|
|
239
|
+
f.headers['Accept'] = 'application/json'
|
|
240
|
+
f.headers['User-Agent'] = "assinafy-ruby-sdk/#{VERSION}"
|
|
241
|
+
f.adapter Faraday.default_adapter
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
|
|
5
|
+
module Assinafy
|
|
6
|
+
# SDK configuration values. {Client} snapshots these values when it builds
|
|
7
|
+
# its connection; construct a new client after changing a configuration.
|
|
8
|
+
#
|
|
9
|
+
# @example Build from a YAML-style hash (e.g. loaded from config/assinafy.yml)
|
|
10
|
+
# raw = YAML.load_file('config/assinafy.yml') # => string-keyed Hash
|
|
11
|
+
# # raw => {
|
|
12
|
+
# # "api_key" => "example_api_key",
|
|
13
|
+
# # "account_id" => "account_example",
|
|
14
|
+
# # "base_url" => "https://api.assinafy.com.br/v1",
|
|
15
|
+
# # "webhook_secret" => "gateway_secret",
|
|
16
|
+
# # "timeout" => 30
|
|
17
|
+
# # }
|
|
18
|
+
# config = Assinafy::Configuration.from_hash(raw)
|
|
19
|
+
# config.api_key # => "example_api_key"
|
|
20
|
+
# config.account_id # => "account_example"
|
|
21
|
+
# config.auth_headers # => { "X-Api-Key" => "example_api_key" }
|
|
22
|
+
class Configuration
|
|
23
|
+
# Default base URL (production v1 API).
|
|
24
|
+
DEFAULT_BASE_URL = 'https://api.assinafy.com.br/v1'
|
|
25
|
+
# Default Faraday open/read timeout, in seconds.
|
|
26
|
+
DEFAULT_TIMEOUT = 30
|
|
27
|
+
|
|
28
|
+
# @!attribute [rw] api_key
|
|
29
|
+
# @return [String, nil] sent as `X-Api-Key`
|
|
30
|
+
# @!attribute [rw] token
|
|
31
|
+
# @return [String, nil] legacy bearer token (used when `api_key` is nil)
|
|
32
|
+
# @!attribute [rw] account_id
|
|
33
|
+
# @return [String, nil] default workspace ID
|
|
34
|
+
# @!attribute [rw] base_url
|
|
35
|
+
# @return [String] API base URL (trailing slash stripped)
|
|
36
|
+
# @!attribute [rw] webhook_secret
|
|
37
|
+
# @return [String, nil] secret for {Support::WebhookVerifier}
|
|
38
|
+
# @!attribute [rw] timeout
|
|
39
|
+
# @return [Integer] Faraday timeout in seconds
|
|
40
|
+
# @!attribute [rw] logger
|
|
41
|
+
# @return [Logger, nil]
|
|
42
|
+
attr_accessor :api_key, :token, :account_id, :base_url, :webhook_secret, :timeout, :logger
|
|
43
|
+
|
|
44
|
+
# Build a configuration directly from keyword arguments. Prefer passing
|
|
45
|
+
# `api_key` (the documented `X-Api-Key` mechanism); `token` is the legacy
|
|
46
|
+
# bearer fallback. `base_url` has its trailing slash stripped on assignment.
|
|
47
|
+
#
|
|
48
|
+
# @param api_key [String, nil] sent as the `X-Api-Key` header
|
|
49
|
+
# @param token [String, nil] legacy bearer token (used only when `api_key` is nil)
|
|
50
|
+
# @param account_id [String, nil] default workspace account ID
|
|
51
|
+
# @param base_url [String] API base URL (trailing slash is stripped)
|
|
52
|
+
# @param webhook_secret [String, nil] secret for {Support::WebhookVerifier}
|
|
53
|
+
# @param timeout [Integer] Faraday open/read timeout in seconds
|
|
54
|
+
# @param logger [Logger, nil] optional logger for Faraday
|
|
55
|
+
#
|
|
56
|
+
# @example Construct with an API key (omits the default base_url)
|
|
57
|
+
# config = Assinafy::Configuration.new(
|
|
58
|
+
# api_key: 'example_api_key',
|
|
59
|
+
# account_id: 'account_example'
|
|
60
|
+
# )
|
|
61
|
+
# config.base_url # => "https://api.assinafy.com.br/v1"
|
|
62
|
+
# config.timeout # => 30
|
|
63
|
+
# config.auth_headers # => { "X-Api-Key" => "example_api_key" }
|
|
64
|
+
#
|
|
65
|
+
# @example Trailing slash on base_url is stripped
|
|
66
|
+
# Assinafy::Configuration.new(base_url: 'https://api.assinafy.com.br/v1/').base_url
|
|
67
|
+
# # => "https://api.assinafy.com.br/v1"
|
|
68
|
+
def initialize(api_key: nil, token: nil, account_id: nil,
|
|
69
|
+
base_url: DEFAULT_BASE_URL, webhook_secret: nil,
|
|
70
|
+
timeout: DEFAULT_TIMEOUT, logger: nil)
|
|
71
|
+
@api_key = api_key
|
|
72
|
+
@token = token
|
|
73
|
+
@account_id = account_id
|
|
74
|
+
raise ValidationError.new('Base URL is required') unless base_url.is_a?(String)
|
|
75
|
+
|
|
76
|
+
@base_url = base_url.strip.sub(%r{/+\z}, '')
|
|
77
|
+
raise ValidationError.new('Base URL is required') if @base_url.empty?
|
|
78
|
+
|
|
79
|
+
@webhook_secret = webhook_secret
|
|
80
|
+
@timeout = normalize_timeout(timeout)
|
|
81
|
+
@logger = logger
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Build a {Configuration} from a Hash with string or symbol keys.
|
|
85
|
+
# Accepts both `'token'` and `'access_token'` for backwards compatibility.
|
|
86
|
+
# Missing keys fall back to defaults (`base_url` => {DEFAULT_BASE_URL},
|
|
87
|
+
# `timeout` => {DEFAULT_TIMEOUT}); numeric strings are accepted, while
|
|
88
|
+
# invalid and non-positive values raise {ValidationError}.
|
|
89
|
+
#
|
|
90
|
+
# @param hash [Hash{String,Symbol=>Object}]
|
|
91
|
+
# @return [Configuration]
|
|
92
|
+
#
|
|
93
|
+
# @example Symbol-keyed hash with the legacy access_token alias
|
|
94
|
+
# config = Assinafy::Configuration.from_hash(
|
|
95
|
+
# access_token: 'legacy-bearer-abc123',
|
|
96
|
+
# account_id: 'account_example',
|
|
97
|
+
# timeout: '45'
|
|
98
|
+
# )
|
|
99
|
+
# config.token # => "legacy-bearer-abc123"
|
|
100
|
+
# config.api_key # => nil
|
|
101
|
+
# config.timeout # => 45
|
|
102
|
+
# config.base_url # => "https://api.assinafy.com.br/v1"
|
|
103
|
+
# config.auth_headers # => { "Authorization" => "Bearer legacy-bearer-abc123" }
|
|
104
|
+
def self.from_hash(hash)
|
|
105
|
+
raise ValidationError.new('Configuration must be a Hash') unless hash.is_a?(Hash)
|
|
106
|
+
|
|
107
|
+
h = hash.transform_keys(&:to_s)
|
|
108
|
+
new(
|
|
109
|
+
api_key: h['api_key'],
|
|
110
|
+
token: h['token'] || h['access_token'],
|
|
111
|
+
account_id: h['account_id'],
|
|
112
|
+
base_url: h.key?('base_url') ? h['base_url'] : DEFAULT_BASE_URL,
|
|
113
|
+
webhook_secret: h['webhook_secret'],
|
|
114
|
+
timeout: h.key?('timeout') ? h['timeout'] : DEFAULT_TIMEOUT,
|
|
115
|
+
logger: h['logger']
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Return the HTTP headers used to authenticate requests, preferring
|
|
120
|
+
# `X-Api-Key` (the documented mechanism) over a bearer token. When `api_key`
|
|
121
|
+
# is set it wins; otherwise a non-nil `token` produces an `Authorization:
|
|
122
|
+
# Bearer` header; with neither credential set an empty Hash is returned.
|
|
123
|
+
#
|
|
124
|
+
# @return [Hash{String=>String}] one of `{ "X-Api-Key" => ... }`,
|
|
125
|
+
# `{ "Authorization" => "Bearer ..." }`, or `{}`
|
|
126
|
+
#
|
|
127
|
+
# @example api_key takes precedence over token
|
|
128
|
+
# Assinafy::Configuration.new(api_key: 'k', token: 't').auth_headers
|
|
129
|
+
# # => { "X-Api-Key" => "k" }
|
|
130
|
+
#
|
|
131
|
+
# @example Bearer fallback when only a token is present
|
|
132
|
+
# Assinafy::Configuration.new(token: 't').auth_headers
|
|
133
|
+
# # => { "Authorization" => "Bearer t" }
|
|
134
|
+
#
|
|
135
|
+
# @example No credentials configured
|
|
136
|
+
# Assinafy::Configuration.new.auth_headers
|
|
137
|
+
# # => {}
|
|
138
|
+
def auth_headers
|
|
139
|
+
key = api_key.to_s.strip
|
|
140
|
+
bearer = token.to_s.strip
|
|
141
|
+
return { 'X-Api-Key' => key } unless key.empty?
|
|
142
|
+
return { 'Authorization' => "Bearer #{bearer}" } unless bearer.empty?
|
|
143
|
+
|
|
144
|
+
{}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def normalize_timeout(value)
|
|
150
|
+
raw = value.nil? ? DEFAULT_TIMEOUT : value
|
|
151
|
+
seconds = Integer(raw, exception: false) if raw.is_a?(Integer) || raw.is_a?(String)
|
|
152
|
+
return seconds if seconds&.positive?
|
|
153
|
+
|
|
154
|
+
raise ValidationError.new('Timeout must be a positive integer')
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Assinafy
|
|
4
|
+
# Base class for all errors raised by the SDK. Carries an optional context
|
|
5
|
+
# Hash that may contain useful debugging details (e.g. response data, IDs).
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
# @return [Hash] arbitrary metadata about the error
|
|
8
|
+
attr_reader :context
|
|
9
|
+
|
|
10
|
+
def initialize(message = nil, context = {})
|
|
11
|
+
super(message)
|
|
12
|
+
@context = context || {}
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Raised when the API responds with a non-2xx status, or with a 2xx
|
|
17
|
+
# response envelope whose embedded status code indicates failure.
|
|
18
|
+
#
|
|
19
|
+
# The Assinafy v1 API returns two distinct error-body shapes, both handled
|
|
20
|
+
# here:
|
|
21
|
+
#
|
|
22
|
+
# - Framework errors: `{"name":"Not Found","message":"Página não encontrada.","code":0,"status":404}`
|
|
23
|
+
# - Application envelopes: `{"status":404,"data":null,"message":"Template não encontrado."}`
|
|
24
|
+
#
|
|
25
|
+
# @example Rescue an API error
|
|
26
|
+
# begin
|
|
27
|
+
# client.documents.details('missing-id')
|
|
28
|
+
# rescue Assinafy::ApiError => e
|
|
29
|
+
# e.status_code # => 404
|
|
30
|
+
# e.message # => "Documento não encontrado."
|
|
31
|
+
# e.error_name # => "Not Found" (nil when the body omits it)
|
|
32
|
+
# e.error_code # => 0 (nil when the body omits it)
|
|
33
|
+
# e.response_data # => the raw parsed body Hash
|
|
34
|
+
# end
|
|
35
|
+
class ApiError < Error
|
|
36
|
+
# @return [Integer] HTTP-style status code reported by the API
|
|
37
|
+
attr_reader :status_code
|
|
38
|
+
# @return [Hash, String, nil] raw response body
|
|
39
|
+
attr_reader :response_data
|
|
40
|
+
# @return [String, nil] the API's `name` field (framework errors only)
|
|
41
|
+
attr_reader :error_name
|
|
42
|
+
# @return [Integer, String, nil] the API's `code` field, when present
|
|
43
|
+
attr_reader :error_code
|
|
44
|
+
|
|
45
|
+
def initialize(message, status_code, response_data = nil)
|
|
46
|
+
super(message, { status_code: status_code, response_data: response_data })
|
|
47
|
+
@status_code = status_code
|
|
48
|
+
@response_data = response_data
|
|
49
|
+
return unless response_data.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
@error_name = response_data['name']
|
|
52
|
+
@error_code = response_data['code']
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Build an {ApiError} from an HTTP status and parsed body. Reads the human
|
|
56
|
+
# message from `message`, `error`, or `name` (in that order).
|
|
57
|
+
#
|
|
58
|
+
# @param status_code [Integer]
|
|
59
|
+
# @param response_data [Hash, Object]
|
|
60
|
+
# @return [ApiError]
|
|
61
|
+
def self.from_response(status_code, response_data)
|
|
62
|
+
data = response_data.is_a?(Hash) ? response_data : {}
|
|
63
|
+
message = data['message'] || data['error'] || data['name'] || 'API request failed'
|
|
64
|
+
new(message.to_s, status_code, response_data)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Raised before a network request is made when the caller's input is
|
|
69
|
+
# invalid (missing IDs, wrong shape, etc.).
|
|
70
|
+
class ValidationError < Error
|
|
71
|
+
# @return [Hash] field-keyed validation details
|
|
72
|
+
attr_reader :errors
|
|
73
|
+
|
|
74
|
+
def initialize(message = 'Validation failed', errors = {})
|
|
75
|
+
super(message, { errors: errors })
|
|
76
|
+
@errors = errors || {}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Raised when Faraday reports a connection, timeout, or TLS error. The
|
|
81
|
+
# original exception's message is included.
|
|
82
|
+
class NetworkError < Error; end
|
|
83
|
+
end
|