Skip to content

Commit 4119ed7

Browse files
committed
v1.3.1: Add tag API coverage
1 parent 6177275 commit 4119ed7

14 files changed

Lines changed: 386 additions & 6 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
All notable changes to the `assinafy` Ruby gem are documented here.
44

5+
## 1.3.1
6+
7+
### Added
8+
9+
- `Client#tags` and `Resources::TagResource` for the documented workspace tag
10+
endpoints.
11+
- Document tag helpers on `Resources::DocumentResource`: `list_tags`,
12+
`replace_tags`, `append_tags`, and `detach_tag`.
13+
14+
### Fixed
15+
16+
- Assignment signer payloads now preserve the documented `step` field for
17+
sequential signing.
18+
519
## 1.3.0
620

721
### Added

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ client = Assinafy::Client.new(
8989

9090
## Resources
9191

92-
The eight resource accessors on `Assinafy::Client` cover every documented endpoint:
92+
The nine resource accessors on `Assinafy::Client` cover every documented endpoint:
9393

9494
| Accessor | What it covers |
9595
| --------------------------- | -------------------------------------------------------------- |
@@ -99,6 +99,7 @@ The eight resource accessors on `Assinafy::Client` cover every documented endpoi
9999
| `client.signer_documents` | Signer-authenticated multi-document operations |
100100
| `client.assignments` | Create/sign/decline/resend/estimate assignments |
101101
| `client.templates` | Template CRUD |
102+
| `client.tags` | Workspace tags |
102103
| `client.fields` | Field definitions + validation + type catalog |
103104
| `client.webhooks` | Subscription, event-type catalog, dispatch history, retries |
104105
| `client.webhook_verifier` | Optional HMAC-SHA256 verifier for signed deliveries |
@@ -133,6 +134,10 @@ client.documents.delete('document-id')
133134
client.documents.verify('signature-hash')
134135
client.documents.public_info('document-id')
135136
client.documents.send_token('document-id', recipient: 'alice@example.com', channel: 'email')
137+
client.documents.list_tags('document-id')
138+
client.documents.replace_tags('document-id', ['Contracts', '2026-Q1'])
139+
client.documents.append_tags('document-id', ['Urgent'])
140+
client.documents.detach_tag('document-id', 'tag-id')
136141

137142
# Template-driven creation
138143
client.documents.create_from_template(
@@ -183,7 +188,7 @@ client.signers.download_signature(signer_access_code: 'code', type: 'signature')
183188
client.assignments.create(
184189
'document-id',
185190
method: 'virtual',
186-
signers: [{ id: 'signer-1', verification_method: 'Email', notification_methods: ['Email'] }],
191+
signers: [{ id: 'signer-1', verification_method: 'Email', notification_methods: ['Email'], step: 1 }],
187192
message: 'Please sign',
188193
expires_at: '2026-12-31T23:59:00Z',
189194
copy_receivers: ['cc-signer-id']
@@ -236,6 +241,16 @@ client.templates.create(name: 'My template', message: 'Please sign')
236241
client.templates.update('template-id', name: 'Renamed template')
237242
```
238243

244+
### Tags
245+
246+
```ruby
247+
client.tags.list(search: 'contract')
248+
client.tags.create(name: 'Contracts', color: 'ff8800')
249+
client.tags.update('tag-id', name: 'Sales Contracts', color: nil)
250+
client.tags.delete('tag-id') # fails with 409 if the tag is in use
251+
client.tags.delete('tag-id', force: true) # detaches from documents/templates first
252+
```
253+
239254
### Fields
240255

241256
```ruby

assinafy.gemspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
1010

1111
spec.summary = 'Ruby SDK for the Assinafy digital signature API'
1212
spec.description = 'Ruby SDK for Assinafy. Covers the documented authentication, ' \
13-
'document, signer, assignment, webhook, template, and field APIs, plus the high-level ' \
13+
'document, signer, assignment, webhook, template, tag, and field APIs, plus the high-level ' \
1414
'upload_and_request_signatures helper.'
1515
spec.homepage = 'https://github.com/assinafy/ruby-sdk'
1616
spec.license = 'MIT'
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
2727
spec.files = Dir['lib/**/*.rb', 'CHANGELOG.md', 'LICENSE', 'README.md']
2828
spec.require_paths = ['lib']
2929

30-
spec.add_dependency 'faraday', '>= 1.10', '< 3.0'
30+
spec.add_dependency 'faraday', '>= 2.14.2', '< 3.0'
3131
spec.add_dependency 'faraday-multipart', '>= 1.0', '< 2.0'
3232

3333
spec.add_development_dependency 'bundler-audit', '~> 0.9'

lib/assinafy.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
require_relative 'assinafy/resources/webhook_resource'
1919
require_relative 'assinafy/resources/template_resource'
2020
require_relative 'assinafy/resources/field_resource'
21+
require_relative 'assinafy/resources/tag_resource'
2122
require_relative 'assinafy/support/webhook_verifier'
2223
require_relative 'assinafy/client'

lib/assinafy/client.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class Client
3131
attr_reader :templates
3232
# @return [Resources::FieldResource]
3333
attr_reader :fields
34+
# @return [Resources::TagResource]
35+
attr_reader :tags
3436
# @return [Support::WebhookVerifier]
3537
attr_reader :webhook_verifier
3638

@@ -65,6 +67,7 @@ def initialize(api_key: nil, token: nil, account_id: nil,
6567
@webhooks = Resources::WebhookResource.new(@connection, account_id, @logger)
6668
@templates = Resources::TemplateResource.new(@connection, account_id, @logger)
6769
@fields = Resources::FieldResource.new(@connection, account_id, @logger)
70+
@tags = Resources::TagResource.new(@connection, account_id, @logger)
6871
@webhook_verifier = Support::WebhookVerifier.new(webhook_secret)
6972
end
7073

lib/assinafy/resources/assignment_resource.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class << self
2222
# shape the API expects. Accepts:
2323
#
2424
# - `signers: ['id1', 'id2']` — bare IDs
25-
# - `signers: [{ id:, verification_method:, notification_methods: }]`
25+
# - `signers: [{ id:, verification_method:, notification_methods:, step: }]`
2626
# - Legacy `signer_ids:`/`signerIds:` arrays of IDs
2727
#
2828
# @param payload [Hash]
@@ -88,6 +88,7 @@ def hash_signer_ref(ref, options)
8888
normalised[:id] = id if id
8989
normalised[:verification_method] = r[:verification_method] if r[:verification_method]
9090
normalised[:notification_methods] = r[:notification_methods] if r[:notification_methods]
91+
normalised[:step] = r[:step] unless r[:step].nil?
9192

9293
return normalised if id.is_a?(String) && !id.empty?
9394
return normalised.tap { |h| h.delete(:id) } if options[:allow_signers_without_id]

lib/assinafy/resources/document_resource.rb

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,73 @@ def send_token(document_id, recipient:, channel:)
286286
end
287287
end
288288

289+
# List tags attached to a document.
290+
#
291+
# @param document_id [String]
292+
# @param account_id_override [String, nil]
293+
# @return [Array<Hash>]
294+
# @see GET /accounts/{account_id}/documents/{document_id}/tags
295+
def list_tags(document_id, account_id_override = nil)
296+
acc_id = account_id(account_id_override)
297+
doc_id = require_id(document_id, 'Document ID')
298+
299+
call('Failed to list document tags') do
300+
http_get("accounts/#{acc_id}/documents/#{doc_id}/tags")
301+
end
302+
end
303+
304+
# Replace the document's full tag set. Passing an empty array detaches
305+
# all tags from the document.
306+
#
307+
# @param document_id [String]
308+
# @param tags [Array<String>] tag names
309+
# @param account_id_override [String, nil]
310+
# @return [Array<Hash>]
311+
# @see PUT /accounts/{account_id}/documents/{document_id}/tags
312+
def replace_tags(document_id, tags, account_id_override = nil)
313+
acc_id = account_id(account_id_override)
314+
doc_id = require_id(document_id, 'Document ID')
315+
316+
call('Failed to replace document tags') do
317+
http_put("accounts/#{acc_id}/documents/#{doc_id}/tags",
318+
body_params(tags: tag_names(tags, allow_empty: true)))
319+
end
320+
end
321+
322+
# Attach additional tags to a document without removing existing tags.
323+
#
324+
# @param document_id [String]
325+
# @param tags [Array<String>] tag names
326+
# @param account_id_override [String, nil]
327+
# @return [Array<Hash>]
328+
# @see POST /accounts/{account_id}/documents/{document_id}/tags
329+
def append_tags(document_id, tags, account_id_override = nil)
330+
acc_id = account_id(account_id_override)
331+
doc_id = require_id(document_id, 'Document ID')
332+
333+
call('Failed to append document tags') do
334+
http_post("accounts/#{acc_id}/documents/#{doc_id}/tags",
335+
body_params(tags: tag_names(tags)))
336+
end
337+
end
338+
339+
# Detach a single tag from a document. The tag itself is not deleted.
340+
#
341+
# @param document_id [String]
342+
# @param tag_id [String]
343+
# @param account_id_override [String, nil]
344+
# @return [Hash]
345+
# @see DELETE /accounts/{account_id}/documents/{document_id}/tags/{tag_id}
346+
def detach_tag(document_id, tag_id, account_id_override = nil)
347+
acc_id = account_id(account_id_override)
348+
doc_id = require_id(document_id, 'Document ID')
349+
tid = require_id(tag_id, 'Tag ID')
350+
351+
call('Failed to detach document tag') do
352+
http_delete("accounts/#{acc_id}/documents/#{doc_id}/tags/#{tid}")
353+
end
354+
end
355+
289356
# Convenience: true when the document is `certificated`, or when the
290357
# embedded assignment summary reports all signers complete.
291358
#
@@ -372,6 +439,24 @@ def template_body(signers_or_payload, options = {})
372439
body_params(body)
373440
end
374441

442+
def tag_names(tags, allow_empty: false)
443+
unless tags.is_a?(Array)
444+
raise ValidationError.new('Tags must be an Array')
445+
end
446+
447+
if tags.empty? && !allow_empty
448+
raise ValidationError.new('Tags must be a non-empty Array')
449+
end
450+
451+
tags.each do |tag|
452+
next unless tag.to_s.strip.empty?
453+
454+
raise ValidationError.new('Tag names cannot be empty')
455+
end
456+
457+
tags
458+
end
459+
375460
def artifact_type(artifact_name)
376461
value = require_id(artifact_name, 'Artifact name').to_s
377462
return value if ARTIFACT_TYPES.include?(value)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# frozen_string_literal: true
2+
3+
module Assinafy
4+
module Resources
5+
# Workspace-scoped tag management.
6+
#
7+
# Tags are labels that can be attached to documents and templates for
8+
# filtering and organization.
9+
#
10+
# See https://api.assinafy.com.br/v1/docs#tag for the full
11+
# documentation of these endpoints.
12+
class TagResource < BaseResource
13+
# List tags in the workspace.
14+
#
15+
# @param params [Hash] query parameters (`search`, `page`, `per_page`)
16+
# @param account_id_override [String, nil]
17+
# @return [Hash{Symbol=>Array,Hash}] `{ data: [...], meta: { ... } }`
18+
# @see GET /accounts/{account_id}/tags
19+
def list(params = {}, account_id_override = nil)
20+
acc_id = account_id(account_id_override)
21+
22+
call_list('Failed to list tags') do
23+
http_get("accounts/#{acc_id}/tags", params)
24+
end
25+
end
26+
27+
# Create a tag in the workspace.
28+
#
29+
# @param payload [Hash]
30+
# @option payload [String] :name required tag display name
31+
# @option payload [String, nil] :color optional 6-character hex color
32+
# @param account_id_override [String, nil]
33+
# @return [Hash]
34+
# @see POST /accounts/{account_id}/tags
35+
def create(payload, account_id_override = nil)
36+
acc_id = account_id(account_id_override)
37+
body = tag_payload(payload, require_name: true)
38+
39+
call('Failed to create tag') do
40+
http_post("accounts/#{acc_id}/tags", body)
41+
end
42+
end
43+
44+
# Update a tag's name and/or color.
45+
#
46+
# @param tag_id [String]
47+
# @param payload [Hash]
48+
# @option payload [String] :name optional new name
49+
# @option payload [String, nil] :color optional new color; nil clears it
50+
# @param account_id_override [String, nil]
51+
# @return [Hash]
52+
# @see PUT /accounts/{account_id}/tags/{tag_id}
53+
def update(tag_id, payload, account_id_override = nil)
54+
acc_id = account_id(account_id_override)
55+
tid = require_id(tag_id, 'Tag ID')
56+
body = tag_payload(payload, require_name: false)
57+
58+
call('Failed to update tag') do
59+
http_put("accounts/#{acc_id}/tags/#{tid}", body)
60+
end
61+
end
62+
63+
# Delete a tag. Pass `force: true` to detach it from documents and
64+
# templates before deletion, matching the documented query parameter.
65+
#
66+
# @param tag_id [String]
67+
# @param account_id_override [String, nil]
68+
# @param force [Boolean]
69+
# @return [Hash]
70+
# @see DELETE /accounts/{account_id}/tags/{tag_id}
71+
def delete(tag_id, account_id_override = nil, force: false)
72+
acc_id = account_id(account_id_override)
73+
tid = require_id(tag_id, 'Tag ID')
74+
params = force ? { force: true } : {}
75+
76+
call('Failed to delete tag') do
77+
http_delete("accounts/#{acc_id}/tags/#{tid}", params)
78+
end
79+
end
80+
81+
private
82+
83+
def tag_payload(payload, require_name:)
84+
raw = require_payload(payload, 'Tag payload')
85+
body = body_params(raw)
86+
name = body['name']
87+
88+
body['color'] = nil if explicit_nil_color?(raw)
89+
90+
if require_name && name.to_s.strip.empty?
91+
raise ValidationError.new('Tag name is required')
92+
end
93+
94+
body
95+
end
96+
97+
def explicit_nil_color?(payload)
98+
(payload.key?(:color) && payload[:color].nil?) ||
99+
(payload.key?('color') && payload['color'].nil?)
100+
end
101+
end
102+
end
103+
end

lib/assinafy/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module Assinafy
4-
VERSION = '1.3.0'
4+
VERSION = '1.3.1'
55
end

spec/api_coverage_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@
8585
'DocumentResource#public_info'],
8686
['PUT', '/public/documents/{document_id}/send-token',
8787
'DocumentResource#send_token'],
88+
['GET', '/accounts/{account_id}/documents/{document_id}/tags',
89+
'DocumentResource#list_tags'],
90+
['PUT', '/accounts/{account_id}/documents/{document_id}/tags',
91+
'DocumentResource#replace_tags'],
92+
['POST', '/accounts/{account_id}/documents/{document_id}/tags',
93+
'DocumentResource#append_tags'],
94+
['DELETE', '/accounts/{account_id}/documents/{document_id}/tags/{tag_id}',
95+
'DocumentResource#detach_tag'],
8896

8997
# Templates
9098
['GET', '/accounts/{account_id}/templates',
@@ -96,6 +104,16 @@
96104
['PUT', '/accounts/{account_id}/templates/{template_id}',
97105
'TemplateResource#update'],
98106

107+
# Tags
108+
['GET', '/accounts/{account_id}/tags',
109+
'TagResource#list'],
110+
['POST', '/accounts/{account_id}/tags',
111+
'TagResource#create'],
112+
['PUT', '/accounts/{account_id}/tags/{tag_id}',
113+
'TagResource#update'],
114+
['DELETE', '/accounts/{account_id}/tags/{tag_id}',
115+
'TagResource#delete'],
116+
99117
# Assignments
100118
['POST', '/documents/{documentId}/assignments',
101119
'AssignmentResource#create'],

0 commit comments

Comments
 (0)