paubox 0.3.2 → 1.0.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.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paubox
4
+ class DynamicTemplates
5
+
6
+ attr_accessor :id, :name, :api_customer_id, :body
7
+
8
+ def initialize(args = {})
9
+ @id = args['id']
10
+ @name = args['name']
11
+ @api_customer_id = args['api_customer_id']
12
+ @body = args['body']
13
+ end
14
+
15
+ def update(template_file_path, template_name = nil)
16
+ path = "dynamic_templates/#{id}"
17
+ payload = {
18
+ data: {
19
+ name: template_name,
20
+ body: File.new(template_file_path)
21
+ }
22
+ }
23
+
24
+ Paubox::Client.new.send_request(method: :patch, payload: payload, path: path)
25
+ end
26
+
27
+ def delete
28
+ path = "dynamic_templates/#{id}"
29
+ Paubox::Client.new.send_request(method: :delete, path: path)
30
+ end
31
+
32
+ class << self
33
+ def create(template_name, template_file_path)
34
+ path = "dynamic_templates"
35
+ payload = {
36
+ data: {
37
+ name: template_name,
38
+ body: File.new(template_file_path)
39
+ }
40
+ }
41
+ Paubox::Client.new.send_request(method: :post, payload: payload, path: path)
42
+ end
43
+
44
+ def find(template_id)
45
+ path = "dynamic_templates/#{template_id.to_s}"
46
+ response = Paubox::Client.new.send_request(method: :get, path: path)
47
+ data = JSON.parse(response.body)
48
+ Paubox::DynamicTemplates.new(data)
49
+ end
50
+
51
+ def list
52
+ path = "dynamic_templates"
53
+ response = Paubox::Client.new.send_request(method: :get, path: path)
54
+ JSON.parse(response.body)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paubox
4
+ class Form
5
+ attr_reader :id, :title, :description, :form_html, :form_json, :form_css,
6
+ :vanity_url, :version, :active, :customer_id, :signable,
7
+ :signature_confirmation_label, :submission_count, :type,
8
+ :deleted, :archived, :created_at, :updated_at,
9
+ :recipient, :subscription_list_id, :old_form_id
10
+
11
+ def initialize(args = {})
12
+ @id = args['id']
13
+ @title = args['title']
14
+ @description = args['description']
15
+ @form_html = args['form_html']
16
+ @form_json = args['form_json']
17
+ @form_css = args['form_css']
18
+ @vanity_url = args['vanity_url']
19
+ @version = args['version']
20
+ @active = args['active']
21
+ @customer_id = args['customer_id']
22
+ @signable = args['signable']
23
+ @signature_confirmation_label = args['signature_confirmation_label']
24
+ @submission_count = args['submission_count']
25
+ @type = args['type']
26
+ @deleted = args['deleted']
27
+ @archived = args['archived']
28
+ @created_at = args['created_at']
29
+ @updated_at = args['updated_at']
30
+ @recipient = args['recipient']
31
+ @subscription_list_id = args['subscription_list_id']
32
+ @old_form_id = args['old_form_id']
33
+ end
34
+
35
+ def active?
36
+ @active
37
+ end
38
+
39
+ def deleted?
40
+ @deleted
41
+ end
42
+
43
+ def archived?
44
+ @archived
45
+ end
46
+
47
+ def signable?
48
+ @signable
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paubox
4
+ class FormSubmission
5
+ attr_reader :id, :form_id, :form_data, :storage_type, :storage_url,
6
+ :submitter_email, :recipients, :attachment_name,
7
+ :attachment_url, :attachment_type, :created_at
8
+
9
+ def initialize(args = {})
10
+ @id = args['id']
11
+ @form_id = args['form_id']
12
+ @form_data = parse_form_data(args['form_data'])
13
+ @storage_type = args['storage_type']
14
+ @storage_url = args['storage_url']
15
+ @submitter_email = args['submitter_email']
16
+ @recipients = args['recipients']
17
+ @attachment_name = args['attachment_name']
18
+ @attachment_url = args['attachment_url']
19
+ @attachment_type = args['attachment_type']
20
+ @created_at = args['created_at']
21
+ end
22
+
23
+ private
24
+
25
+ def parse_form_data(form_data)
26
+ return {} if form_data.nil?
27
+ return form_data if form_data.is_a?(Hash)
28
+
29
+ JSON.parse(form_data)
30
+ rescue JSON::ParserError
31
+ {}
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+ require 'rest-client'
5
+
6
+ module Paubox
7
+ class FormsClient
8
+ FORMS_HOST = 'api.paubox.com'
9
+ FORMS_PROTOCOL = 'https://'
10
+ FORMS_BASE = '/forms'
11
+
12
+ TIMEOUT_SECONDS = 30
13
+
14
+ UUID_RE = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
15
+
16
+ LIST_FORMS_PARAMS = %i[customer_id form_id search order order_by
17
+ archived active page items].freeze
18
+ LIST_SUBMISSIONS_PARAMS = %i[submission_id order_by order page items].freeze
19
+
20
+ # Raised on any non-2xx response from the Forms API. Carries only
21
+ # status_code / url / response_body so error reporters that capture
22
+ # raising-frame locals never see the Authorization header or api_key.
23
+ class Error < StandardError
24
+ attr_reader :status_code, :url, :response_body
25
+
26
+ def initialize(message, status_code: nil, url: nil, response_body: nil)
27
+ super(message)
28
+ @status_code = status_code
29
+ @url = url
30
+ @response_body = response_body
31
+ end
32
+ end
33
+
34
+ def initialize(args = {})
35
+ @host = args[:host] || FORMS_HOST
36
+ @protocol = args[:protocol] || FORMS_PROTOCOL
37
+ @base = args[:base] || FORMS_BASE
38
+ @api_key = args[:api_key] ||
39
+ (Paubox.configuration && Paubox.configuration.forms_api_key)
40
+ end
41
+
42
+ def get_form(form_id)
43
+ assert_uuid!(form_id, 'form_id')
44
+ url = "#{@protocol}#{@host}#{@base}/public/form_data/#{encode_segment(form_id)}"
45
+ response = request(method: :get, url: url, headers: { accept: :json })
46
+ Paubox::Form.new(JSON.parse(response.body))
47
+ end
48
+
49
+ def submit_form(form_id, form_data:, attachments: [])
50
+ assert_uuid!(form_id, 'form_id')
51
+ url = "#{@protocol}#{@host}#{@base}/api/forms/#{encode_segment(form_id)}/submissions"
52
+ payload = { form_data: form_data }
53
+ payload[:attachments] = attachments unless attachments.empty?
54
+ request(method: :post, url: url, payload: payload.to_json,
55
+ headers: { content_type: :json, accept: :json })
56
+ end
57
+
58
+ def list_forms(params = {})
59
+ url = "#{@protocol}#{@host}#{@base}/api/forms"
60
+ query = params.transform_keys(&:to_sym)
61
+ .select { |k, _v| LIST_FORMS_PARAMS.include?(k) }
62
+ if query[:customer_id].nil?
63
+ raise ArgumentError, 'customer_id is required for list_forms and must ' \
64
+ "match the API key's customer, e.g. " \
65
+ 'client.list_forms(customer_id: 123)'
66
+ end
67
+
68
+ response = request(method: :get, url: url,
69
+ headers: auth_headers.merge(params: query))
70
+ body = JSON.parse(response.body)
71
+ forms = (body['results'] || []).map { |form| Paubox::Form.new(form) }
72
+ { forms: forms, page_info: body['page_info'] }
73
+ end
74
+
75
+ def create_form(attrs)
76
+ url = "#{@protocol}#{@host}#{@base}/api/forms"
77
+ response = request(method: :post, url: url, payload: attrs.to_json,
78
+ headers: auth_headers.merge(content_type: :json))
79
+ JSON.parse(response.body)
80
+ end
81
+
82
+ def find_form(form_id)
83
+ assert_uuid!(form_id, 'form_id')
84
+ url = "#{@protocol}#{@host}#{@base}/api/forms/#{encode_segment(form_id)}"
85
+ response = request(method: :get, url: url, headers: auth_headers)
86
+ Paubox::Form.new(JSON.parse(response.body)['data'])
87
+ end
88
+
89
+ def update_form(form_id, attrs)
90
+ assert_uuid!(form_id, 'form_id')
91
+ url = "#{@protocol}#{@host}#{@base}/api/forms/#{encode_segment(form_id)}"
92
+ response = request(method: :put, url: url, payload: attrs.to_json,
93
+ headers: auth_headers.merge(content_type: :json))
94
+ JSON.parse(response.body)
95
+ end
96
+
97
+ def archive_form(form_id)
98
+ assert_uuid!(form_id, 'form_id')
99
+ url = "#{@protocol}#{@host}#{@base}/api/forms/#{encode_segment(form_id)}/archive"
100
+ response = request(method: :post, url: url, payload: '{}',
101
+ headers: auth_headers.merge(content_type: :json))
102
+ JSON.parse(response.body)
103
+ end
104
+
105
+ def unarchive_form(form_id)
106
+ assert_uuid!(form_id, 'form_id')
107
+ url = "#{@protocol}#{@host}#{@base}/api/forms/#{encode_segment(form_id)}/unarchive"
108
+ response = request(method: :post, url: url, payload: '{}',
109
+ headers: auth_headers.merge(content_type: :json))
110
+ JSON.parse(response.body)
111
+ end
112
+
113
+ def copy_form(form_id, title:)
114
+ assert_uuid!(form_id, 'form_id')
115
+ url = "#{@protocol}#{@host}#{@base}/api/forms/copy"
116
+ payload = { form_id: form_id, title: title }
117
+ response = request(method: :post, url: url, payload: payload.to_json,
118
+ headers: auth_headers.merge(content_type: :json))
119
+ Paubox::Form.new(JSON.parse(response.body))
120
+ end
121
+
122
+ def form_stats(customer_id: nil)
123
+ url = "#{@protocol}#{@host}#{@base}/api/forms/stats"
124
+ headers = auth_headers
125
+ headers = headers.merge(params: { customer_id: customer_id }) unless customer_id.nil?
126
+ response = request(method: :get, url: url, headers: headers)
127
+ JSON.parse(response.body)
128
+ end
129
+
130
+ def list_submissions(form_id, params = {})
131
+ assert_uuid!(form_id, 'form_id')
132
+ url = "#{@protocol}#{@host}#{@base}/api/forms/#{encode_segment(form_id)}/submissions"
133
+ query = params.transform_keys(&:to_sym)
134
+ .select { |k, _v| LIST_SUBMISSIONS_PARAMS.include?(k) }
135
+ response = request(method: :get, url: url,
136
+ headers: auth_headers.merge(params: query))
137
+ body = JSON.parse(response.body)
138
+ submissions = (body['data'] || []).map { |s| Paubox::FormSubmission.new(s) }
139
+ { submissions: submissions, total: body['total'], page: body['page'],
140
+ items: body['items'] }
141
+ end
142
+
143
+ def submissions_csv(form_id, submission_id: nil)
144
+ assert_uuid!(form_id, 'form_id')
145
+ base = "#{@protocol}#{@host}#{@base}/api/forms/#{encode_segment(form_id)}" \
146
+ '/submissions/submission-csv'
147
+ url = if submission_id.nil?
148
+ base
149
+ else
150
+ assert_uuid!(submission_id, 'submission_id')
151
+ "#{base}/#{encode_segment(submission_id)}"
152
+ end
153
+ request(method: :get, url: url, headers: auth_headers(accept: 'text/csv')).body
154
+ end
155
+
156
+ def submission_pdf(form_id, submission_id)
157
+ assert_uuid!(form_id, 'form_id')
158
+ assert_uuid!(submission_id, 'submission_id')
159
+ url = "#{@protocol}#{@host}#{@base}/api/forms/#{encode_segment(form_id)}/submissions/" \
160
+ "#{encode_segment(submission_id)}/submission-pdf"
161
+ request(method: :get, url: url, headers: auth_headers(accept: 'application/pdf')).body
162
+ end
163
+
164
+ private
165
+
166
+ def auth_headers(accept: :json)
167
+ if @api_key.nil? || @api_key.to_s.empty?
168
+ raise ArgumentError, "api_key is required for this endpoint. Pass a scoped " \
169
+ "API key with the 'forms' scope: " \
170
+ 'Paubox::FormsClient.new(api_key: ...)'
171
+ end
172
+
173
+ { authorization: "Bearer #{@api_key}", accept: accept }
174
+ end
175
+
176
+ def assert_uuid!(value, name)
177
+ return if value.is_a?(String) && value.match?(UUID_RE)
178
+
179
+ raise ArgumentError, "#{name} must be a UUID string (received #{value.inspect})"
180
+ end
181
+
182
+ def encode_segment(value)
183
+ CGI.escape(value.to_s)
184
+ end
185
+
186
+ # Funnels every request through RestClient::Request.execute so we can
187
+ # always set an explicit timeout (rest-client's default is nil) and
188
+ # translate ExceptionWithResponse into Paubox::FormsClient::Error,
189
+ # which carries only status_code / url / response_body — never the
190
+ # Authorization header or api_key. Keeps the Bearer token out of any
191
+ # error reporter that captures raising-frame locals.
192
+ def request(method:, url:, headers: {}, payload: nil)
193
+ RestClient::Request.execute(
194
+ method: method,
195
+ url: url,
196
+ payload: payload,
197
+ headers: headers,
198
+ timeout: TIMEOUT_SECONDS
199
+ )
200
+ rescue RestClient::ExceptionWithResponse => e
201
+ response = e.response
202
+ raise Error.new(
203
+ "Paubox Forms API request failed: #{e.class.name.split('::').last} " \
204
+ "(status #{response && response.code || 'unknown'})",
205
+ status_code: response && response.code,
206
+ url: url,
207
+ response_body: response && response.body
208
+ )
209
+ end
210
+ end
211
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Paubox
4
- VERSION = '0.3.2'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/paubox.rb CHANGED
@@ -2,11 +2,15 @@
2
2
 
