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
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ class Resource
5
+ class QueryProxy
6
+ include Enumerable
7
+
8
+ def initialize(resource_class, builder)
9
+ @resource_class = resource_class
10
+ @builder = builder
11
+ end
12
+
13
+ def filter(**params)
14
+ @builder.filter(**params)
15
+ self
16
+ end
17
+
18
+ def include(*types)
19
+ @builder.include(*types)
20
+ self
21
+ end
22
+
23
+ def order(field)
24
+ @builder.order(field)
25
+ self
26
+ end
27
+
28
+ def page(number)
29
+ @builder.page(number)
30
+ self
31
+ end
32
+
33
+ def per(size)
34
+ @builder.per(size)
35
+ self
36
+ end
37
+
38
+ def fields(**types)
39
+ @builder.fields(**types)
40
+ self
41
+ end
42
+
43
+ def to_a
44
+ @resource_class.send(:fetch_list, @builder.to_params)
45
+ end
46
+
47
+ def first
48
+ to_a.first
49
+ end
50
+
51
+ def last
52
+ to_a.last
53
+ end
54
+
55
+ def count
56
+ to_a.size
57
+ end
58
+
59
+ def each(&block)
60
+ to_a.each(&block)
61
+ end
62
+
63
+ def auto_paging_each(&block)
64
+ return enum_for(:auto_paging_each) unless block_given?
65
+
66
+ @resource_class.send(:fetch_auto_pages, @builder.to_params) do |page|
67
+ page.each(&block)
68
+ end
69
+ end
70
+
71
+ def each_page(&block)
72
+ return enum_for(:each_page) unless block_given?
73
+
74
+ @resource_class.send(:fetch_auto_pages, @builder.to_params, &block)
75
+ end
76
+
77
+ def auto_paging
78
+ enum_for(:auto_paging_each)
79
+ end
80
+ end
81
+
82
+ class << self
83
+ attr_writer :resource_type, :endpoint
84
+
85
+ def resource_type
86
+ @resource_type || infer_resource_type
87
+ end
88
+
89
+ def endpoint
90
+ @endpoint || "/#{resource_type}"
91
+ end
92
+
93
+ def list(**filters)
94
+ return fetch_list({}) if filters.empty?
95
+
96
+ filter(**filters).to_a
97
+ end
98
+
99
+ def retrieve(id)
100
+ raw = client.get("#{endpoint}/#{id}")
101
+ parsed = JsonApi::Parser.parse(raw)
102
+ build_instance(parsed[:data].first)
103
+ end
104
+
105
+ def create(**attributes)
106
+ relationships = attributes.delete(:relationships) || {}
107
+ raw = client.post(
108
+ endpoint,
109
+ body: JsonApi::Serializer.dump(
110
+ type: resource_type, attributes: attributes, relationships: relationships,
111
+ ),
112
+ )
113
+ parsed = JsonApi::Parser.parse(raw)
114
+ build_instance(parsed[:data].first)
115
+ end
116
+
117
+ def filter(**params)
118
+ QueryProxy.new(self, JsonApi::QueryBuilder.new.filter(**params))
119
+ end
120
+
121
+ def include(*types)
122
+ QueryProxy.new(self, JsonApi::QueryBuilder.new.include(*types))
123
+ end
124
+
125
+ def order(field)
126
+ QueryProxy.new(self, JsonApi::QueryBuilder.new.order(field))
127
+ end
128
+
129
+ def page(number)
130
+ QueryProxy.new(self, JsonApi::QueryBuilder.new.page(number))
131
+ end
132
+
133
+ def per(size)
134
+ QueryProxy.new(self, JsonApi::QueryBuilder.new.per(size))
135
+ end
136
+
137
+ def fields(**types)
138
+ QueryProxy.new(self, JsonApi::QueryBuilder.new.fields(**types))
139
+ end
140
+
141
+ def nested_list(parent_id, nested_type:, as: self, params: {})
142
+ raw = client.get("#{endpoint}/#{parent_id}/#{nested_type}", params: params)
143
+ parsed = JsonApi::Parser.parse(raw)
144
+ parsed[:data].map { |item| as.send(:build_instance, item, parent_id: parent_id) }
145
+ end
146
+
147
+ def filter_params(**filters)
148
+ filters.empty? ? {} : JsonApi::QueryBuilder.new.filter(**filters).to_params
149
+ end
150
+
151
+ def auto_paging_each(&block)
152
+ return enum_for(:auto_paging_each) unless block_given?
153
+
154
+ fetch_auto_pages({}) { |page| page.each(&block) }
155
+ end
156
+
157
+ def each_page(&block)
158
+ return enum_for(:each_page) unless block_given?
159
+
160
+ fetch_auto_pages({}, &block)
161
+ end
162
+
163
+ def client
164
+ Thread.current[:clicksign_client] || Clicksign.client
165
+ end
166
+
167
+ private
168
+
169
+ def fetch_list(params)
170
+ raw = client.get(endpoint, params: params)
171
+ parsed = JsonApi::Parser.parse(raw)
172
+ parsed[:data].map { |item| build_instance(item) }
173
+ end
174
+
175
+ def fetch_auto_pages(params)
176
+ per = (params['page[size]'] || 20).to_i
177
+ base = params.except('page[number]')
178
+ page = 1
179
+
180
+ loop do
181
+ raw = client.get(endpoint,
182
+ params: base.merge('page[number]' => page,
183
+ 'page[size]' => per))
184
+ items = JsonApi::Parser.parse(raw)[:data].map { |item| build_instance(item) }
185
+ yield items
186
+ break if items.size < per
187
+
188
+ page += 1
189
+ end
190
+ end
191
+
192
+ def build_instance(data, parent_id: nil)
193
+ instance = allocate
194
+ instance.send(:load_data, data, parent_id: parent_id)
195
+ instance
196
+ end
197
+
198
+ def infer_resource_type
199
+ "#{name.split('::').last
200
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
201
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
202
+ .downcase}s"
203
+ end
204
+ end
205
+
206
+ attr_reader :id, :relationships
207
+
208
+ def update(**attributes)
209
+ raw = self.class.client.patch(
210
+ "#{base_path}/#{@id}",
211
+ body: JsonApi::Serializer.dump(
212
+ type: self.class.resource_type, id: @id, attributes: attributes,
213
+ ),
214
+ )
215
+ parsed = JsonApi::Parser.parse(raw)
216
+ load_data(parsed[:data].first, parent_id: @_parent_id)
217
+ self
218
+ end
219
+
220
+ def delete
221
+ self.class.client.delete("#{base_path}/#{@id}")
222
+ nil
223
+ end
224
+
225
+ def reload
226
+ raw = self.class.client.get("#{base_path}/#{@id}")
227
+ parsed = JsonApi::Parser.parse(raw)
228
+ load_data(parsed[:data].first, parent_id: @_parent_id)
229
+ self
230
+ end
231
+
232
+ def base_path
233
+ self.class.endpoint
234
+ end
235
+
236
+ def [](key)
237
+ @_attributes&.[](key.to_s)
238
+ end
239
+
240
+ def method_missing(name, *args, &block)
241
+ key = name.to_s.delete_suffix('=')
242
+ if @_attributes&.key?(key)
243
+ name.to_s.end_with?('=') ? @_attributes[key] = args.first : @_attributes[key]
244
+ else
245
+ super
246
+ end
247
+ end
248
+
249
+ def respond_to_missing?(name, include_private = false)
250
+ key = name.to_s.delete_suffix('=')
251
+ @_attributes&.key?(key) || super
252
+ end
253
+
254
+ private
255
+
256
+ def load_data(data, parent_id: nil)
257
+ @id = data['id']
258
+ @relationships = data['relationships'] || {}
259
+ @_attributes = data['attributes'] || {}
260
+ @_parent_id = parent_id
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ module AcceptanceTerm
6
+ class Whatsapp < Clicksign::Resource
7
+ self.resource_type = 'acceptance_term_whatsapps'
8
+ self.endpoint = '/acceptance_term/whatsapps'
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ class AccessControlList < Clicksign::Resource
6
+ self.resource_type = 'access_control_lists'
7
+
8
+ def self.create(folder_id:, group_id:)
9
+ super(
10
+ relationships: acl_relationships(folder_id: folder_id, group_id: group_id)
11
+ )
12
+ end
13
+
14
+ def self.destroy(folder_id:, group_id:)
15
+ client.delete(
16
+ endpoint,
17
+ body: JsonApi::Serializer.dump(
18
+ type: resource_type,
19
+ attributes: {},
20
+ relationships: acl_relationships(folder_id: folder_id, group_id: group_id),
21
+ ),
22
+ )
23
+ nil
24
+ end
25
+
26
+ def self.acl_relationships(folder_id:, group_id:)
27
+ {
28
+ folder: { data: { type: 'folders', id: folder_id } },
29
+ group: { data: { type: 'groups', id: group_id } },
30
+ }
31
+ end
32
+ private_class_method :acl_relationships
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ module AutoSignature
6
+ class Term < Clicksign::Resource
7
+ self.resource_type = 'auto_signature_terms'
8
+ self.endpoint = '/auto_signature/terms'
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ class EnvelopeBulkCreation < Clicksign::Resource
6
+ self.resource_type = 'envelope_bulk_creations'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ class Folder < Clicksign::Resource
6
+ self.resource_type = 'folders'
7
+
8
+ def self.create(name:, folder_id: nil)
9
+ rels = folder_id ? { folder: { data: { type: 'folders', id: folder_id } } } : {}
10
+ super(name: name, relationships: rels)
11
+ end
12
+
13
+ def folder_id
14
+ relationships.dig('folder', 'data', 'id')
15
+ end
16
+
17
+ def child_folder_ids
18
+ Array(relationships.dig('folders', 'data')).filter_map { |d| d['id'] }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ class Group < Clicksign::Resource
6
+ self.resource_type = 'groups'
7
+
8
+ def self.add_users(group_id, user_ids)
9
+ data = Array(user_ids).map { |id| { type: 'users', id: id } }
10
+ client.post("/groups/#{group_id}/relationships/users", body: { data: data })
11
+ nil
12
+ end
13
+
14
+ def self.remove_users(group_id, user_ids)
15
+ data = Array(user_ids).map { |id| { type: 'users', id: id } }
16
+ client.delete("/groups/#{group_id}/relationships/users", body: { data: data })
17
+ nil
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ class Membership < Clicksign::Resource
6
+ self.resource_type = 'memberships'
7
+
8
+ def self.create(role:, user_id:, **attributes)
9
+ super(
10
+ **attributes,
11
+ role: role,
12
+ relationships: { user: { data: { type: 'users', id: user_id } } }
13
+ )
14
+ end
15
+
16
+ def user_id
17
+ relationships.dig('user', 'data', 'id')
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ module Notarial
6
+ class BulkRequirement
7
+ class OperationResult
8
+ attr_reader :index, :op, :requirement, :errors, :raw
9
+
10
+ def initialize(index:, op:, requirement:, errors:, raw:)
11
+ @index = index
12
+ @op = op
13
+ @requirement = requirement
14
+ @errors = errors
15
+ @raw = raw
16
+ end
17
+
18
+ def success?
19
+ errors.nil? || (errors.is_a?(Array) && errors.empty?)
20
+ end
21
+ end
22
+
23
+ class Response
24
+ attr_reader :envelope_id, :results
25
+
26
+ def initialize(envelope_id:, results:)
27
+ @envelope_id = envelope_id
28
+ @results = results
29
+ end
30
+
31
+ def success?
32
+ results.all?(&:success?)
33
+ end
34
+
35
+ def requirements
36
+ results.filter_map(&:requirement)
37
+ end
38
+
39
+ def failures
40
+ results.reject(&:success?)
41
+ end
42
+ end
43
+
44
+ def self.create(envelope_id:, &block)
45
+ raise ArgumentError, 'block is required' unless block
46
+
47
+ ops = JsonApi::Operations::BulkRequirement.new
48
+ yield ops
49
+
50
+ raw = Clicksign.bulk_operations_client.post(
51
+ "/envelopes/#{envelope_id}/bulk_requirements",
52
+ body: ops.to_h,
53
+ )
54
+
55
+ Response.new(
56
+ envelope_id: envelope_id,
57
+ results: JsonApi::AtomicResultsParser.parse(
58
+ raw,
59
+ envelope_id: envelope_id,
60
+ operations: ops.entries,
61
+ ),
62
+ )
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ module Notarial
6
+ class Document < Clicksign::Resource
7
+ self.resource_type = 'documents'
8
+
9
+ def self.retrieve(id, envelope_id:)
10
+ raw = client.get("/envelopes/#{envelope_id}/documents/#{id}")
11
+ parsed = JsonApi::Parser.parse(raw)
12
+ build_instance(parsed[:data].first, parent_id: envelope_id)
13
+ end
14
+
15
+ def self.create(envelope_id:, **attributes)
16
+ raw = client.post(
17
+ "/envelopes/#{envelope_id}/documents",
18
+ body: JsonApi::Serializer.dump(type: resource_type, attributes: attributes),
19
+ )
20
+ parsed = JsonApi::Parser.parse(raw)
21
+ build_instance(parsed[:data].first, parent_id: envelope_id)
22
+ end
23
+
24
+ def self.list_events(document_id, envelope_id:)
25
+ raw = client.get("/envelopes/#{envelope_id}/documents/#{document_id}/events")
26
+ parsed = JsonApi::Parser.parse(raw)
27
+ parsed[:data].map { |item| Event.send(:build_instance, item) }
28
+ end
29
+
30
+ def base_path
31
+ "/envelopes/#{@_parent_id || envelope_id}/documents"
32
+ end
33
+
34
+ def envelope_id
35
+ @_parent_id || relationships.dig('envelope', 'data', 'id')
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ module Notarial
6
+ class Envelope < Clicksign::Resource
7
+ self.resource_type = 'envelopes'
8
+
9
+ def self.create(folder_id: nil, **attributes)
10
+ relationships = if folder_id
11
+ { folder: { data: { type: 'folders',
12
+ id: folder_id } } }
13
+ else
14
+ {}
15
+ end
16
+ super(**attributes, relationships: relationships)
17
+ end
18
+
19
+ # POST /envelopes/:id/activate — rota disponível mas prefira
20
+ # envelope.update(status: 'running') via PATCH, que é o caminho
21
+ # recomendado e suportado por todos os ambientes.
22
+ def self.activate(id)
23
+ raw = client.post("#{endpoint}/#{id}/activate", body: {})
24
+ parsed = JsonApi::Parser.parse(raw)
25
+ build_instance(parsed[:data].first)
26
+ end
27
+
28
+ def self.list_events(envelope_id, **filters)
29
+ nested_list(envelope_id, nested_type: 'events', as: Event,
30
+ params: filter_params(**filters))
31
+ end
32
+
33
+ def self.list_documents(envelope_id, **filters)
34
+ nested_list(envelope_id, nested_type: 'documents', as: Document,
35
+ params: filter_params(**filters))
36
+ end
37
+
38
+ def self.list_signers(envelope_id, **filters)
39
+ nested_list(envelope_id, nested_type: 'signers', as: Signer,
40
+ params: filter_params(**filters))
41
+ end
42
+
43
+ def self.list_signature_watchers(envelope_id, **filters)
44
+ nested_list(envelope_id, nested_type: 'signature_watchers',
45
+ as: SignatureWatcher,
46
+ params: filter_params(**filters))
47
+ end
48
+
49
+ def self.list_requirements(envelope_id, **filters)
50
+ nested_list(
51
+ envelope_id,
52
+ nested_type: 'requirements',
53
+ as: Requirement,
54
+ params: filter_params(**filters),
55
+ )
56
+ end
57
+
58
+ def self.notify(id, message: nil, **email_customization)
59
+ attributes = {}
60
+ attributes[:message] = message if message
61
+ unless email_customization.empty?
62
+ attributes[:email_customization] =
63
+ email_customization
64
+ end
65
+ body = { data: { type: 'notifications', attributes: attributes } }
66
+ client.post("#{endpoint}/#{id}/notifications", body: body)
67
+ nil
68
+ end
69
+
70
+ def notify(message: nil, **email_customization)
71
+ self.class.notify(@id, message: message, **email_customization)
72
+ end
73
+
74
+ def folder_id
75
+ relationships.dig('folder', 'data', 'id')
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ module Notarial
6
+ class Event < Clicksign::Resource
7
+ self.resource_type = 'events'
8
+
9
+ def self.create_for_document(envelope_id:, document_id:, **attributes)
10
+ raw = client.post(
11
+ "/envelopes/#{envelope_id}/documents/#{document_id}/events",
12
+ body: JsonApi::Serializer.dump(type: resource_type, attributes: attributes),
13
+ )
14
+ parsed = JsonApi::Parser.parse(raw)
15
+ build_instance(parsed[:data].first)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clicksign
4
+ module Resources
5
+ module Notarial
6
+ class Requirement < Clicksign::Resource
7
+ self.resource_type = 'requirements'
8
+
9
+ def self.retrieve(id, envelope_id:)
10
+ raw = client.get("/envelopes/#{envelope_id}/requirements/#{id}")
11
+ parsed = JsonApi::Parser.parse(raw)
12
+ build_instance(parsed[:data].first, parent_id: envelope_id)
13
+ end
14
+
15
+ def self.create(envelope_id:, **attributes)
16
+ relationships = attributes.delete(:relationships) || {}
17
+ raw = client.post(
18
+ "/envelopes/#{envelope_id}/requirements",
19
+ body: JsonApi::Serializer.dump(
20
+ type: resource_type, attributes: attributes, relationships: relationships,
21
+ ),
22
+ )
23
+ parsed = JsonApi::Parser.parse(raw)
24
+ build_instance(parsed[:data].first, parent_id: envelope_id)
25
+ end
26
+
27
+ def self.list_for_document(document_id, **filters)
28
+ list_related('documents', document_id, **filters)
29
+ end
30
+
31
+ def self.list_for_signer(signer_id, **filters)
32
+ list_related('signers', signer_id, **filters)
33
+ end
34
+
35
+ def self.list_related(resource_type, resource_id, **filters)
36
+ raw = client.get(
37
+ "/#{resource_type}/#{resource_id}/relationships/requirements",
38
+ params: filter_params(**filters),
39
+ )
40
+ parsed = JsonApi::Parser.parse(raw)
41
+ parsed[:data].map { |item| build_instance(item) }
42
+ end
43
+ private_class_method :list_related
44
+
45
+ def base_path
46
+ "/envelopes/#{@_parent_id || envelope_id}/requirements"
47
+ end
48
+
49
+ def envelope_id
50
+ @_parent_id || relationships.dig('envelope', 'data', 'id')
51
+ end
52
+
53
+ def document_id
54
+ relationships.dig('document', 'data', 'id')
55
+ end
56
+
57
+ def signer_id
58
+ relationships.dig('signer', 'data', 'id')
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end