formable 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 661116dc7b31f9ec87081b7cd8a217c9af6990a9d2b36155aabb791c1de681fc
4
+ data.tar.gz: a8a7a898ad4e18b48d2b0662614780e5cc5d6fd59ff1f1bc30ad274148ee04fd
5
+ SHA512:
6
+ metadata.gz: 78617740a61a52f1df0e1423915e24d1d434a9008a364485418637f9abf35720725da61d8ecbfa57ce3e3f6e227ad1eeb6f1c3c86811d9a5be6724670d703f2f
7
+ data.tar.gz: fe15e224a3f82d7b3b05185320a27112887467848fc7fa2699c4d73de1fafb5b3831f05262db90a3f87e3ecd51bd17df1bf9c218a9cf4c43b03b35e2ca1dcd82
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Formable
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # formable-ruby
2
+
3
+ Official Ruby SDK for the [Formable API](https://api.formabledocs.com) (v1). Covers templates, signature requests, redlining, and billing.
4
+
5
+ - Faraday HTTP client (injectable)
6
+ - Keyword arguments and snake_case method names
7
+ - Ruby 3.1+
8
+
9
+ ## Installation
10
+
11
+ ```ruby
12
+ # Gemfile
13
+ gem "formable"
14
+ ```
15
+
16
+ ```bash
17
+ bundle add formable
18
+ # or
19
+ gem install formable
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ruby
25
+ require "formable"
26
+
27
+ formable = Formable.new(api_key: ENV.fetch("FORMABLE_API_KEY"))
28
+ ```
29
+
30
+ ### Templates
31
+
32
+ ```ruby
33
+ result = formable.templates.create(
34
+ file: "nda.docx",
35
+ signer_roles: [
36
+ { name: "Client", order: 0 },
37
+ { name: "Witness", order: 1 }
38
+ ]
39
+ )
40
+
41
+ template_id = result["templateId"]
42
+
43
+ # Mint a fresh edit URL later (expires after 1 day)
44
+ edit = formable.templates.create_edit_url(template_id)
45
+ puts edit["editUrl"], edit["expiresAt"]
46
+ ```
47
+
48
+ `file` accepts a path, binary string, or IO. Pass `filename:` when `file` is not a path.
49
+
50
+ ### Signature requests
51
+
52
+ ```ruby
53
+ # Formable emails each signer a signing link
54
+ request = formable.signature_requests.create(
55
+ template_id: template_id,
56
+ signers: [
57
+ { email: "jane@example.com", name: "Jane Doe", role: "Client" },
58
+ { email: "bob@example.com", name: "Bob Smith", role: "Witness" }
59
+ ]
60
+ )
61
+
62
+ # Embedded flow: mint signing URLs to embed in an iframe yourself
63
+ embedded = formable.signature_requests.create_embedded(
64
+ template_id: template_id,
65
+ signers: [{ email: "jane@example.com", name: "Jane Doe", role: "Client" }],
66
+ test_mode: true
67
+ )
68
+
69
+ signer = embedded["signers"].first
70
+ signing = formable.signature_requests.create_signing_url(
71
+ signer["recipientSignatureId"]
72
+ )
73
+
74
+ # Track progress
75
+ current = formable.signature_requests.get(embedded["signatureRequestId"])
76
+ all_requests = formable.signature_requests.list(
77
+ updated_since: Time.utc(2026, 1, 1)
78
+ )
79
+ events = formable.signature_requests.get_events(embedded["signatureRequestId"])
80
+
81
+ # Download the signed document once completed
82
+ envelope = formable.signature_requests.get_signed_envelope(
83
+ embedded["signatureRequestId"]
84
+ )
85
+ puts envelope["signedEnvelopePresignedUrl"]
86
+ ```
87
+
88
+ ### Redline requests
89
+
90
+ ```ruby
91
+ created = formable.redline_requests.create(
92
+ template_id: template_id,
93
+ members: [
94
+ { email: "us@example.com", display_name: "John Doe", role: "DisclosingParty" },
95
+ { email: "them@example.com", display_name: "Jane Smith", role: "ReceivingParty" }
96
+ ],
97
+ metadata: { subject: "Mutual NDA" }
98
+ )
99
+
100
+ redline_request_id = created["redlineRequestId"]
101
+
102
+ # Mint a redline URL for a member (embed in an iframe)
103
+ url = formable.redline_requests.create_url(redline_request_id, "them@example.com")
104
+
105
+ # Manage members and track progress
106
+ formable.redline_requests.update_members(
107
+ redline_request_id,
108
+ [{ email: "counsel@example.com", display_name: "Counsel", role: "ReceivingCounsel" }]
109
+ )
110
+ redline = formable.redline_requests.get(redline_request_id)
111
+ events = formable.redline_requests.get_events(redline_request_id)
112
+ ```
113
+
114
+ ### Billing and health
115
+
116
+ ```ruby
117
+ billing = formable.billing
118
+ puts billing["numberOfRedliningSessions"]
119
+
120
+ health = formable.health
121
+ ```
122
+
123
+ ## Error handling
124
+
125
+ All non-2xx responses raise a `Formable::Error` with the server's error message, HTTP status, and parsed response body.
126
+
127
+ ```ruby
128
+ begin
129
+ formable.signature_requests.get("missing-id")
130
+ rescue Formable::Error => error
131
+ warn "#{error.status} #{error.message}"
132
+ end
133
+ ```
134
+
135
+ ## Configuration
136
+
137
+ | Option | Description | Default |
138
+ | ------------ | --------------------------------------------------------- | --------------------------------- |
139
+ | `api_key` | Your Formable API key (sent as a bearer token). Required. | - |
140
+ | `base_url` | Override the API base URL. | `https://api.formabledocs.com/v1` |
141
+ | `timeout` | Per-request timeout in seconds. | `60` |
142
+ | `connection` | Custom `Faraday::Connection`. | Built-in client with 60s timeout |
143
+
144
+ Request hashes accept snake_case keys (`template_id`, `display_name`, `field_id`). Responses use the API's camelCase field names (`templateId`, `displayName`).
145
+
146
+ ## Development
147
+
148
+ ```bash
149
+ bundle install
150
+ bundle exec rake test
151
+ ```
152
+
153
+ ## Publishing
154
+
155
+ ```bash
156
+ gem build formable.gemspec
157
+ gem push formable-*.gem
158
+ ```
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formable
4
+ class Client
5
+ attr_reader :templates, :signature_requests, :redline_requests
6
+
7
+ def initialize(api_key:, base_url: nil, timeout: 60, connection: nil)
8
+ @http = Http.new(
9
+ api_key: api_key,
10
+ base_url: base_url,
11
+ timeout: timeout,
12
+ connection: connection
13
+ )
14
+ @templates = Templates.new(@http)
15
+ @signature_requests = SignatureRequests.new(@http)
16
+ @redline_requests = RedlineRequests.new(@http)
17
+ end
18
+
19
+ def billing
20
+ @http.get("/billing")
21
+ end
22
+
23
+ def health
24
+ @http.get("/health")
25
+ end
26
+
27
+ def close
28
+ @http.close
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formable
4
+ # Raised for any non-2xx response from the Formable API.
5
+ class Error < StandardError
6
+ attr_reader :status, :body
7
+
8
+ def initialize(message, status:, body: nil)
9
+ super(message)
10
+ @status = status
11
+ @body = body
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/multipart"
5
+ require "json"
6
+
7
+ module Formable
8
+ class Http
9
+ def initialize(api_key:, base_url: nil, timeout: 60, connection: nil)
10
+ raise ArgumentError, "Formable API key is required" if api_key.nil? || api_key.strip.empty?
11
+
12
+ @api_key = api_key
13
+ @base_url = (base_url || DEFAULT_BASE_URL).sub(%r{/+\z}, "")
14
+ @connection = connection || build_connection(timeout)
15
+ end
16
+
17
+ def get(path, query = {})
18
+ request(:get, path, query: query)
19
+ end
20
+
21
+ def post(path, body = nil)
22
+ request(:post, path, json: body)
23
+ end
24
+
25
+ def put(path, body = nil)
26
+ request(:put, path, json: body)
27
+ end
28
+
29
+ def post_form(path, fields)
30
+ request(:post, path, form: fields)
31
+ end
32
+
33
+ def close
34
+ @connection.close if @connection.respond_to?(:close)
35
+ end
36
+
37
+ private
38
+
39
+ def build_connection(timeout)
40
+ Faraday.new do |conn|
41
+ conn.request :multipart
42
+ conn.options.timeout = timeout
43
+ conn.options.open_timeout = timeout
44
+ conn.adapter Faraday.default_adapter
45
+ end
46
+ end
47
+
48
+ def request(method, path, query: nil, json: nil, form: nil)
49
+ response = @connection.run_request(method, url(path), nil, headers) do |req|
50
+ req.params.update(Params.compact(query)) if query
51
+ if form
52
+ req.body = form
53
+ elsif !json.nil?
54
+ req.headers["Content-Type"] = "application/json"
55
+ req.body = JSON.generate(json)
56
+ end
57
+ end
58
+ handle(response)
59
+ end
60
+
61
+ def url(path)
62
+ @base_url + path
63
+ end
64
+
65
+ def headers
66
+ {
67
+ "Authorization" => "Bearer #{@api_key}",
68
+ "Accept" => "application/json",
69
+ "User-Agent" => "formable-ruby/#{VERSION}"
70
+ }
71
+ end
72
+
73
+ def handle(response)
74
+ body = parse_body(response.body)
75
+ return body if response.success?
76
+
77
+ message = body.is_a?(Hash) ? body["error"] : nil
78
+ raise Error.new(
79
+ message || "Request failed with status #{response.status}",
80
+ status: response.status,
81
+ body: body
82
+ )
83
+ end
84
+
85
+ def parse_body(text)
86
+ return if text.nil? || text.empty?
87
+
88
+ JSON.parse(text)
89
+ rescue JSON::ParserError
90
+ text
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Formable
6
+ module Params
7
+ module_function
8
+
9
+ def encode_path(value)
10
+ URI.encode_uri_component(value)
11
+ end
12
+
13
+ def iso8601(value)
14
+ return if value.nil?
15
+ return value.iso8601 if value.respond_to?(:iso8601)
16
+
17
+ value.to_s
18
+ end
19
+
20
+ def compact(hash)
21
+ hash.reject { |_, value| value.nil? }
22
+ end
23
+
24
+ def camelize_keys(value)
25
+ case value
26
+ when Array
27
+ value.map { |item| camelize_keys(item) }
28
+ when Hash
29
+ value.each_with_object({}) do |(key, item), result|
30
+ result[camelize_key(key)] = camelize_keys(item)
31
+ end
32
+ else
33
+ value
34
+ end
35
+ end
36
+
37
+ def json_body(hash)
38
+ compact(camelize_keys(hash))
39
+ end
40
+
41
+ def camelize_key(key)
42
+ key.to_s.gsub(/_([a-z\d])/) { ::Regexp.last_match(1).upcase }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formable
4
+ class RedlineRequests
5
+ def initialize(http)
6
+ @http = http
7
+ end
8
+
9
+ def create(template_id:, members:, test_mode: nil, metadata: nil)
10
+ @http.post("/redline-requests", Params.json_body(
11
+ template_id: template_id,
12
+ members: members,
13
+ test_mode: test_mode,
14
+ metadata: metadata
15
+ ))
16
+ end
17
+
18
+ def list(updated_since: nil)
19
+ @http.get("/redline-requests", "updatedSince" => Params.iso8601(updated_since))
20
+ end
21
+
22
+ def get(redline_request_id)
23
+ @http.get(path(redline_request_id))
24
+ end
25
+
26
+ def update_members(redline_request_id, members)
27
+ @http.put(
28
+ path(redline_request_id, "/members"),
29
+ Params.json_body(members: members)
30
+ )
31
+ end
32
+
33
+ def create_url(redline_request_id, member_email)
34
+ @http.post(
35
+ path(redline_request_id, "/url"),
36
+ Params.json_body(member_email: member_email)
37
+ )
38
+ end
39
+
40
+ def get_events(redline_request_id)
41
+ @http.get(path(redline_request_id, "/events"))
42
+ end
43
+
44
+ private
45
+
46
+ def path(redline_request_id, suffix = "")
47
+ "/redline-requests/#{Params.encode_path(redline_request_id)}#{suffix}"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formable
4
+ class SignatureRequests
5
+ def initialize(http)
6
+ @http = http
7
+ end
8
+
9
+ def create(template_id:, signers:, sender: nil, test_mode: nil, fields: nil)
10
+ @http.post("/signature-requests", create_body(
11
+ template_id: template_id,
12
+ signers: signers,
13
+ sender: sender,
14
+ test_mode: test_mode,
15
+ fields: fields
16
+ ))
17
+ end
18
+
19
+ def create_embedded(template_id:, signers:, sender: nil, test_mode: nil, fields: nil)
20
+ @http.post("/signature-requests/embedded", create_body(
21
+ template_id: template_id,
22
+ signers: signers,
23
+ sender: sender,
24
+ test_mode: test_mode,
25
+ fields: fields
26
+ ))
27
+ end
28
+
29
+ def list(updated_since: nil)
30
+ @http.get("/signature-requests", "updatedSince" => Params.iso8601(updated_since))
31
+ end
32
+
33
+ def get(signature_request_id)
34
+ @http.get(path(signature_request_id))
35
+ end
36
+
37
+ def get_events(signature_request_id)
38
+ @http.get(path(signature_request_id, "/events"))
39
+ end
40
+
41
+ def get_signed_envelope(signature_request_id)
42
+ @http.get(path(signature_request_id, "/signed-envelope"))
43
+ end
44
+
45
+ def create_signing_url(recipient_signature_id)
46
+ @http.post("/recipient-signatures/#{Params.encode_path(recipient_signature_id)}/url")
47
+ end
48
+
49
+ private
50
+
51
+ def create_body(template_id:, signers:, sender:, test_mode:, fields:)
52
+ Params.json_body(
53
+ template_id: template_id,
54
+ signers: signers,
55
+ sender: sender,
56
+ test_mode: test_mode,
57
+ fields: fields
58
+ )
59
+ end
60
+
61
+ def path(signature_request_id, suffix = "")
62
+ "/signature-requests/#{Params.encode_path(signature_request_id)}#{suffix}"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "pathname"
5
+ require "stringio"
6
+
7
+ require "faraday/multipart"
8
+
9
+ module Formable
10
+ class Templates
11
+ def initialize(http)
12
+ @http = http
13
+ end
14
+
15
+ def create(file:, filename: nil, signer_roles: nil)
16
+ io_or_path, resolved_filename = normalize_file(file, filename)
17
+ fields = {
18
+ "file" => Faraday::Multipart::FilePart.new(
19
+ io_or_path,
20
+ "application/octet-stream",
21
+ resolved_filename
22
+ ),
23
+ "filename" => resolved_filename
24
+ }
25
+ unless signer_roles.nil?
26
+ fields["signer_roles"] = JSON.generate(signer_roles)
27
+ end
28
+ @http.post_form("/templates", fields)
29
+ end
30
+
31
+ def create_edit_url(template_id)
32
+ @http.post("/templates/#{Params.encode_path(template_id)}/edit-url")
33
+ end
34
+
35
+ private
36
+
37
+ def normalize_file(file, filename)
38
+ case file
39
+ when Pathname
40
+ raise ArgumentError, "Unable to read file: #{file}" unless file.file?
41
+
42
+ [file.to_s, filename || file.basename.to_s]
43
+ when String
44
+ if File.file?(file)
45
+ [file, filename || File.basename(file)]
46
+ else
47
+ require_filename!(filename, "file is not a path")
48
+ [StringIO.new(file), filename]
49
+ end
50
+ else
51
+ unless file.respond_to?(:read)
52
+ raise ArgumentError, "file must be a path, IO, or binary string"
53
+ end
54
+
55
+ require_filename!(filename, "file is an IO")
56
+ [file, filename]
57
+ end
58
+ end
59
+
60
+ def require_filename!(filename, reason)
61
+ return unless filename.nil? || filename.empty?
62
+
63
+ raise ArgumentError, "filename is required when #{reason}"
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formable
4
+ VERSION = "0.1.0"
5
+ end
data/lib/formable.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "formable/version"
4
+ require_relative "formable/error"
5
+ require_relative "formable/params"
6
+ require_relative "formable/http"
7
+ require_relative "formable/resources/templates"
8
+ require_relative "formable/resources/signature_requests"
9
+ require_relative "formable/resources/redline_requests"
10
+ require_relative "formable/client"
11
+
12
+ module Formable
13
+ DEFAULT_BASE_URL = "https://api.formabledocs.com/v1"
14
+
15
+ def self.new(**kwargs)
16
+ Client.new(**kwargs)
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: formable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Formable
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.9'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.9'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday-multipart
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ description: Official Ruby SDK for the Formable API. Covers templates, signature requests,
41
+ redlining, and billing.
42
+ email:
43
+ - matt@formabledocs.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE
49
+ - README.md
50
+ - lib/formable.rb
51
+ - lib/formable/client.rb
52
+ - lib/formable/error.rb
53
+ - lib/formable/http.rb
54
+ - lib/formable/params.rb
55
+ - lib/formable/resources/redline_requests.rb
56
+ - lib/formable/resources/signature_requests.rb
57
+ - lib/formable/resources/templates.rb
58
+ - lib/formable/version.rb
59
+ homepage: https://www.formabledocs.com
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ homepage_uri: https://www.formabledocs.com
64
+ source_code_uri: https://github.com/FormableDocs/formable-ruby
65
+ bug_tracker_uri: https://github.com/FormableDocs/formable-ruby/issues
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '3.1'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 4.0.16
81
+ specification_version: 4
82
+ summary: Official Ruby SDK for the Formable API
83
+ test_files: []