3
3
  require 'paubox/version'
4
4
  require 'paubox/client'
5
+ require 'paubox/dynamic_templates'
5
6
  require 'paubox/format_helper'
6
7
  require 'paubox/mail_to_message'
7
8
  require 'paubox/message'
8
9
  require 'paubox/templated_message'
9
10
  require 'paubox/email_disposition'
11
+ require 'paubox/form'
12
+ require 'paubox/form_submission'
13
+ require 'paubox/forms_client'
10
14
  require 'mail/paubox'
11
15
 
12
16
  module Paubox
@@ -20,6 +24,10 @@ module Paubox
20
24
  end
21
25
 
22
26
  class Configuration
23
- attr_accessor :api_key, :api_user
27
+ attr_accessor :api_key, :forms_api_key
28
+
29
+ # Deprecated: api_user is no longer used by the Email API client.
30
+ # Kept only for backward compatibility; it has no effect on requests.
31
+ attr_accessor :api_user
24
32
  end
25
- end
33
+ end
data/paubox_ruby.gemspec CHANGED
@@ -29,6 +29,10 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency 'rspec', '~> 3.2'
30
30
  spec.add_development_dependency 'webmock', '~> 2.1'
31
31
 
32
+ # base64 and ostruct left the default gems (base64 in Ruby 3.4, ostruct in
33
+ # 4.0). lib/ requires both, so they must be declared or the gem fails to load.
34
+ spec.add_dependency 'base64'
35
+ spec.add_dependency 'ostruct'
32
36
  spec.add_dependency 'mail', '>= 2.5'
