clicksign-ruby-sdk 0.1.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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +535 -0
  3. data/lib/clicksign/client.rb +143 -0
  4. data/lib/clicksign/configuration.rb +29 -0
  5. data/lib/clicksign/error_handler.rb +56 -0
  6. data/lib/clicksign/errors.rb +53 -0
  7. data/lib/clicksign/instrumentation.rb +32 -0
  8. data/lib/clicksign/json_api/atomic_results_parser.rb +61 -0
  9. data/lib/clicksign/json_api/bulk_operations_client.rb +95 -0
  10. data/lib/clicksign/json_api/operations/bulk_requirement.rb +89 -0
  11. data/lib/clicksign/json_api/operations.rb +38 -0
  12. data/lib/clicksign/json_api/parser.rb +31 -0
  13. data/lib/clicksign/json_api/query_builder.rb +45 -0
  14. data/lib/clicksign/json_api/serializer.rb +14 -0
  15. data/lib/clicksign/resource.rb +263 -0
  16. data/lib/clicksign/resources/acceptance_term/whatsapp.rb +12 -0
  17. data/lib/clicksign/resources/access_control_list.rb +35 -0
  18. data/lib/clicksign/resources/auto_signature/term.rb +12 -0
  19. data/lib/clicksign/resources/envelope_bulk_creation.rb +9 -0
  20. data/lib/clicksign/resources/folder.rb +22 -0
  21. data/lib/clicksign/resources/group.rb +21 -0
  22. data/lib/clicksign/resources/membership.rb +21 -0
  23. data/lib/clicksign/resources/notarial/bulk_requirement.rb +67 -0
  24. data/lib/clicksign/resources/notarial/document.rb +40 -0
  25. data/lib/clicksign/resources/notarial/envelope.rb +80 -0
  26. data/lib/clicksign/resources/notarial/event.rb +20 -0
  27. data/lib/clicksign/resources/notarial/requirement.rb +63 -0
  28. data/lib/clicksign/resources/notarial/signature_watcher.rb +39 -0
  29. data/lib/clicksign/resources/notarial/signer.rb +56 -0
  30. data/lib/clicksign/resources/template.rb +13 -0
  31. data/lib/clicksign/resources/template_field.rb +9 -0
  32. data/lib/clicksign/resources/user.rb +15 -0
  33. data/lib/clicksign/resources/webhook.rb +9 -0
  34. data/lib/clicksign/services.rb +35 -0
  35. data/lib/clicksign/version.rb +5 -0
  36. data/lib/clicksign/webhook.rb +41 -0
  37. data/lib/clicksign.rb +73 -0
  38. metadata +81 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7c3da29608ca505f7705b718fa579adb1d346bf8000e50fc87382616833e645f
