hybiscus_pdf_report 0.1.0 → 0.9.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -1
- data/README.md +258 -40
- data/hybiscus_pdf_report.gemspec +2 -1
- data/lib/hybiscus_pdf_report/api_errors.rb +41 -0
- data/lib/hybiscus_pdf_report/client.rb +45 -22
- data/lib/hybiscus_pdf_report/config.rb +47 -0
- data/lib/hybiscus_pdf_report/errors.rb +32 -19
- data/lib/hybiscus_pdf_report/object.rb +7 -20
- data/lib/hybiscus_pdf_report/objects/response.rb +5 -1
- data/lib/hybiscus_pdf_report/report_builder.rb +99 -0
- data/lib/hybiscus_pdf_report/request.rb +23 -46
- data/lib/hybiscus_pdf_report/request_retry_wrapper.rb +72 -0
- data/lib/hybiscus_pdf_report/response_object.rb +47 -0
- data/lib/hybiscus_pdf_report/version.rb +2 -1
- data/lib/hybiscus_pdf_report.rb +24 -2
- data/sig/hybiscus_pdf_report.rbs +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0007a7c6d4494de9ca87ab92f259b192ad550d26172d225f5be109823a514298
|
|
4
|
+
data.tar.gz: ccc6a69f6c7d28b34b168a240b059c9e54c51a0802097ff829bb07111909ea53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: efbe50eb7882ce18576a146b77593e19364176b819973ded959f57d1f6a279982789839fb2503e34f66ca2022ff2bff5148d5c3c76d7f8ccb249cc68df756a4e
|
|
7
|
+
data.tar.gz: 63c0076efef641022cbf9be95a074bb0903b9386308ab7eb1541b6d2a456854c55831e4e17ab08171cf56fe41e4c33f4aa59605ff36edd937c2437cf5363f879
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Hybiscus
|
|
1
|
+
# Hybiscus PDF Report Ruby Gem
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Ruby client library for the [Hybiscus PDF Reports API](https://hybiscus.dev/), providing an easy way to generate PDF reports from JSON data.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -12,74 +12,289 @@ gem 'hybiscus_pdf_report'
|
|
|
12
12
|
|
|
13
13
|
And then execute:
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
16
18
|
|
|
17
19
|
Or install it yourself as:
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
```bash
|
|
22
|
+
gem install hybiscus_pdf_report
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
### Environment Variables (Recommended)
|
|
28
|
+
|
|
29
|
+
Set your API key and optionally the API URL as environment variables:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export HYBISCUS_API_KEY="your_api_key_here"
|
|
33
|
+
export HYBISCUS_API_URL="https://api.hybiscus.dev/api/v1/" # Optional, defaults to this URL
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Programmatic Configuration
|
|
37
|
+
|
|
38
|
+
You can also configure the gem programmatically:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
HybiscusPdfReport.configure do |config|
|
|
42
|
+
config.api_key = "your_api_key_here"
|
|
43
|
+
config.api_url = "https://api.hybiscus.dev/api/v1/" # Optional
|
|
44
|
+
config.timeout = 30 # Optional, defaults to 10 seconds
|
|
45
|
+
end
|
|
46
|
+
```
|
|
20
47
|
|
|
21
48
|
## Usage
|
|
22
|
-
### Configure the client
|
|
23
|
-
The Gem is configured by default to work with the public platform of Hybiscus at
|
|
24
|
-
* API: `https://api.hybiscus.dev/api/v1/`
|
|
25
49
|
|
|
26
|
-
###
|
|
50
|
+
### Creating a Client
|
|
51
|
+
|
|
27
52
|
```ruby
|
|
28
|
-
#
|
|
29
|
-
client = HybiscusPdfReport::Client.new(api_key: your_api_key)
|
|
30
|
-
# Or if you have set ENV['HIBISCUS_API_KEY'] set, you don't need to pass in the api key.
|
|
53
|
+
# Using environment variables (recommended)
|
|
31
54
|
client = HybiscusPdfReport::Client.new
|
|
32
55
|
|
|
33
|
-
#
|
|
34
|
-
client = HybiscusPdfReport::Client.new(api_key:
|
|
56
|
+
# Or passing the API key directly
|
|
57
|
+
client = HybiscusPdfReport::Client.new(api_key: "your_api_key_here")
|
|
35
58
|
|
|
36
|
-
#
|
|
37
|
-
client = HybiscusPdfReport::Client.new(
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
# With custom timeout
|
|
60
|
+
client = HybiscusPdfReport::Client.new(
|
|
61
|
+
api_key: "your_api_key_here",
|
|
62
|
+
timeout: 30
|
|
63
|
+
)
|
|
40
64
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
# With custom API URL (for private cloud instances)
|
|
66
|
+
client = HybiscusPdfReport::Client.new(
|
|
67
|
+
api_key: "your_api_key_here",
|
|
68
|
+
api_url: "https://your-private-hybiscus-instance.com/api/v1/"
|
|
69
|
+
)
|
|
45
70
|
```
|
|
46
|
-
|
|
71
|
+
|
|
72
|
+
### API Endpoints
|
|
73
|
+
|
|
74
|
+
#### 1. Build Report
|
|
75
|
+
|
|
76
|
+
Submit a report request for processing:
|
|
77
|
+
|
|
47
78
|
```ruby
|
|
48
|
-
|
|
49
|
-
|
|
79
|
+
# Your report JSON data
|
|
80
|
+
report_data = { _JSON_structure }
|
|
81
|
+
|
|
82
|
+
response = client.request.build_report(report_data)
|
|
83
|
+
|
|
84
|
+
# Access response data
|
|
85
|
+
puts response.task_id
|
|
86
|
+
puts response.status
|
|
50
87
|
```
|
|
51
88
|
|
|
52
|
-
|
|
89
|
+
#### 2. Preview Report
|
|
90
|
+
|
|
91
|
+
Generate a preview of your report without consuming your quota:
|
|
92
|
+
|
|
53
93
|
```ruby
|
|
54
|
-
response = client.request.preview_report(
|
|
94
|
+
response = client.request.preview_report(report_data)
|
|
95
|
+
puts response.task_id
|
|
96
|
+
puts response.status
|
|
55
97
|
```
|
|
56
|
-
|
|
98
|
+
|
|
99
|
+
#### 3. Check Task Status
|
|
100
|
+
|
|
101
|
+
Monitor the status of a report generation task:
|
|
102
|
+
|
|
57
103
|
```ruby
|
|
58
|
-
|
|
59
|
-
|
|
104
|
+
# Using a specific task ID
|
|
105
|
+
response = client.request.get_task_status("task_id_here")
|
|
106
|
+
|
|
107
|
+
# Or check the status of the last submitted task
|
|
60
108
|
response = client.request.get_last_task_status
|
|
109
|
+
puts response.status # "pending", "processing", "completed", "failed"
|
|
61
110
|
```
|
|
62
111
|
|
|
63
|
-
|
|
112
|
+
#### 4. Download Report
|
|
113
|
+
|
|
114
|
+
Retrieve the generated PDF report:
|
|
115
|
+
|
|
64
116
|
```ruby
|
|
65
|
-
|
|
66
|
-
|
|
117
|
+
# Using a specific task ID
|
|
118
|
+
response = client.request.get_report("task_id_here")
|
|
119
|
+
|
|
120
|
+
# Or get the last generated report
|
|
67
121
|
response = client.request.get_last_report
|
|
122
|
+
|
|
123
|
+
# The report is base64 encoded
|
|
124
|
+
pdf_content = Base64.decode64(response.report)
|
|
125
|
+
|
|
126
|
+
# Save to file
|
|
127
|
+
File.open("report.pdf", "wb") do |file|
|
|
128
|
+
file.write(pdf_content)
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### 5. Check Remaining Quota
|
|
133
|
+
|
|
134
|
+
Check your remaining API quota:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
response = client.request.get_remaining_quota
|
|
138
|
+
puts response.remaining_single_page_reports
|
|
139
|
+
puts response.remaining_multi_page_reports
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Complete Workflow Example
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
require 'hybiscus_pdf_report'
|
|
146
|
+
require 'base64'
|
|
147
|
+
|
|
148
|
+
# Initialize client
|
|
149
|
+
client = HybiscusPdfReport::Client.new
|
|
150
|
+
|
|
151
|
+
# Prepare report data
|
|
152
|
+
report_data = { _SOME_JSON_STRUCTURE_ }
|
|
153
|
+
|
|
154
|
+
begin
|
|
155
|
+
# Submit report for processing
|
|
156
|
+
response = client.request.build_report(report_data)
|
|
157
|
+
task_id = response.task_id
|
|
158
|
+
|
|
159
|
+
puts "Report submitted. Task ID: #{task_id}"
|
|
160
|
+
|
|
161
|
+
# Poll for completion
|
|
162
|
+
loop do
|
|
163
|
+
status_response = client.request.get_task_status(task_id)
|
|
164
|
+
status = status_response.status
|
|
165
|
+
|
|
166
|
+
puts "Current status: #{status}"
|
|
167
|
+
|
|
168
|
+
case status
|
|
169
|
+
when "completed"
|
|
170
|
+
puts "Report generation completed!"
|
|
171
|
+
break
|
|
172
|
+
when "failed"
|
|
173
|
+
puts "Report generation failed!"
|
|
174
|
+
exit 1
|
|
175
|
+
when "pending", "processing"
|
|
176
|
+
puts "Still processing... waiting 5 seconds"
|
|
177
|
+
sleep 5
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Download the completed report
|
|
182
|
+
report_response = client.request.get_report(task_id)
|
|
183
|
+
pdf_content = Base64.decode64(report_response.report)
|
|
184
|
+
|
|
185
|
+
# Save to file
|
|
186
|
+
File.open("generated_report.pdf", "wb") do |file|
|
|
187
|
+
file.write(pdf_content)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
puts "Report saved as generated_report.pdf"
|
|
191
|
+
|
|
192
|
+
rescue HybiscusPdfReport::ApiErrors::ApiRequestsQuotaReachedError => e
|
|
193
|
+
puts "API quota reached: #{e.message}"
|
|
194
|
+
rescue HybiscusPdfReport::ApiErrors::PaymentRequiredError => e
|
|
195
|
+
puts "Payment required: #{e.message}"
|
|
196
|
+
rescue HybiscusPdfReport::ApiErrors::RateLimitError => e
|
|
197
|
+
puts "Rate limit error persisted after automatic retries: #{e.message}"
|
|
198
|
+
rescue HybiscusPdfReport::ApiErrors::ApiError => e
|
|
199
|
+
puts "API error: #{e.message}"
|
|
200
|
+
rescue ArgumentError => e
|
|
201
|
+
puts "Argument error: #{e.message}"
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Error Handling
|
|
206
|
+
|
|
207
|
+
The gem includes specific error classes for different API error conditions and **automatically handles transient errors** like rate limits and network timeouts with exponential backoff retry logic.
|
|
208
|
+
|
|
209
|
+
#### Automatic Retry Handling
|
|
210
|
+
|
|
211
|
+
The gem automatically retries the following errors up to 5 times with exponential backoff (1s, 2s, 4s, 8s, 16s):
|
|
212
|
+
|
|
213
|
+
- `RateLimitError` (HTTP 503) - When the API is temporarily overloaded
|
|
214
|
+
- `Faraday::TimeoutError` - Network timeout errors
|
|
215
|
+
- `Faraday::ConnectionFailed` - Network connection failures
|
|
216
|
+
|
|
217
|
+
You don't need to handle these errors manually - the gem will automatically retry and only raise an exception if all retry attempts are exhausted.
|
|
218
|
+
|
|
219
|
+
#### Manual Error Handling
|
|
220
|
+
|
|
221
|
+
For other API errors, you should handle them explicitly in your code:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
begin
|
|
225
|
+
response = client.request.build_report(report_data)
|
|
226
|
+
rescue HybiscusPdfReport::ApiErrors::ApiRequestsQuotaReachedError => e
|
|
227
|
+
puts "API quota reached (HTTP 429). Please upgrade your plan."
|
|
228
|
+
rescue HybiscusPdfReport::ApiErrors::PaymentRequiredError => e
|
|
229
|
+
puts "Payment required (HTTP 402). Please check your account."
|
|
230
|
+
rescue HybiscusPdfReport::ApiErrors::UnauthorizedError => e
|
|
231
|
+
puts "Unauthorized (HTTP 401). Please check your API key."
|
|
232
|
+
rescue HybiscusPdfReport::ApiErrors::BadRequestError => e
|
|
233
|
+
puts "Bad request (HTTP 400). Please check your request data."
|
|
234
|
+
rescue HybiscusPdfReport::ApiErrors::RateLimitError => e
|
|
235
|
+
puts "Rate limit error persisted after retries. Please try again later."
|
|
236
|
+
rescue HybiscusPdfReport::ApiErrors::ApiError => e
|
|
237
|
+
puts "API error: #{e.message} (HTTP #{e.status_code})"
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### Available Error Classes
|
|
242
|
+
|
|
243
|
+
- `BadRequestError` (HTTP 400) - Invalid request data
|
|
244
|
+
- `UnauthorizedError` (HTTP 401) - Invalid or missing API key
|
|
245
|
+
- `PaymentRequiredError` (HTTP 402) - Payment required for the account
|
|
246
|
+
- `ForbiddenError` (HTTP 403) - Access forbidden
|
|
247
|
+
- `NotFoundError` (HTTP 404) - Resource not found
|
|
248
|
+
- `UnprocessableContentError` (HTTP 422) - Request data cannot be processed
|
|
249
|
+
- `ApiRequestsQuotaReachedError` (HTTP 429) - API request quota exceeded
|
|
250
|
+
- `RateLimitError` (HTTP 503) - Rate limit exceeded (automatically retried)
|
|
251
|
+
|
|
252
|
+
### Response Objects
|
|
253
|
+
|
|
254
|
+
All API responses return `Response` objects that provide dynamic attribute access:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
response = client.request.build_report(report_data)
|
|
258
|
+
|
|
259
|
+
# Access attributes dynamically
|
|
260
|
+
puts response.task_id
|
|
261
|
+
puts response.status
|
|
262
|
+
|
|
263
|
+
# Response objects support nested attribute access
|
|
264
|
+
if response.respond_to?(:error)
|
|
265
|
+
puts response.error.message
|
|
266
|
+
end
|
|
68
267
|
```
|
|
69
268
|
|
|
70
269
|
## Development
|
|
71
270
|
|
|
72
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake
|
|
271
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
272
|
+
|
|
273
|
+
### Running Tests
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
bundle exec rspec
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Console Testing
|
|
73
280
|
|
|
74
|
-
|
|
281
|
+
```bash
|
|
282
|
+
bin/console
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Then in the console:
|
|
75
286
|
|
|
76
|
-
To test the application in the console
|
|
77
287
|
```ruby
|
|
78
|
-
client = HybiscusPdfReport::Client.new(api_key:
|
|
79
|
-
|
|
80
|
-
|
|
288
|
+
client = HybiscusPdfReport::Client.new(api_key: "your_api_key")
|
|
289
|
+
response = client.request.get_remaining_quota
|
|
290
|
+
puts response.remaining_single_page_reports
|
|
81
291
|
```
|
|
82
292
|
|
|
293
|
+
## Requirements
|
|
294
|
+
|
|
295
|
+
- Ruby >= 3.0.0
|
|
296
|
+
- Faraday HTTP client library
|
|
297
|
+
|
|
83
298
|
## Contributing
|
|
84
299
|
|
|
85
300
|
Bug reports and pull requests are welcome on GitHub at https://github.com/Timly-Software-AG/HybiscusPdfReportRubyGem.
|
|
@@ -88,7 +303,10 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/Timly-
|
|
|
88
303
|
|
|
89
304
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
90
305
|
|
|
91
|
-
##
|
|
92
|
-
|
|
306
|
+
## Links
|
|
307
|
+
|
|
308
|
+
- [Hybiscus PDF Reports API Documentation](https://hybiscus.dev/)
|
|
309
|
+
- [GitHub Repository](https://github.com/Timly-Software-AG/HybiscusPdfReportRubyGem)
|
|
310
|
+
- [RubyGems.org](https://rubygems.org/gems/hybiscus_pdf_report)
|
|
93
311
|
|
|
94
312
|
|
data/hybiscus_pdf_report.gemspec
CHANGED
|
@@ -23,7 +23,8 @@ Gem::Specification.new do |spec|
|
|
|
23
23
|
spec.files = Dir.chdir(__dir__) do
|
|
24
24
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
25
25
|
(File.expand_path(f) == __FILE__) ||
|
|
26
|
-
f.start_with?(*%w[bin/ test/ spec/ features/ .git .gitlab-ci.yml appveyor Gemfile])
|
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .gitlab-ci.yml appveyor Gemfile]) ||
|
|
27
|
+
f.end_with?(".gem")
|
|
27
28
|
end
|
|
28
29
|
end
|
|
29
30
|
spec.bindir = "exe"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# These errors are automatically raised by the client based on HTTP status codes returned
|
|
4
|
+
# by the Hybiscus API. Extend or rescue these as needed in application code.
|
|
5
|
+
|
|
6
|
+
module HybiscusPdfReport
|
|
7
|
+
# rubocop:disable Style/CommentedKeyword
|
|
8
|
+
# Base error class for all Hybiscus PDF Report API errors.
|
|
9
|
+
class ApiError < StandardError
|
|
10
|
+
attr_reader :response, :status_code
|
|
11
|
+
|
|
12
|
+
def initialize(message = nil, response: nil, status_code: nil)
|
|
13
|
+
super(message)
|
|
14
|
+
@response = response
|
|
15
|
+
@status_code = status_code
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class BadRequestError < ApiError; end # 400
|
|
20
|
+
class UnauthorizedError < ApiError; end # 401
|
|
21
|
+
class PaymentRequiredError < ApiError; end # 402
|
|
22
|
+
class ForbiddenError < ApiError; end # 403
|
|
23
|
+
class NotFoundError < ApiError; end # 404
|
|
24
|
+
class UnprocessableContentError < ApiError; end # 422
|
|
25
|
+
class ApiRequestsQuotaReachedError < ApiError; end # 429
|
|
26
|
+
class RateLimitError < ApiError; end # 503
|
|
27
|
+
# rubocop:enable Style/CommentedKeyword
|
|
28
|
+
|
|
29
|
+
HTTP_ERROR_STATUS_CODES = {
|
|
30
|
+
400 => BadRequestError,
|
|
31
|
+
401 => UnauthorizedError,
|
|
32
|
+
402 => PaymentRequiredError,
|
|
33
|
+
403 => ForbiddenError,
|
|
34
|
+
404 => NotFoundError,
|
|
35
|
+
422 => UnprocessableContentError,
|
|
36
|
+
429 => ApiRequestsQuotaReachedError,
|
|
37
|
+
503 => RateLimitError
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
HTTP_OK_CODE = 200
|
|
41
|
+
end
|
|
@@ -1,32 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "faraday"
|
|
4
|
-
require_relative "
|
|
4
|
+
require_relative "api_errors"
|
|
5
|
+
require_relative "config"
|
|
5
6
|
|
|
6
7
|
module HybiscusPdfReport
|
|
7
|
-
#
|
|
8
|
+
# HTTP client for the Hybiscus PDF Reports API.
|
|
9
|
+
#
|
|
10
|
+
# This class handles all HTTP communication with the Hybiscus API, including
|
|
11
|
+
# authentication, connection management, and request routing. It uses Faraday
|
|
12
|
+
# for HTTP requests and supports custom adapters for testing.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# client = HybiscusPdfReport::Client.new(api_key: "your_api_key")
|
|
16
|
+
# response = client.request.build_report(report_data)
|
|
17
|
+
#
|
|
18
|
+
# @example Using environment variables
|
|
19
|
+
# ENV["HYBISCUS_API_KEY"] = "your_api_key"
|
|
20
|
+
# client = HybiscusPdfReport::Client.new
|
|
21
|
+
#
|
|
22
|
+
# @example Custom configuration
|
|
23
|
+
# client = HybiscusPdfReport::Client.new(
|
|
24
|
+
# api_key: "your_key",
|
|
25
|
+
# api_url: "https://api.hybiscus.dev/api/v1/",
|
|
26
|
+
# timeout: 30
|
|
27
|
+
# )
|
|
8
28
|
class Client
|
|
9
|
-
attr_reader :api_key, :
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@hibiskus_api_url = hibiskus_api_url || BASE_URL_API
|
|
23
|
-
# default to the main Adnexo production account
|
|
24
|
-
@timeout = timeout
|
|
29
|
+
attr_reader :api_key, :api_url, :adapter, :last_request
|
|
30
|
+
|
|
31
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
32
|
+
def initialize(api_key: nil, api_url: nil, timeout: nil, adapter: nil, stubs: nil)
|
|
33
|
+
@api_key = (api_key || config.api_key)&.strip
|
|
34
|
+
if @api_key.nil? || @api_key.empty?
|
|
35
|
+
raise ArgumentError,
|
|
36
|
+
"No API key defined. Set it in config or pass to Client.new."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@api_url = api_url || config.api_url
|
|
40
|
+
@timeout = timeout || config.timeout
|
|
41
|
+
|
|
25
42
|
# param made available for testing purposes: In the rspec tests the following adapter is used: :test
|
|
26
43
|
# https://www.rubydoc.info/gems/faraday/Faraday/Adapter/Test
|
|
27
|
-
@adapter
|
|
28
|
-
@stubs
|
|
44
|
+
@adapter = adapter || config.adapter
|
|
45
|
+
@stubs = stubs || config.stubs
|
|
29
46
|
end
|
|
47
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
30
48
|
|
|
31
49
|
def request
|
|
32
50
|
@request ||= Request.new(self)
|
|
@@ -41,16 +59,21 @@ module HybiscusPdfReport
|
|
|
41
59
|
# rubocop:disable Metrics/AbcSize
|
|
42
60
|
def build_connection(header)
|
|
43
61
|
Faraday.new do |conn|
|
|
44
|
-
conn.url_prefix =
|
|
62
|
+
conn.url_prefix = api_url ## typically the base URL
|
|
45
63
|
conn.request :json
|
|
46
64
|
conn.response :json, content_type: "application/json"
|
|
47
65
|
conn.adapter adapter, @stubs
|
|
48
|
-
conn.headers["X-API-KEY"] = api_key
|
|
66
|
+
conn.headers["X-API-KEY"] = api_key unless api_key.nil? || api_key.empty?
|
|
49
67
|
# adds additional header information to the connection
|
|
68
|
+
|
|
50
69
|
header.each { |key, value| conn.headers[key] = value }
|
|
51
70
|
conn.options.timeout = @timeout || 10
|
|
52
71
|
end
|
|
53
72
|
end
|
|
54
73
|
# rubocop:enable Metrics/AbcSize
|
|
74
|
+
|
|
75
|
+
def config
|
|
76
|
+
@config ||= HybiscusPdfReport.config
|
|
77
|
+
end
|
|
55
78
|
end
|
|
56
79
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
|
|
5
|
+
# Main module for the Hybiscus PDF Report gem
|
|
6
|
+
module HybiscusPdfReport
|
|
7
|
+
# Configuration class for the Hybiscus PDF Report gem.
|
|
8
|
+
#
|
|
9
|
+
# This class manages all configuration settings including API credentials,
|
|
10
|
+
# URLs, timeouts, and connection adapters. Configuration can be set through
|
|
11
|
+
# environment variables or programmatically.
|
|
12
|
+
#
|
|
13
|
+
# @example Setting configuration programmatically
|
|
14
|
+
# HybiscusPdfReport.configure do |config|
|
|
15
|
+
# config.api_key = "your_api_key"
|
|
16
|
+
# config.api_url = "https://api.hybiscus.dev/api/v1/"
|
|
17
|
+
# config.timeout = 30
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Using environment variables
|
|
21
|
+
# ENV["HYBISCUS_API_KEY"] = "your_api_key"
|
|
22
|
+
# ENV["HYBISCUS_API_URL"] = "https://api.hybiscus.dev/api/v1/"
|
|
23
|
+
class Config
|
|
24
|
+
attr_accessor :api_key, :api_url, :timeout, :adapter, :stubs
|
|
25
|
+
|
|
26
|
+
DEFAULT_API_URL = "https://api.hybiscus.dev/api/v1/"
|
|
27
|
+
DEFAULT_TIMEOUT = 10
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@api_key = ENV["HYBISCUS_API_KEY"]
|
|
31
|
+
@api_url = ENV["HYBISCUS_API_URL"] || DEFAULT_API_URL
|
|
32
|
+
@timeout = DEFAULT_TIMEOUT
|
|
33
|
+
@adapter = Faraday.default_adapter
|
|
34
|
+
@stubs = nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# yields the global configuration
|
|
39
|
+
def self.configure
|
|
40
|
+
yield(config)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# returns the global config instance
|
|
44
|
+
def self.config
|
|
45
|
+
@config ||= Config.new
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# These errors are automatically raised by the client based on HTTP status codes returned
|
|
4
|
+
# by the Hybiscus API. Extend or rescue these as needed in application code.
|
|
5
|
+
|
|
4
6
|
module HybiscusPdfReport
|
|
5
|
-
ApiError
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
class ApiError < StandardError; end
|
|
8
|
+
|
|
9
|
+
# 400
|
|
10
|
+
class BadRequestError < ApiError; end
|
|
11
|
+
# 401
|
|
12
|
+
class UnauthorizedError < ApiError; end
|
|
13
|
+
# 402
|
|
14
|
+
class PaymentRequiredError < ApiError; end
|
|
15
|
+
# 403
|
|
16
|
+
class ForbiddenError < ApiError; end
|
|
17
|
+
# 404
|
|
18
|
+
class NotFoundError < ApiError; end
|
|
19
|
+
# 422
|
|
20
|
+
class UnprocessableContentError < ApiError; end
|
|
21
|
+
# 429
|
|
22
|
+
class ApiRequestsQuotaReachedError < ApiError; end
|
|
23
|
+
# 503
|
|
24
|
+
class RateLimitError < ApiError; end
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
HTTP_ERROR_STATUS_CODES = {
|
|
27
|
+
400 => BadRequestError,
|
|
28
|
+
401 => UnauthorizedError,
|
|
29
|
+
402 => PaymentRequiredError,
|
|
30
|
+
403 => ForbiddenError,
|
|
31
|
+
404 => NotFoundError,
|
|
32
|
+
422 => UnprocessableContentError,
|
|
33
|
+
429 => ApiRequestsQuotaReachedError,
|
|
34
|
+
503 => RateLimitError
|
|
35
|
+
}.freeze
|
|
16
36
|
|
|
17
|
-
|
|
18
|
-
HTTP_UNAUTHORIZED_CODE = 401
|
|
19
|
-
HTTP_PAYMENT_REQUIRED_CODE = 402
|
|
20
|
-
HTTP_FORBIDDEN_CODE = 403
|
|
21
|
-
HTTP_NOT_FOUND_CODE = 404
|
|
22
|
-
HTTP_UNPROCESSABLE_CONTENT_CODE = 422
|
|
23
|
-
HTTP_UNPROCESSABLE_ENTITY_CODE = 429
|
|
24
|
-
HTTP_SERVICE_UNAVAILABLE_CODE = 503
|
|
37
|
+
HTTP_OK_CODE = 200
|
|
25
38
|
end
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
# Legacy placeholder file for backward compatibility
|
|
4
|
+
# This file exists for historical reasons and may be removed in future versions.
|
|
5
|
+
# Use HybiscusPdfReport::ResponseObject instead.
|
|
5
6
|
module HybiscusPdfReport
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def method_missing(method, *args, &block)
|
|
13
|
-
attribute = @attributes.send(method, *args, &block)
|
|
14
|
-
|
|
15
|
-
return super if attribute.nil?
|
|
16
|
-
return Object.new(attribute) if attribute.is_a?(Hash)
|
|
17
|
-
|
|
18
|
-
attribute
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def respond_to_missing?(method, _include_private = false)
|
|
22
|
-
@attributes.respond_to? method
|
|
23
|
-
end
|
|
7
|
+
# Placeholder for legacy compatibility
|
|
8
|
+
# @deprecated Use ResponseObject instead
|
|
9
|
+
module Object
|
|
10
|
+
# This module is deprecated
|
|
24
11
|
end
|
|
25
12
|
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Response wrapper class for Hybiscus PDF Report API responses
|
|
4
|
+
require_relative "../response_object"
|
|
5
|
+
|
|
3
6
|
module HybiscusPdfReport
|
|
4
|
-
|
|
7
|
+
# Standard response object that inherits from ResponseObject
|
|
8
|
+
class Response < HybiscusPdfReport::ResponseObject
|
|
5
9
|
end
|
|
6
10
|
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module HybiscusPdfReport
|
|
7
|
+
# Base class for building PDF reports with JSON templates
|
|
8
|
+
#
|
|
9
|
+
# This class allows users to create custom report builders by inheriting from it.
|
|
10
|
+
# It provides a simple way to generate JSON structures for the Hybiscus API using ERB templates.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# class InvoiceReport < HybiscusPdfReport::ReportBuilder
|
|
14
|
+
# def initialize(invoice:, **options)
|
|
15
|
+
# @invoice = invoice
|
|
16
|
+
# super(report_name: "Invoice Report", **options)
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# report = InvoiceReport.new(invoice: my_invoice)
|
|
21
|
+
# json_data = report.generate
|
|
22
|
+
class ReportBuilder
|
|
23
|
+
DEFAULT_TEMPLATE_DIR = File.dirname(__FILE__)
|
|
24
|
+
|
|
25
|
+
attr_reader :report_name, :template_dir
|
|
26
|
+
|
|
27
|
+
def initialize(report_name: nil, template_dir: nil, **template_params)
|
|
28
|
+
# Set the report name - use provided name or derive from class name
|
|
29
|
+
@report_name = report_name || derive_report_name
|
|
30
|
+
@template_dir = template_dir || DEFAULT_TEMPLATE_DIR
|
|
31
|
+
|
|
32
|
+
# Dynamically set all parameters as instance variables
|
|
33
|
+
template_params.each do |key, value|
|
|
34
|
+
instance_variable_set("@#{key}", value)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Main method to generate the report JSON
|
|
39
|
+
# Returns a JSON string that can be sent to the Hybiscus API
|
|
40
|
+
def generate
|
|
41
|
+
render_json
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns the full path to the template file
|
|
45
|
+
def template_path
|
|
46
|
+
File.join(template_dir, template_name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns the template filename (can be overridden in subclasses)
|
|
50
|
+
def template_name
|
|
51
|
+
"#{underscore(class_name)}.json.erb"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def load_configuration_first?
|
|
55
|
+
# By default, we assume the report doesn't require pre-loading configuration.
|
|
56
|
+
# This can be overridden in subclasses if configuration loading is needed.
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Renders the ERB template with all instance variables available
|
|
63
|
+
def render_json
|
|
64
|
+
unless File.exist?(template_path)
|
|
65
|
+
raise "Template file not found: #{template_path}. " \
|
|
66
|
+
"Create a template file or override the render_json method."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
template_content = File.read(template_path)
|
|
70
|
+
template = ERB.new(template_content)
|
|
71
|
+
|
|
72
|
+
# Render the template with all instance variables in scope
|
|
73
|
+
template.result(binding)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Derives a report name from the class name
|
|
77
|
+
def derive_report_name
|
|
78
|
+
humanize(class_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns the class name without module prefix
|
|
82
|
+
def class_name
|
|
83
|
+
self.class.name.split("::").last
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Converts CamelCase to snake_case
|
|
87
|
+
def underscore(string)
|
|
88
|
+
string
|
|
89
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
90
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
91
|
+
.downcase
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Converts CamelCase to human readable format
|
|
95
|
+
def humanize(string)
|
|
96
|
+
underscore(string).tr("_", " ").split.map(&:capitalize).join(" ")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "base64"
|
|
5
|
+
require "pry"
|
|
5
6
|
|
|
6
7
|
module HybiscusPdfReport
|
|
7
8
|
# Request Handler with the individual endpoints handling the communication with the Hybiscus PDF Reports API
|
|
8
9
|
class Request
|
|
9
|
-
attr_reader :client, :response, :last_request_time_counting_against_rate_limit, :last_task_id, :last_task_status
|
|
10
|
-
:remaining_single_page_reports, :remaining_multi_page_reports
|
|
10
|
+
attr_reader :client, :response, :last_request_time_counting_against_rate_limit, :last_task_id, :last_task_status
|
|
11
11
|
|
|
12
12
|
def initialize(client)
|
|
13
13
|
@client = client
|
|
@@ -20,21 +20,15 @@ module HybiscusPdfReport
|
|
|
20
20
|
def build_report(report_request_as_json)
|
|
21
21
|
response_body = request(endpoint: "build-report", http_method: :post, body: report_request_as_json)
|
|
22
22
|
## HANDLE 402 RESPONSE --> PAYMENT REQUIRED
|
|
23
|
-
update_last_request_information
|
|
23
|
+
update_last_request_information(response_body)
|
|
24
24
|
|
|
25
|
-
response = Response.new(response_body
|
|
26
|
-
remaining_single_page_reports: @response.headers["x-remaining-single-page-reports"],
|
|
27
|
-
remaining_multi_page_reports: @response.headers["x-remaining-multi-page-reports"]
|
|
28
|
-
))
|
|
29
|
-
|
|
30
|
-
update_quota_information(response)
|
|
31
|
-
response
|
|
25
|
+
@response = Response.new(response_body)
|
|
32
26
|
end
|
|
33
27
|
|
|
34
28
|
# POST
|
|
35
29
|
def preview_report(report_request_as_json)
|
|
36
30
|
response_body = request(endpoint: "preview-report", http_method: :post, body: report_request_as_json)
|
|
37
|
-
update_last_request_information
|
|
31
|
+
update_last_request_information(response_body)
|
|
38
32
|
Response.new(response_body)
|
|
39
33
|
end
|
|
40
34
|
|
|
@@ -43,7 +37,7 @@ module HybiscusPdfReport
|
|
|
43
37
|
response_body = request(endpoint: "get-task-status", params: { task_id: task_id })
|
|
44
38
|
# The last task status is stored. If this method is called with the same task_id, the last task status is updated
|
|
45
39
|
# in the instance variable
|
|
46
|
-
@last_task_status =
|
|
40
|
+
@last_task_status = response_body["status"] if last_task_id == task_id
|
|
47
41
|
Response.new(response_body)
|
|
48
42
|
end
|
|
49
43
|
|
|
@@ -55,7 +49,7 @@ module HybiscusPdfReport
|
|
|
55
49
|
|
|
56
50
|
def get_report(task_id)
|
|
57
51
|
response_body = request(endpoint: "get-report", http_method: :get, params: { task_id: task_id })
|
|
58
|
-
Response.new(
|
|
52
|
+
Response.new(report: Base64.encode64(response_body), status: HTTP_OK_CODE)
|
|
59
53
|
end
|
|
60
54
|
|
|
61
55
|
def get_last_report
|
|
@@ -76,60 +70,43 @@ module HybiscusPdfReport
|
|
|
76
70
|
def request(endpoint:, http_method: :get, headers: {}, params: {}, body: {})
|
|
77
71
|
raise "Client not defined" unless defined? @client
|
|
78
72
|
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
retry_wrapper = RequestRetryWrapper.new(logger: defined?(Rails) ? Rails.logger : nil)
|
|
74
|
+
|
|
75
|
+
response_body = retry_wrapper.with_retries do
|
|
76
|
+
@response = client.connection.public_send(http_method, endpoint, params.merge(body), headers)
|
|
77
|
+
raise_error unless response_successful? && no_json_error?
|
|
78
|
+
|
|
79
|
+
# Return raw body for binary data (no JSON parsing)
|
|
80
|
+
@response.body
|
|
81
|
+
end
|
|
81
82
|
|
|
82
83
|
@last_request = Time.now
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
response_body
|
|
85
86
|
end
|
|
86
87
|
|
|
87
|
-
def update_last_request_information
|
|
88
|
+
def update_last_request_information(response_body)
|
|
88
89
|
@last_request_time_counting_against_rate_limit = Time.now
|
|
89
|
-
@last_task_id =
|
|
90
|
-
@last_task_status =
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def update_quota_information(response_object)
|
|
94
|
-
@remaining_single_page_reports = response_object.remaining_single_page_reports
|
|
95
|
-
@remaining_multi_page_reports = response_object.remaining_multi_page_reports
|
|
90
|
+
@last_task_id = response_body["task_id"]
|
|
91
|
+
@last_task_status = response_body["status"]
|
|
96
92
|
end
|
|
97
93
|
|
|
98
94
|
def raise_error
|
|
99
|
-
|
|
95
|
+
# logger.debug response.body
|
|
100
96
|
|
|
101
97
|
raise error_class(response.status), "Code: #{response.status}, response: #{response.reason_phrase}"
|
|
102
98
|
end
|
|
103
99
|
|
|
104
|
-
# rubocop: disable Metrics/Metrics/MethodLength
|
|
105
100
|
def error_class(status)
|
|
106
|
-
|
|
107
|
-
when HTTP_BAD_REQUEST_CODE
|
|
108
|
-
BadRequestError
|
|
109
|
-
when HTTP_UNAUTHORIZED_CODE
|
|
110
|
-
UnauthorizedError
|
|
111
|
-
when HTTP_NOT_FOUND_CODE, HTTP_FORBIDDEN_CODE
|
|
112
|
-
NotFoundError
|
|
113
|
-
when HTTP_UNPROCESSABLE_ENTITY_CODE
|
|
114
|
-
UnprocessableEntityError
|
|
115
|
-
when HTTP_PAYMENT_REQUIRED_CODE
|
|
116
|
-
PaymentRequiredError
|
|
117
|
-
when HTTP_SERVICE_UNAVAILABLE_CODE
|
|
118
|
-
# Hybiscus API returns 503 when the rate limit is reached
|
|
119
|
-
# https://hybiscus.dev/docs/api/rate-limitting
|
|
120
|
-
RateLimitError
|
|
121
|
-
else
|
|
122
|
-
ApiError
|
|
123
|
-
end
|
|
101
|
+
HybiscusPdfReport::HTTP_ERROR_STATUS_CODES[status] || ApiError
|
|
124
102
|
end
|
|
125
|
-
# rubocop: enable Metrics/Metrics/MethodLength
|
|
126
103
|
|
|
127
104
|
def response_successful?
|
|
128
105
|
response.status == HTTP_OK_CODE
|
|
129
106
|
end
|
|
130
107
|
|
|
131
108
|
def no_json_error?
|
|
132
|
-
response.status !=
|
|
109
|
+
response.status != HybiscusPdfReport::UnprocessableContentError
|
|
133
110
|
end
|
|
134
111
|
|
|
135
112
|
def pretty_print_json_response
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require_relative "api_errors"
|
|
5
|
+
|
|
6
|
+
module HybiscusPdfReport
|
|
7
|
+
# RequestRetryWrapper provides automatic retry logic for transient errors
|
|
8
|
+
# when communicating with the Hybiscus API (e.g., rate limits, timeouts).
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# wrapper = HybiscusPdfReport::RequestRetryWrapper.new(max_attempts: 3)
|
|
12
|
+
# wrapper.with_retries do
|
|
13
|
+
# api_client.perform_request
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Retries will apply exponential backoff (1s, 2s, 4s, etc.)
|
|
17
|
+
# and will log retry attempts if a logger is provided.
|
|
18
|
+
class RequestRetryWrapper
|
|
19
|
+
DEFAULT_RETRY_ERRORS = [
|
|
20
|
+
RateLimitError,
|
|
21
|
+
Faraday::TimeoutError,
|
|
22
|
+
Faraday::ConnectionFailed
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def initialize(max_attempts: 5, base_delay: 1, logger: nil)
|
|
26
|
+
@max_attempts = max_attempts
|
|
27
|
+
@base_delay = base_delay
|
|
28
|
+
@logger = logger
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def with_retries
|
|
32
|
+
attempts = 0
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
yield
|
|
36
|
+
rescue *DEFAULT_RETRY_ERRORS => e
|
|
37
|
+
attempts += 1
|
|
38
|
+
handle_retry_or_raise(e, attempts)
|
|
39
|
+
retry
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def handle_retry_or_raise(error, attempts)
|
|
46
|
+
if attempts >= @max_attempts
|
|
47
|
+
log_retry_exhausted(error, attempts)
|
|
48
|
+
raise error
|
|
49
|
+
else
|
|
50
|
+
wait_time = compute_delay(attempts)
|
|
51
|
+
log_retry_attempt(error, attempts, wait_time)
|
|
52
|
+
sleep(wait_time)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def compute_delay(attempts)
|
|
57
|
+
@base_delay * (2**(attempts - 1))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def log_retry_attempt(error, attempts, wait_time)
|
|
61
|
+
log("Retry ##{attempts} in #{wait_time}s due to #{error.class}: #{error.message}")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def log_retry_exhausted(error, attempts)
|
|
65
|
+
log("Retries exhausted after #{attempts} attempts: #{error.class} - #{error.message}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def log(message)
|
|
69
|
+
@logger&.warn(message) || puts("[HybiscusRetry] #{message}")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
|
|
5
|
+
module HybiscusPdfReport
|
|
6
|
+
# Base class for all objects returned by the Hybiscus PDF Reports API
|
|
7
|
+
class ResponseObject
|
|
8
|
+
def initialize(attributes)
|
|
9
|
+
@original_attributes = attributes || {}
|
|
10
|
+
@attributes = OpenStruct.new(@original_attributes)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Delegate certain OpenStruct methods directly
|
|
14
|
+
def to_h
|
|
15
|
+
@attributes.to_h
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_s
|
|
19
|
+
@attributes.to_s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def inspect
|
|
23
|
+
@attributes.inspect
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def method_missing(method, *args, &block)
|
|
27
|
+
# Handle attribute access
|
|
28
|
+
if @attributes.respond_to?(method)
|
|
29
|
+
attribute = @attributes.send(method, *args, &block)
|
|
30
|
+
return attribute.is_a?(Hash) ? ResponseObject.new(attribute) : attribute
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Try to access as OpenStruct attribute (this will return nil for non-existent attributes)
|
|
34
|
+
begin
|
|
35
|
+
attribute = @attributes.public_send(method, *args, &block)
|
|
36
|
+
attribute.is_a?(Hash) ? ResponseObject.new(attribute) : attribute
|
|
37
|
+
rescue NoMethodError
|
|
38
|
+
# If OpenStruct doesn't recognize it, delegate to super
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def respond_to_missing?(method, include_private = false)
|
|
44
|
+
@attributes.respond_to?(method) || super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/hybiscus_pdf_report.rb
CHANGED
|
@@ -4,8 +4,30 @@ require_relative "hybiscus_pdf_report/version"
|
|
|
4
4
|
|
|
5
5
|
# Service to interact with the Hybiscus PDF Reports API
|
|
6
6
|
module HybiscusPdfReport
|
|
7
|
+
# Core functionality
|
|
7
8
|
autoload :Client, "hybiscus_pdf_report/client"
|
|
8
|
-
autoload :
|
|
9
|
-
autoload :Object, "hybiscus_pdf_report/object"
|
|
9
|
+
autoload :Config, "hybiscus_pdf_report/config"
|
|
10
10
|
autoload :Response, "hybiscus_pdf_report/objects/response"
|
|
11
|
+
|
|
12
|
+
# Request handling and retries
|
|
13
|
+
autoload :Request, "hybiscus_pdf_report/request"
|
|
14
|
+
autoload :RequestRetryWrapper, "hybiscus_pdf_report/request_retry_wrapper"
|
|
15
|
+
|
|
16
|
+
# Object handling
|
|
17
|
+
autoload :ResponseObject, "hybiscus_pdf_report/response_object"
|
|
18
|
+
|
|
19
|
+
# Error handling
|
|
20
|
+
autoload :APIErrors, "hybiscus_pdf_report/api_errors"
|
|
21
|
+
|
|
22
|
+
# Report building
|
|
23
|
+
autoload :ReportBuilder, "hybiscus_pdf_report/report_builder"
|
|
24
|
+
|
|
25
|
+
# Module-level configuration
|
|
26
|
+
def self.config
|
|
27
|
+
@config ||= Config.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.configure
|
|
31
|
+
yield(config) if block_given?
|
|
32
|
+
end
|
|
11
33
|
end
|
data/sig/hybiscus_pdf_report.rbs
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hybiscus_pdf_report
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.9.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philipp Baumann
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2025-07-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -68,11 +68,16 @@ files:
|
|
|
68
68
|
- Rakefile
|
|
69
69
|
- hybiscus_pdf_report.gemspec
|
|
70
70
|
- lib/hybiscus_pdf_report.rb
|
|
71
|
+
- lib/hybiscus_pdf_report/api_errors.rb
|
|
71
72
|
- lib/hybiscus_pdf_report/client.rb
|
|
73
|
+
- lib/hybiscus_pdf_report/config.rb
|
|
72
74
|
- lib/hybiscus_pdf_report/errors.rb
|
|
73
75
|
- lib/hybiscus_pdf_report/object.rb
|
|
74
76
|
- lib/hybiscus_pdf_report/objects/response.rb
|
|
77
|
+
- lib/hybiscus_pdf_report/report_builder.rb
|
|
75
78
|
- lib/hybiscus_pdf_report/request.rb
|
|
79
|
+
- lib/hybiscus_pdf_report/request_retry_wrapper.rb
|
|
80
|
+
- lib/hybiscus_pdf_report/response_object.rb
|
|
76
81
|
- lib/hybiscus_pdf_report/version.rb
|
|
77
82
|
- sig/hybiscus_pdf_report.rbs
|
|
78
83
|
homepage: https://hybiscus.dev/
|