33
37
  spec.add_dependency 'rest-client', '~> 2.0', '>= 2.0.2'
34
38
  end
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "ruby",
6
+ "package-name": "paubox",
7
+ "version-file": "lib/paubox/version.rb",
8
+ "include-component-in-tag": false,
9
+ "include-v-in-tag": true,
10
+ "draft": false,
11
+ "prerelease": false
12
+ }
13
+ }
14
+ }
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paubox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paubox
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2022-10-06 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -80,6 +79,34 @@ dependencies:
80
79
  - - "~>"
81
80
  - !ruby/object:Gem::Version
82
81
  version: '2.1'
82
+ - !ruby/object:Gem::Dependency
83
+ name: base64
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: ostruct
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
83
110
  - !ruby/object:Gem::Dependency
84
111
  name: mail
85
112
  requirement: !ruby/object:Gem::Requirement
@@ -121,31 +148,42 @@ executables: []
121
148
  extensions: []
122
149
  extra_rdoc_files: []
123
150
  files:
151
+ - ".github/scripts/assert_specs_ran.rb"
152
+ - ".github/workflows/ci.yml"
153
+ - ".github/workflows/pr-title.yml"
154
+ - ".github/workflows/release-please.yml"
124
155
  - ".gitignore"
156
+ - ".release-please-manifest.json"
125
157
  - ".rspec"