4
+ data.tar.gz: e92995a59c7939aac989d9a165956b9309523a505178a2f79f168e0b6b6578d5
5
+ SHA512:
6
+ metadata.gz: f1ccd82b00a37e3f88cd0e8fc65b0279500750ad8c008c8e98fb50451d22770440809e7d0308d01753e6a66c78dab72f151e949fcd3cfa9249dc9f10e9a5c1dd
7
+ data.tar.gz: c80bdcec1a9c6f7eb47b649cb17756c6cfde384d4fddfca8239b4efa2e26ccc415aa407aa7d83de852706b11571750ef94e1c8d2efa5e3265cb43efda3f9247d
data/README.md ADDED
@@ -0,0 +1,535 @@
1
+ # Clicksign Ruby SDK
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/clicksign-ruby-sdk.svg)](https://badge.fury.io/rb/clicksign-ruby-sdk)
4
+ [![CI](https://github.com/djosino/clicksign-ruby-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/djosino/clicksign-ruby-sdk/actions/workflows/ci.yml)
5
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0-red)](https://www.ruby-lang.org)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](clicksign-ruby-sdk.gemspec)
7
+
8
+ Cliente Ruby oficial para a [API v3 da Clicksign](https://developers.clicksign.com/) (JSON:API). Permite criar envelopes, adicionar documentos e signatários, configurar requisitos de assinatura, webhooks e demais recursos da plataforma com uma API idiomática em Ruby.
9
+
10
+ **Requisitos:** Ruby >= 3.0 · dependências de runtime: apenas biblioteca padrão (`net/http`, `json`).
11
+
12
+ **Documentação da API:** [Sandbox](https://sandbox.clicksign.com/api/v3) · [Produção](https://app.clicksign.com/api/v3) · Referência interna: [`docs/SPEC.md`](docs/SPEC.md)
13
+
14
+ ---
15
+
16
+ ## Índice
17
+
18
+ - [Instalação](#instalação)
19
+ - [Configuração](#configuração)
20
+ - [Início rápido](#início-rápido)
21
+ - [Fluxo de assinatura (notarial)](#fluxo-de-assinatura-notarial)
22
+ - [Filtros, ordenação e paginação](#filtros-ordenação-e-paginação)
23
+ - [Outros recursos](#outros-recursos)
24
+ - [Tratamento de erros](#tratamento-de-erros)
25
+ - [Ambientes](#ambientes)
26
+ - [Desenvolvimento](#desenvolvimento)
27
+ - [Licença](#licença)
28
+
29
+ > **Exemplo passo a passo:** [`docs/WORKFLOW.md`](docs/WORKFLOW.md) — fluxo completo de envelope → documento → signatário → requisitos → ativação → notificação.
30
+
31
+ ---
32
+
33
+ ## Instalação
34
+
35
+ Adicione ao `Gemfile`:
36
+
37
+ ```ruby
38
+ source 'https://rubygems.org'
39
+
40
+ gem 'clicksign-ruby-sdk'
41
+ ```
42
+
43
+ Depois:
44
+
45
+ ```bash
46
+ bundle install
47
+ ```
48
+
49
+ Ou instale diretamente:
50
+
51
+ ```bash
52
+ gem install clicksign-ruby-sdk
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Configuração
58
+
59
+ Configure a chave de API e a URL base **uma vez** no boot da aplicação (initializer, `config/initializers/clicksign.rb`, script, etc.):
60
+
61
+ ```ruby
62
+ require 'clicksign'
63
+
64
+ Clicksign.configure do |c|
65
+ c.api_key = ENV.fetch('CLICKSIGN_API_KEY')
66
+ c.base_url = ENV.fetch('CLICKSIGN_API_BASE_URL', 'https://sandbox.clicksign.com/api/v3')
67
+ end
68
+ ```
69
+
70
+ A API usa o header `Authorization: <seu-token>` **sem** o prefixo `Bearer`.
71
+
72
+ > **Segurança:** não commite tokens no código. Use variáveis de ambiente ou cofre de secrets (Rails credentials, etc.).
73
+
74
+ Para testar interativamente no console da gem:
75
+
76
+ ```bash
77
+ CLICKSIGN_API_KEY=seu-token bundle exec ruby bin/console
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Início rápido
83
+
84
+ Listar envelopes em rascunho e criar um novo:
85
+
86
+ ```ruby
87
+ require 'clicksign'
88
+
89
+ Clicksign.configure do |c|
90
+ c.api_key = ENV['CLICKSIGN_API_KEY']
91
+ c.base_url = 'https://sandbox.clicksign.com/api/v3'
92
+ end
93
+
94
+ Envelope = Clicksign::Resources::Notarial::Envelope
95
+
96
+ # Listar com filtro
97
+ drafts = Envelope.filter(status: 'draft').to_a
98
+ puts drafts.map { |e| [e.id, e.name, e.status] }
99
+
100
+ # Criar envelope (status inicial: draft)
101
+ envelope = Envelope.create(
102
+ name: 'Contrato de prestação de serviços',
103
+ locale: 'pt-BR',
104
+ auto_close: true
105
+ )
106
+
107
+ puts envelope.id # UUID do envelope
108
+ puts envelope.status # => "draft"
109
+ ```
110
+
111
+ Buscar, atualizar e excluir:
112
+
113
+ ```ruby
114
+ found = Envelope.retrieve(envelope.id)
115
+ found.update(name: 'Contrato — revisão 2')
116
+
117
+ found.delete
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Fluxo de assinatura (notarial)
123
+
124
+ Namespace principal: `Clicksign::Resources::Notarial`.
125
+
126
+ ### 1. Envelope
127
+
128
+ ```ruby
129
+ Envelope = Clicksign::Resources::Notarial::Envelope
130
+ Folder = Clicksign::Resources::Folder
131
+
132
+ # Opcional: associar a uma pasta
133
+ folder = Folder.filter(in_root: true).first
134
+ envelope = Envelope.create(
135
+ name: 'Proposta comercial #1042',
136
+ folder_id: folder&.id,
137
+ deadline_at: '2026-12-31T23:59:59.000-03:00',
138
+ default_subject: 'Documentos para assinatura',
139
+ default_message: 'Por favor, assine os documentos em anexo.'
140
+ )
141
+ ```
142
+
143
+ ### 2. Documento
144
+
145
+ Envie o PDF em Base64, via URL ou a partir de um template:
146
+
147
+ ```ruby
148
+ Document = Clicksign::Resources::Notarial::Document
149
+
150
+ document = Document.create(
151
+ envelope_id: envelope.id,
152
+ filename: 'contrato.pdf',
153
+ content_base64: 'data:application/pdf;base64,JVBERi0xLjQK...'
154
+ )
155
+
156
+ # Alternativas (mutuamente exclusivas na API):
157
+ # content_url: 'https://exemplo.com/arquivo.pdf'
158
+ # template: { key: template_id, data: {} } # filename deve ser .doc ou .docx
159
+
160
+ # Listar documentos do envelope
161
+ Envelope.list_documents(envelope.id).each do |doc|
162
+ puts "#{doc.id} — #{doc.filename} (#{doc.status})"
163
+ end
164
+ ```
165
+
166
+ ### 3. Signatário
167
+
168
+ ```ruby
169
+ Signer = Clicksign::Resources::Notarial::Signer
170
+
171
+ signer = Signer.create(
172
+ envelope_id: envelope.id,
173
+ name: 'Maria Silva',
174
+ email: 'maria.silva@example.com',
175
+ phone_number: '11999998888',
176
+ has_documentation: true,
177
+ documentation: '12345678909',
178
+ refusable: true,
179
+ communicate_events: {
180
+ signature_request: 'email', # canal de convite para assinar
181
+ signature_reminder: 'email', # canal de lembrete automático
182
+ document_signed: 'email', # canal de confirmação pós-assinatura
183
+ },
184
+ )
185
+
186
+ # Reenviar notificação por e-mail
187
+ signer.notify(message: 'Lembrete: seu documento aguarda assinatura.')
188
+
189
+ # Ou via classe
190
+ Signer.notify(signer.id, envelope_id: envelope.id, message: 'Lembrete de assinatura')
191
+ ```
192
+
193
+ ### 4. Requisitos (requirements)
194
+
195
+ Cada requisito associa um signatário a um documento com uma ação (`agree`, `provide_evidence`, `rubricate`). O envelope precisa estar em **draft** para criar ou remover requisitos.
196
+
197
+ #### 4.1 Endpoint padrão
198
+
199
+ `POST /envelopes/:envelope_id/requirements` — uma operação por requisição.
200
+
201
+ ```ruby
202
+ Requirement = Clicksign::Resources::Notarial::Requirement
203
+
204
+ rels = {
205
+ document: { data: { type: 'documents', id: document.id } },
206
+ signer: { data: { type: 'signers', id: signer.id } },
207
+ }
208
+
209
+ # Concordância (agree)
210
+ agree = Requirement.create(
211
+ envelope_id: envelope.id,
212
+ action: 'agree',
213
+ role: 'sign',
214
+ relationships: rels
215
+ )
216
+
217
+ # Evidência de autenticação (ex.: e-mail)
218
+ Requirement.create(
219
+ envelope_id: envelope.id,
220
+ action: 'provide_evidence',
221
+ auth: 'email',
222
+ relationships: rels
223
+ )
224
+
225
+ # Rubrica em todas as páginas
226
+ Requirement.create(
227
+ envelope_id: envelope.id,
228
+ action: 'rubricate',
229
+ pages: 'all',
230
+ relationships: rels
231
+ )
232
+
233
+ # Rubrica em campo específico do documento
234
+ Requirement.create(
235
+ envelope_id: envelope.id,
236
+ action: 'rubricate',
237
+ rubric_field: 'campo_rubrica_1',
238
+ relationships: rels
239
+ )
240
+
241
+ # Consultar
242
+ Requirement.retrieve(agree.id, envelope_id: envelope.id)
243
+ Envelope.list_requirements(envelope.id, 'signer.key': signer.id)
244
+ Requirement.list_for_document(document.id)
245
+ Requirement.list_for_signer(signer.id)
246
+
247
+ # Remover (envelope em draft)
248
+ agree.delete
249
+ ```
250
+
251
+ #### 4.2 Operações em lote (bulk)
252
+
253
+ `POST /envelopes/:envelope_id/bulk_requirements` — várias operações em uma requisição (`atomic:operations` → `atomic:results`). Indicado quando você monta o setup completo de uma vez.
254
+
255
+ ```ruby
256
+ BulkRequirement = Clicksign::Resources::Notarial::BulkRequirement
257
+
258
+ response = BulkRequirement.create(envelope_id: envelope.id) do |ops|
259
+ ops.add_agree(
260
+ signer_id: signer.id,
261
+ document_id: document.id,
262
+ role: 'sign'
263
+ )
264
+ ops.add_provide_evidence(
265
+ signer_id: signer.id,
266
+ document_id: document.id,
267
+ auth: 'email'
268
+ )
269
+ ops.add_rubricate(
270
+ signer_id: signer.id,
271
+ document_id: document.id,
272
+ pages: 'all'
273
+ )
274
+ # ops.remove(requirement_id: requisito_antigo.id)
275
+ end
276
+
277
+ if response.success?
278
+ response.requirements.each { |r| puts "OK: #{r.id} (#{r.action})" }
279
+ else
280
+ response.failures.each do |failure|
281
+ puts "Falha na operação #{failure.index}: #{failure.errors}"
282
+ end
283
+ end
284
+ ```
285
+
286
+ | Abordagem | Endpoint | Quando usar |
287
+ |-----------|----------|-------------|
288
+ | **4.1 Padrão** | `/requirements` | Criar/alterar requisitos um a um, fluxos incrementais |
289
+ | **4.2 Bulk** | `/bulk_requirements` | Várias ações na mesma chamada; tratar sucesso/falha por slot |
290
+
291
+ ### 5. Ativar o envelope
292
+
293
+ Atualize o `status` para `running` via PATCH:
294
+
295
+ ```ruby
296
+ activated = envelope.update(status: 'running')
297
+ puts activated.status # => "running"
298
+ ```
299
+
300
+ ### 6. Observadores e eventos
301
+
302
+ ```ruby
303
+ SignatureWatcher = Clicksign::Resources::Notarial::SignatureWatcher
304
+ Event = Clicksign::Resources::Notarial::Event
305
+
306
+ watcher = SignatureWatcher.create(
307
+ envelope_id: envelope.id,
308
+ email: 'compliance@empresa.com',
309
+ kind: 'all_steps',
310
+ attach_documents_enabled: true
311
+ )
312
+
313
+ # Eventos do envelope
314
+ Envelope.list_events(envelope.id)
315
+
316
+ # Eventos de um documento
317
+ Document.list_events(document.id, envelope_id: envelope.id)
318
+
319
+ # Criar evento customizado no documento
320
+ Event.create_for_document(
321
+ envelope_id: envelope.id,
322
+ document_id: document.id,
323
+ name: 'custom',
324
+ data: { description: 'Etapa interna concluída' }
325
+ )
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Filtros, ordenação e paginação
331
+
332
+ A API de listagem é chainable:
333
+
334
+ ```ruby
335
+ Envelope
336
+ .filter(status: 'running', name: 'Contrato')
337
+ .order('-created')
338
+ .page(1)
339
+ .per(20)
340
+ .to_a
341
+
342
+ # Atalho quando há poucos filtros
343
+ Template.filter(name: 'NDA padrão').first
344
+
345
+ # Navegação no resultado
346
+ Envelope.order('-created').first # mais recente
347
+ Envelope.order('-created').last # mais antigo
348
+ Envelope.filter(status: 'draft').count # total de rascunhos
349
+ ```
350
+
351
+ Atributos dos objetos retornados são acessados como métodos ou por chave string:
352
+
353
+ ```ruby
354
+ envelope.name # método gerado dinamicamente
355
+ envelope['name'] # equivalente via operador []
356
+ envelope['status'] # útil quando a chave é uma variável
357
+ ```
358
+
359
+ O `QueryProxy` inclui `Enumerable`, então `each`, `map`, `select` e afins funcionam diretamente na chain sem precisar de `.to_a`:
360
+
361
+ ```ruby
362
+ Envelope.filter(status: 'draft').each { |e| puts e.id }
363
+ Envelope.filter(status: 'running').map(&:name)
364
+ Envelope.order('-created').select { |e| e.auto_close }
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Outros recursos
370
+
371
+ | Recurso | Classe | Exemplo |
372
+ |---------|--------|---------|
373
+ | Webhook | `Clicksign::Resources::Webhook` | `Webhook.create(endpoint: 'https://...', events: ['envelope.completed'], status: 'active')` |
374
+ | Pasta | `Clicksign::Resources::Folder` | `Folder.create(name: 'Contratos 2026', folder_id: pai&.id)` |
375
+ | Template | `Clicksign::Resources::Template` | `Template.list` · `Template.list_template_fields(id)` |
376
+ | Usuário | `Clicksign::Resources::User` | `User.me` · `User.filter(email: '...')` |
377
+ | Membership | `Clicksign::Resources::Membership` | `Membership.create(role: 'member', user_id: user.id)` |
378
+ | Grupo | `Clicksign::Resources::Group` | `Group.add_users(group_id, [user.id])` |
379
+ | ACL pasta/grupo | `Clicksign::Resources::AccessControlList` | `AccessControlList.create(folder_id:, group_id:)` |
380
+ | Criação em lote de envelopes | `Clicksign::Resources::EnvelopeBulkCreation` | Ver exemplo abaixo |
381
+ | Auto assinatura | `Clicksign::Resources::AutoSignature::Term` | `AutoSignature::Term.create(...)` |
382
+ | Termo WhatsApp | `Clicksign::Resources::AcceptanceTerm::Whatsapp` | `AcceptanceTerm::Whatsapp.list` |
383
+
384
+ Exemplo de webhook:
385
+
386
+ ```ruby
387
+ Webhook = Clicksign::Resources::Webhook
388
+
389
+ hook = Webhook.create(
390
+ endpoint: 'https://minhaapp.com/webhooks/clicksign',
391
+ events: %w[sign close cancel add_signer],
392
+ status: 'active'
393
+ )
394
+
395
+ hook.update(status: 'inactive')
396
+ hook.delete
397
+ ```
398
+
399
+ Exemplo de usuário e membership:
400
+
401
+ ```ruby
402
+ User = Clicksign::Resources::User
403
+ Membership = Clicksign::Resources::Membership
404
+
405
+ eu = User.me
406
+ puts "#{eu.name} <#{eu.email}>"
407
+
408
+ novo = User.create(
409
+ name: 'João Integração',
410
+ email: 'joao.integracao@example.com',
411
+ phone_number: '11988887777'
412
+ )
413
+
414
+ Membership.create(role: 'admin', user_id: novo.id)
415
+ ```
416
+
417
+ Criação de envelope em lote (job assíncrono):
418
+
419
+ ```ruby
420
+ EnvelopeBulkCreation = Clicksign::Resources::EnvelopeBulkCreation
421
+
422
+ job = EnvelopeBulkCreation.create(
423
+ envelope: { name: 'Contrato Lote', locale: 'pt-BR', auto_close: true },
424
+ document: {
425
+ filename: 'contrato.docx',
426
+ content_base64: 'data:application/msword;base64,...'
427
+ },
428
+ signers: [
429
+ {
430
+ name: 'Carlos',
431
+ email: 'carlos@example.com',
432
+ requirements: [{ action: 'agree', role: 'sign', auth: 'email' }]
433
+ }
434
+ ]
435
+ )
436
+
437
+ puts job.job_id # UUID do job enfileirado
438
+ puts job.enqueued_at # timestamp de enfileiramento
439
+ ```
440
+
441
+ Controle de acesso em pasta:
442
+
443
+ ```ruby
444
+ AccessControlList = Clicksign::Resources::AccessControlList
445
+
446
+ AccessControlList.create(folder_id: folder.id, group_id: group.id)
447
+ AccessControlList.destroy(folder_id: folder.id, group_id: group.id)
448
+ ```
449
+
450
+ ---
451
+
452
+ ## Tratamento de erros
453
+
454
+ Erros HTTP são convertidos em exceções antes de chegar ao seu código:
455
+
456
+ | HTTP | Exceção |
457
+ |------|---------|
458
+ | 401, 403 | `Clicksign::AuthenticationError` |
459
+ | 404 | `Clicksign::NotFoundError` |
460
+ | 400, 422 | `Clicksign::ValidationError` |
461
+ | 409 | `Clicksign::ConflictError` |
462
+ | 429 | `Clicksign::RateLimitError` |
463
+ | 5xx | `Clicksign::ServerError` |
464
+ | Timeout / conexão | `Clicksign::TimeoutError` |
465
+
466
+ Exemplo:
467
+
468
+ ```ruby
469
+ begin
470
+ Envelope.retrieve('00000000-0000-0000-0000-000000000000')
471
+ rescue Clicksign::NotFoundError
472
+ puts 'Envelope não encontrado'
473
+ rescue Clicksign::ValidationError => e
474
+ puts "Dados inválidos: #{e.message}"
475
+ rescue Clicksign::AuthenticationError
476
+ puts 'Verifique CLICKSIGN_API_KEY'
477
+ end
478
+ ```
479
+
480
+ Operações em lote (`BulkRequirement`) podem retornar falhas **por slot** em `response.failures` sem lançar exceção, quando a API responde com `atomic:results` parcial.
481
+
482
+ ---
483
+
484
+ ## Ambientes
485
+
486
+ | Ambiente | `base_url` |
487
+ |----------|------------|
488
+ | Sandbox | `https://sandbox.clicksign.com/api/v3` |
489
+ | Produção | `https://app.clicksign.com/api/v3` |
490
+
491
+ O padrão da gem em `Clicksign::Configuration` é produção (`app.clicksign.com`). Para desenvolvimento, defina explicitamente o sandbox:
492
+
493
+ ```ruby
494
+ Clicksign.configure do |c|
495
+ c.api_key = ENV['CLICKSIGN_API_KEY']
496
+ c.base_url = 'https://sandbox.clicksign.com/api/v3'
497
+ end
498
+ ```
499
+
500
+ Gere tokens de API no painel da Clicksign do ambiente correspondente.
501
+
502
+ ---
503
+
504
+ ## Desenvolvimento
505
+
506
+ Clone o repositório e instale dependências de desenvolvimento:
507
+
508
+ ```bash
509
+ bundle install
510
+ bundle exec rspec
511
+ ```
512
+
513
+ | Variável | Uso |
514
+ |----------|-----|
515
+ | `CLICKSIGN_API_KEY` | Token para testes contra sandbox (opcional) |
516
+ | `CLICKSIGN_API_BASE_URL` | URL da API (padrão sandbox nos specs de integração legados) |
517
+
518
+ A suíte principal usa **WebMock** e não exige rede. Alguns specs antigos ainda podem usar **VCR** com gravação no sandbox.
519
+
520
+ Estrutura relevante:
521
+
522
+ ```
523
+ lib/clicksign/
524
+ client.rb # HTTP (GET, POST, PATCH, DELETE)
525
+ resource.rb # CRUD base, filtros, nested lists
526
+ resources/notarial/ # Envelope, Document, Signer, Requirement, ...
527
+ json_api/ # Serializer, Parser, bulk operations
528
+ docs/SPEC.md # mapa completo de resources e rotas
529
+ ```
530
+
531
+ ---
532
+
533
+ ## Licença
534
+
535
+ MIT — ver [`clicksign-ruby-sdk.gemspec`](clicksign-ruby-sdk.gemspec).
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ module Clicksign
8
+ class Client
9
+ HEADERS = {
10
+ 'Content-Type' => 'application/vnd.api+json',
11
+ 'Accept' => 'application/vnd.api+json',
12
+ }.freeze
13
+
14
+ def initialize(api_key:, base_url:, open_timeout: 2, read_timeout: 10,
15
+ write_timeout: 10, max_retries: 0)
16
+ @api_key = api_key
17
+ @base_url = base_url
18
+ @open_timeout = open_timeout
19
+ @read_timeout = read_timeout
20
+ @write_timeout = write_timeout
21
+ @max_retries = max_retries
22
+ end
23
+
24
+ def get(path, params: {})
25
+ uri = build_uri(path, params)
26
+ execute_with_retry(Net::HTTP::Get.new(uri, headers), uri)
27
+ end
28
+
29
+ def post(path, body:)
30
+ uri = build_uri(path)
31
+ request = Net::HTTP::Post.new(uri, headers)
32
+ request.body = body.to_json
33
+ execute_with_retry(request, uri)
34
+ end
35
+
36
+ def patch(path, body:)
37
+ uri = build_uri(path)
38
+ request = Net::HTTP::Patch.new(uri, headers)
39
+ request.body = body.to_json
40
+ execute_with_retry(request, uri)
41
+ end
42
+
43
+ def delete(path, body: nil)
44
+ uri = build_uri(path)
45
+ request = Net::HTTP::Delete.new(uri, headers)
46
+ request.body = body.to_json if body
47
+ execute_with_retry(request, uri)
48
+ end
49
+
50
+ private
51
+
52
+ def headers
53
+ HEADERS.merge('Authorization' => @api_key)
54
+ end
55
+
56
+ def build_uri(path, params = {})
57
+ uri = URI.parse("#{@base_url}#{path}")
58
+ uri.query = URI.encode_www_form(params) unless params.empty?
59
+ uri
60
+ end
61
+
62
+ def execute_with_retry(request, uri)
63
+ attempts = 0
64
+ begin
65
+ attempts += 1
66
+ execute_once(request, uri, attempt: attempts)
67
+ rescue Clicksign::TimeoutError, Clicksign::RateLimitError,
68
+ Clicksign::ServerError => e
69
+ raise unless e.retryable? && attempts <= @max_retries
70
+
71
+ delay = backoff_delay(attempts)
72
+ Instrumentation.publish(:retry, {
73
+ method: request.method.downcase.to_sym,
74
+ path: resource_path(uri),
75
+ attempt: attempts,
76
+ max_retries: @max_retries,
77
+ error: e,
78
+ wait_ms: (delay * 1000).round,
79
+ })
80
+ sleep(delay)
81
+ retry
82
+ end
83
+ end
84
+
85
+ def execute_once(request, uri, attempt: 1)
86
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
87
+ context = { method: request.method.downcase.to_sym, path: resource_path(uri),
88
+ attempt: attempt }
89
+ response = http_request(request, uri)
90
+ handle_response(response, context, start)
91
+ rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED => e
92
+ handle_network_error(e, context, elapsed_ms(start))
93
+ end
94
+
95
+ def http_request(request, uri)
96
+ Net::HTTP.start(uri.host, uri.port,
97
+ use_ssl: uri.scheme == 'https',
98
+ open_timeout: @open_timeout,
99
+ read_timeout: @read_timeout,
100
+ write_timeout: @write_timeout,
101
+ &proc { |http| http.request(request) })
102
+ end
103
+
104
+ def handle_response(response, context, start)
105
+ duration = elapsed_ms(start)
106
+ status = response.code.to_i
107
+ begin
108
+ ErrorHandler.call(response)
109
+ rescue Error => e
110
+ publish_event(:request, context, status: status, duration_ms: duration)
111
+ publish_event(:error, context, error: e, status: status, duration_ms: duration)
112
+ raise
113
+ end
114
+ publish_event(:request, context, status: status, duration_ms: duration)
115
+ return nil if response.body.nil? || response.body.empty?
116
+
117
+ JSON.parse(response.body)
118
+ end
119
+
120
+ def handle_network_error(error, context, duration)
121
+ err = TimeoutError.new(error.message)
122
+ publish_event(:error, context, error: err, status: nil, duration_ms: duration)
123
+ raise err, error.message, error.backtrace
124
+ end
125
+
126
+ def publish_event(type, context, extra)
127
+ Instrumentation.publish(type, context.merge(extra))
128
+ end
129
+
130
+ def elapsed_ms(start)
131
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(1)
132
+ end
133
+
134
+ def resource_path(uri)
135
+ base = URI.parse(@base_url).path
136
+ uri.path.delete_prefix(base)
137
+ end
138
+
139
+ def backoff_delay(attempt)
140
+ [0.5 * (2**(attempt - 1)), 30].min
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ class Configuration
5
+ ENVIRONMENTS = {
6
+ sandbox: 'https://sandbox.clicksign.com/api/v3',
7
+ production: 'https://app.clicksign.com/api/v3',
8
+ }.freeze
9
+
10
+ attr_accessor :api_key, :base_url, :open_timeout, :read_timeout,
11
+ :write_timeout, :max_retries
12
+
13
+ def initialize
14
+ @base_url = 'https://app.clicksign.com/api/v3'
15
+ @open_timeout = 2
16
+ @read_timeout = 10
17
+ @write_timeout = 10
18
+ @max_retries = 0
19
+ end
20
+
21
+ def environment=(env)
22
+ url = ENVIRONMENTS.fetch(env.to_sym) do
23
+ raise ArgumentError,
24
+ "Unknown environment: #{env}. Valid: #{ENVIRONMENTS.keys.join(', ')}"
25
+ end
26
+ self.base_url = url
27
+ end
28
+ end
29
+ end