postscale 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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +224 -0
- data/lib/postscale/attachments.rb +76 -0
- data/lib/postscale/client.rb +84 -0
- data/lib/postscale/configuration.rb +40 -0
- data/lib/postscale/errors.rb +26 -0
- data/lib/postscale/http_client.rb +280 -0
- data/lib/postscale/resources/aliases.rb +27 -0
- data/lib/postscale/resources/dkim.rb +28 -0
- data/lib/postscale/resources/domains.rb +35 -0
- data/lib/postscale/resources/emails.rb +36 -0
- data/lib/postscale/resources/inbound.rb +22 -0
- data/lib/postscale/resources/resource.rb +35 -0
- data/lib/postscale/resources/stats.rb +28 -0
- data/lib/postscale/resources/suppressions.rb +36 -0
- data/lib/postscale/resources/templates.rb +31 -0
- data/lib/postscale/resources/trust.rb +15 -0
- data/lib/postscale/resources/usage.rb +11 -0
- data/lib/postscale/resources/warming.rb +33 -0
- data/lib/postscale/resources/webhooks.rb +35 -0
- data/lib/postscale/response.rb +36 -0
- data/lib/postscale/version.rb +5 -0
- data/lib/postscale/webhook_verification.rb +107 -0
- data/lib/postscale.rb +71 -0
- data/postscale.gemspec +29 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: eb9a38ed2e7fa18e4a523855431fe47856c78d5bd7ea264be016e0cfd6909673
|
|
4
|
+
data.tar.gz: e62f7e0c2373bcf34ab465019abe871635e1d3274d4d57a8f89b4985c362220c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a3bf1e293bda9a1bf5dfa5ad1d9b60a5b119c9bbfaac94b14d292a88cafc6100964c228d232b31f4cd559b8f5361fc0ae6f2e87fc595ca09e99e3bed3a6195c3
|
|
7
|
+
data.tar.gz: f0226c8b2dbfe5c230aee5e178c754b7f5513e47df8c8f7a8a8cb71f6390136c82f0f42637c2c5c606f36df8ede84aacbe8da9dce7f7797df0fd80c2eafdb3db
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Postscale
|
|
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,224 @@
|
|
|
1
|
+
# Postscale Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for the [Postscale](https://postscale.io) email API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install postscale
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or add to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem 'postscale'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Ruby 3.0 or newer.
|
|
18
|
+
|
|
19
|
+
## Send Email
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require 'postscale'
|
|
23
|
+
|
|
24
|
+
postscale = Postscale::Client.new(api_key: ENV.fetch('POSTSCALE_API_KEY'))
|
|
25
|
+
|
|
26
|
+
result = postscale.emails.send(
|
|
27
|
+
from: 'hello@example.com',
|
|
28
|
+
to: ['user@example.com'],
|
|
29
|
+
subject: 'Welcome!',
|
|
30
|
+
html_body: '<strong>It works.</strong>'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if result.error
|
|
34
|
+
raise "#{result.error.code}: #{result.error.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
puts result.data['message_id']
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The SDK keeps Postscale API-native field names. Use `html_body`,
|
|
41
|
+
`text_body`, `template_id`, and `unsubscribe_scope`; the MVP SDK does not add
|
|
42
|
+
`html` or `text` aliases.
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
postscale = Postscale::Client.new(
|
|
48
|
+
api_key: ENV['POSTSCALE_API_KEY'],
|
|
49
|
+
base_url: 'https://api.postscale.io',
|
|
50
|
+
timeout: 30,
|
|
51
|
+
max_retries: 3,
|
|
52
|
+
headers: {}
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Option | Description | Default |
|
|
57
|
+
|--------|-------------|---------|
|
|
58
|
+
| `api_key` | Your Postscale API key. Falls back to `POSTSCALE_API_KEY`. | Required |
|
|
59
|
+
| `base_url` | API base URL | `https://api.postscale.io` |
|
|
60
|
+
| `timeout` | Request timeout in seconds | `30` |
|
|
61
|
+
| `max_retries` | Retries for idempotent `GET`, `HEAD`, and `OPTIONS` requests | `3` |
|
|
62
|
+
| `headers` | Additional default request headers | `{}` |
|
|
63
|
+
|
|
64
|
+
If no base URL is passed, the SDK reads `POSTSCALE_BASE_URL` and then falls
|
|
65
|
+
back to `https://api.postscale.io`.
|
|
66
|
+
|
|
67
|
+
## Results and Errors
|
|
68
|
+
|
|
69
|
+
Every API method returns a result object:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
result = postscale.domains.list(limit: 10)
|
|
73
|
+
|
|
74
|
+
if result.error
|
|
75
|
+
warn "#{result.error.code}: #{result.error.message}"
|
|
76
|
+
warn result.error.request_id
|
|
77
|
+
else
|
|
78
|
+
puts result.data
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
API errors are returned as `Postscale::APIError` objects inside
|
|
83
|
+
`result.error`. Configuration and local validation errors are raised before a
|
|
84
|
+
request is sent.
|
|
85
|
+
|
|
86
|
+
## Attachments
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
attachment = Postscale.attachment_from_file('invoice.pdf', 'application/pdf')
|
|
90
|
+
|
|
91
|
+
result = postscale.emails.send(
|
|
92
|
+
from: 'billing@example.com',
|
|
93
|
+
to: ['customer@example.com'],
|
|
94
|
+
subject: 'Invoice',
|
|
95
|
+
text_body: 'Attached.',
|
|
96
|
+
attachments: [attachment]
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Attachment helpers return API-native hashes with `filename`, `content`, and
|
|
101
|
+
`content_type`. Client-side checks enforce the documented limits: 10
|
|
102
|
+
attachments, 25 MB per attachment, and 50 MB total decoded attachment payload.
|
|
103
|
+
|
|
104
|
+
## Webhook Verification
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
result = Postscale.verify_webhook_signature(
|
|
108
|
+
raw_body,
|
|
109
|
+
request.get_header('HTTP_X_POSTSCALE_SIGNATURE'),
|
|
110
|
+
[current_secret, previous_secret]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
halt 401 unless result.valid?
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The verifier requires the Postscale signature format:
|
|
117
|
+
`t=<unix_seconds>,v1=<hex_hmac_sha256>`, signed over `<t>.<raw_body>`. It
|
|
118
|
+
supports multiple `v1=` signatures and multiple candidate secrets for rotation
|
|
119
|
+
windows.
|
|
120
|
+
|
|
121
|
+
## API Reference
|
|
122
|
+
|
|
123
|
+
### Emails
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
postscale.emails.send(...)
|
|
127
|
+
postscale.emails.send_batch(emails: [...])
|
|
128
|
+
postscale.emails.list(...)
|
|
129
|
+
postscale.emails.get(email_id)
|
|
130
|
+
postscale.emails.list_events(email_id)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Domains
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
postscale.domains.create(domain: 'example.com')
|
|
137
|
+
postscale.domains.list
|
|
138
|
+
postscale.domains.get(domain_id)
|
|
139
|
+
postscale.domains.update(domain_id, active: true)
|
|
140
|
+
postscale.domains.verify(domain_id)
|
|
141
|
+
postscale.domains.dns(domain_id)
|
|
142
|
+
postscale.domains.delete(domain_id)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### DKIM
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
postscale.dkim.generate(domain_id, selector: 's1')
|
|
149
|
+
postscale.dkim.list(domain_id)
|
|
150
|
+
postscale.dkim.rotate(domain_id, old_selector: 's1', new_selector: 's2')
|
|
151
|
+
postscale.dkim.deactivate(domain_id, 's1')
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Inbound
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
postscale.inbound.list(...)
|
|
158
|
+
postscale.inbound.get(email_id)
|
|
159
|
+
postscale.inbound.download_attachment(email_id, attachment_id)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Suppressions
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
postscale.suppressions.list
|
|
166
|
+
postscale.suppressions.add(email: 'user@example.com', reason: 'hard_bounce')
|
|
167
|
+
postscale.suppressions.check('user@example.com')
|
|
168
|
+
postscale.suppressions.remove('user@example.com')
|
|
169
|
+
postscale.suppressions.import_preview(...)
|
|
170
|
+
postscale.suppressions.import_job(job_id)
|
|
171
|
+
postscale.suppressions.commit_import(job_id)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Templates
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
postscale.templates.create(...)
|
|
178
|
+
postscale.templates.list
|
|
179
|
+
postscale.templates.get(template_id)
|
|
180
|
+
postscale.templates.update(template_id, ...)
|
|
181
|
+
postscale.templates.preview(template_id, ...)
|
|
182
|
+
postscale.templates.delete(template_id)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Webhooks
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
postscale.webhooks.create(url: 'https://example.com/webhooks', events: ['email.delivered'])
|
|
189
|
+
postscale.webhooks.list
|
|
190
|
+
postscale.webhooks.deliveries
|
|
191
|
+
postscale.webhooks.endpoint_deliveries(webhook_id)
|
|
192
|
+
postscale.webhooks.rotate_secret(webhook_id)
|
|
193
|
+
postscale.webhooks.delete(webhook_id)
|
|
194
|
+
postscale.webhooks.verify_signature(raw_body, signature_header, secrets)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Warming, Stats, Usage, and Trust
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
postscale.warming.get_status(domain_id)
|
|
201
|
+
postscale.warming.history(domain_id)
|
|
202
|
+
postscale.warming.start(domain_id)
|
|
203
|
+
postscale.warming.pause(domain_id)
|
|
204
|
+
postscale.warming.resume(domain_id)
|
|
205
|
+
|
|
206
|
+
postscale.stats.aggregate(domain_id)
|
|
207
|
+
postscale.stats.daily(domain_id)
|
|
208
|
+
postscale.stats.hourly(domain_id)
|
|
209
|
+
postscale.stats.isp(domain_id)
|
|
210
|
+
|
|
211
|
+
postscale.usage.summary
|
|
212
|
+
postscale.trust.get_review_request
|
|
213
|
+
postscale.trust.create_review_request(...)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Testing
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
rake test
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscale
|
|
4
|
+
module Attachments
|
|
5
|
+
MAX_ATTACHMENTS_PER_EMAIL = 10
|
|
6
|
+
MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024
|
|
7
|
+
MAX_TOTAL_ATTACHMENT_BYTES = 50 * 1024 * 1024
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def from_file(path, content_type = "application/octet-stream")
|
|
12
|
+
from_bytes(File.basename(path), File.binread(path), content_type)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def from_bytes(filename, data, content_type = "application/octet-stream")
|
|
16
|
+
{
|
|
17
|
+
"filename" => filename,
|
|
18
|
+
"content" => Base64.strict_encode64(data.to_s),
|
|
19
|
+
"content_type" => content_type
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate(attachments)
|
|
24
|
+
return if attachments.nil? || attachments.empty?
|
|
25
|
+
|
|
26
|
+
unless attachments.is_a?(Array)
|
|
27
|
+
raise ValidationError, "attachments must be an array."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if attachments.length > MAX_ATTACHMENTS_PER_EMAIL
|
|
31
|
+
raise ValidationError, "Postscale supports at most #{MAX_ATTACHMENTS_PER_EMAIL} attachments per email."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
total = 0
|
|
35
|
+
attachments.each_with_index do |attachment, index|
|
|
36
|
+
filename = value_for(attachment, "filename")
|
|
37
|
+
content = value_for(attachment, "content")
|
|
38
|
+
|
|
39
|
+
raise ValidationError, "attachments[#{index}].filename is required." if blank?(filename)
|
|
40
|
+
raise ValidationError, "attachments[#{index}].content is required." if blank?(content)
|
|
41
|
+
|
|
42
|
+
bytes = decoded_bytes(content, index)
|
|
43
|
+
if bytes > MAX_ATTACHMENT_BYTES
|
|
44
|
+
raise ValidationError, "attachments[#{index}] exceeds the 25 MB per-attachment limit."
|
|
45
|
+
end
|
|
46
|
+
total += bytes
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
raise ValidationError, "Attachments exceed the 50 MB total message limit." if total > MAX_TOTAL_ATTACHMENT_BYTES
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def value_for(hash, key)
|
|
53
|
+
return nil unless hash.respond_to?(:[])
|
|
54
|
+
|
|
55
|
+
hash[key] || hash[key.to_sym]
|
|
56
|
+
end
|
|
57
|
+
private_class_method :value_for
|
|
58
|
+
|
|
59
|
+
def blank?(value)
|
|
60
|
+
value.nil? || value.to_s.empty?
|
|
61
|
+
end
|
|
62
|
+
private_class_method :blank?
|
|
63
|
+
|
|
64
|
+
def decoded_bytes(content, index)
|
|
65
|
+
Base64.strict_decode64(strip_data_url(content.to_s)).bytesize
|
|
66
|
+
rescue ArgumentError
|
|
67
|
+
raise ValidationError, "attachments[#{index}].content must be valid base64."
|
|
68
|
+
end
|
|
69
|
+
private_class_method :decoded_bytes
|
|
70
|
+
|
|
71
|
+
def strip_data_url(value)
|
|
72
|
+
value.include?(",") ? value.split(",", 2).last : value
|
|
73
|
+
end
|
|
74
|
+
private_class_method :strip_data_url
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscale
|
|
4
|
+
class Client
|
|
5
|
+
attr_reader :config, :http
|
|
6
|
+
|
|
7
|
+
# Initialize a new Postscale API client.
|
|
8
|
+
#
|
|
9
|
+
# client = Postscale::Client.new(api_key: "ps_live_...")
|
|
10
|
+
# client = Postscale::Client.new(
|
|
11
|
+
# api_key: "ps_test_...",
|
|
12
|
+
# base_url: "https://api.postscale.io",
|
|
13
|
+
# timeout: 30,
|
|
14
|
+
# retries: 3,
|
|
15
|
+
# debug: false
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
def initialize(api_key: nil, base_url: nil, timeout: Configuration::DEFAULT_TIMEOUT, max_retries: Configuration::DEFAULT_MAX_RETRIES, headers: nil, transport: nil, sleeper: nil)
|
|
19
|
+
resolved_api_key = api_key || ENV["POSTSCALE_API_KEY"]
|
|
20
|
+
resolved_base_url = base_url || ENV["POSTSCALE_BASE_URL"] || Configuration::DEFAULT_BASE_URL
|
|
21
|
+
|
|
22
|
+
@config = Configuration.new(
|
|
23
|
+
api_key: resolved_api_key,
|
|
24
|
+
base_url: resolved_base_url,
|
|
25
|
+
timeout: timeout,
|
|
26
|
+
max_retries: max_retries,
|
|
27
|
+
headers: headers,
|
|
28
|
+
transport: transport,
|
|
29
|
+
sleeper: sleeper
|
|
30
|
+
)
|
|
31
|
+
@http = HttpClient.new(@config)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# ----- Resource accessors -----
|
|
35
|
+
|
|
36
|
+
def emails
|
|
37
|
+
@emails ||= Resources::Emails.new(@http)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def domains
|
|
41
|
+
@domains ||= Resources::Domains.new(@http)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def dkim
|
|
45
|
+
@dkim ||= Resources::DKIM.new(@http)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def aliases
|
|
49
|
+
@aliases ||= Resources::Aliases.new(@http)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def inbound
|
|
53
|
+
@inbound ||= Resources::Inbound.new(@http)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def stats
|
|
57
|
+
@stats ||= Resources::Stats.new(@http)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def warming
|
|
61
|
+
@warming ||= Resources::Warming.new(@http)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def suppressions
|
|
65
|
+
@suppressions ||= Resources::Suppressions.new(@http)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def webhooks
|
|
69
|
+
@webhooks ||= Resources::Webhooks.new(@http)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def templates
|
|
73
|
+
@templates ||= Resources::Templates.new(@http)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def usage
|
|
77
|
+
@usage ||= Resources::Usage.new(@http)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def trust
|
|
81
|
+
@trust ||= Resources::Trust.new(@http)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscale
|
|
4
|
+
class Configuration
|
|
5
|
+
DEFAULT_BASE_URL = "https://api.postscale.io"
|
|
6
|
+
DEFAULT_TIMEOUT = 30
|
|
7
|
+
DEFAULT_MAX_RETRIES = 3
|
|
8
|
+
|
|
9
|
+
attr_reader :api_key, :base_url, :timeout, :max_retries, :headers, :transport, :sleeper
|
|
10
|
+
|
|
11
|
+
def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES, headers: nil, transport: nil, sleeper: nil)
|
|
12
|
+
@api_key = api_key
|
|
13
|
+
@base_url = base_url.chomp("/")
|
|
14
|
+
@timeout = timeout
|
|
15
|
+
@max_retries = max_retries
|
|
16
|
+
@headers = headers || {}
|
|
17
|
+
@transport = transport
|
|
18
|
+
@sleeper = sleeper || Kernel.method(:sleep)
|
|
19
|
+
|
|
20
|
+
validate!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate!
|
|
24
|
+
raise ConfigurationError, "API key is required" if @api_key.nil? || @api_key.strip.empty?
|
|
25
|
+
|
|
26
|
+
raise ConfigurationError, "Timeout must be a positive number" unless @timeout.is_a?(Numeric) && @timeout.positive?
|
|
27
|
+
unless @max_retries.is_a?(Integer) && @max_retries >= 0
|
|
28
|
+
raise ConfigurationError, "max_retries must be a non-negative integer"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def live?
|
|
33
|
+
@api_key.start_with?("ps_live_")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test?
|
|
37
|
+
@api_key.start_with?("ps_test_")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscale
|
|
4
|
+
# Base error class for all Postscale SDK errors.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when the client configuration is invalid (e.g. missing API key).
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when local SDK input validation fails before a request is sent.
|
|
11
|
+
class ValidationError < Error; end
|
|
12
|
+
|
|
13
|
+
# Structured error object returned inside Postscale::Result for API and
|
|
14
|
+
# transport failures. API responses are not raised by public SDK methods.
|
|
15
|
+
class APIError < Error
|
|
16
|
+
attr_reader :code, :status_code, :request_id, :body
|
|
17
|
+
|
|
18
|
+
def initialize(message:, code: "api_error", status_code: nil, request_id: nil, body: nil)
|
|
19
|
+
@code = code
|
|
20
|
+
@status_code = status_code
|
|
21
|
+
@request_id = request_id
|
|
22
|
+
@body = body
|
|
23
|
+
super(message)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|