126
- - ".travis.yml"
158
+ - CHANGELOG.md
159
+ - CLAUDE.md
127
160
  - Gemfile
128
161
  - LICENSE.txt
129
162
  - README.md
130
163
  - Rakefile
164
+ - api.md
131
165
  - bin/console
132
166
  - bin/setup
133
167
  - lib/mail/paubox.rb
134
168
  - lib/paubox.rb
135
169
  - lib/paubox/client.rb
170
+ - lib/paubox/dynamic_templates.rb
136
171
  - lib/paubox/email_disposition.rb
172
+ - lib/paubox/form.rb
173
+ - lib/paubox/form_submission.rb
137
174
  - lib/paubox/format_helper.rb
175
+ - lib/paubox/forms_client.rb
138
176
  - lib/paubox/mail_to_message.rb
139
177
  - lib/paubox/message.rb
140
178
  - lib/paubox/templated_message.rb
141
179
  - lib/paubox/version.rb
142
180
  - lib/paubox_ruby.rb
143
181
  - paubox_ruby.gemspec
182
+ - release-please-config.json
144
183
  homepage: https://www.paubox.com
145
184
  licenses:
146
185
  - Apache-2.0
147
186
  metadata: {}
148
- post_install_message:
149
187
  rdoc_options: []
150
188
  require_paths:
151
189
  - lib
@@ -160,8 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
198
  - !ruby/object:Gem::Version
161
199
  version: '0'
162
200
  requirements: []
163
- rubygems_version: 3.3.14
164
- signing_key:
201
+ rubygems_version: 3.6.9
165
202
  specification_version: 4
166
203
  summary: Paubox's Official Ruby SDK
167
204
  test_files: []
data/.travis.yml DELETED
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.3.1
5
- before_install: gem install bundler -v 1.14.6