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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8661a6c187761886b7a0e41f3112957a31eff162cc42c3167bfeb2be793dcc4
4
- data.tar.gz: 36465eed4413bcaa80b5d05893cd24cdeea139c1d1795b7526dd03c9d11b6de4
3
+ metadata.gz: 0007a7c6d4494de9ca87ab92f259b192ad550d26172d225f5be109823a514298
4
+ data.tar.gz: ccc6a69f6c7d28b34b168a240b059c9e54c51a0802097ff829bb07111909ea53
5
5
  SHA512:
6
- metadata.gz: '0463680072442b123263ea823606daae30a64489babe71db6078731645a42d12847df20f390d16ddc09becc8400b48490d009ccf2208e5faaf9c108e52de7a64'
7
- data.tar.gz: 690d63a2dd4e4414fc20e526b260c284560d393a33d5c6038f1f708812bddb24ac823c07710a315abc714b9049b7a09d07c2b57bb688cdd4c63b1eedc70fed97
6
+ metadata.gz: efbe50eb7882ce18576a146b77593e19364176b819973ded959f57d1f6a279982789839fb2503e34f66ca2022ff2bff5148d5c3c76d7f8ccb249cc68df756a4e
7
+ data.tar.gz: 63c0076efef641022cbf9be95a074bb0903b9386308ab7eb1541b6d2a456854c55831e4e17ab08171cf56fe41e4c33f4aa59605ff36edd937c2437cf5363f879
data/.rubocop.yml CHANGED
@@ -12,4 +12,5 @@ Style/StringLiteralsInInterpolation:
12
12
  Layout/LineLength:
13
13
  Max: 120
14
14
 
15
-
15
+ Metrics/BlockLength:
16
+ AllowedMethods: ['describe', 'context']
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Hybiscus API Ruby Wrapper
1
+ # Hybiscus PDF Report Ruby Gem
2
2
 
3
- This is the API Wrapper for the Hybiscus REST API
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
- $ bundle install
15
+ ```bash
16
+ bundle install
17
+ ```
16
18
 
17
19
  Or install it yourself as:
18
20
 
19
- $ gem install hybiscus_pdf_report
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
- ### Instantiate a client
50
+ ### Creating a Client
51
+
27
52
  ```ruby
28
- # To connect to the default SaaS Instance of Adnexo: https://prod.api.ax-track.ch/api/v1
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
- # The default time out is 10 seconds. To change the value, pass in the parameter
34
- client = HybiscusPdfReport::Client.new(api_key: your_api_key, timeout: 20)
56
+ # Or passing the API key directly
57
+ client = HybiscusPdfReport::Client.new(api_key: "your_api_key_here")
35
58
 
36
- # If you have a Hybiscus in your private cloud and have a different URL, you can pass the URL as a parameter
37
- client = HybiscusPdfReport::Client.new(hibiskus_api_url: #URL#)
38
- # You can also set the URL as ENV["HIBISCUS_API_URL"]
39
- ```
59
+ # With custom timeout
60
+ client = HybiscusPdfReport::Client.new(
61
+ api_key: "your_api_key_here",
62
+ timeout: 30
63
+ )
40
64
 
41
- ## Accessing the Endpoints
42
- ### Submit to 'build-report'
43
- ```ruby
44
- response = client.request.build_report(json)
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
- The Response object is returned, containint the `task_id` AND the task `status`. These information are also stored and can be accessed as follows
71
+
72
+ ### API Endpoints
73
+
74
+ #### 1. Build Report
75
+
76
+ Submit a report request for processing:
77
+
47
78
  ```ruby
48
- response = client.request.last_task_id
49
- response = client.request.last_task_status
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
- ### Submit to 'preview-report'
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(json)
94
+ response = client.request.preview_report(report_data)
95
+ puts response.task_id
96
+ puts response.status
55
97
  ```
56
- ### Submit to 'get-task-status'
98
+
99
+ #### 3. Check Task Status
100
+
101
+ Monitor the status of a report generation task:
102
+
57
103
  ```ruby
58
- response = client.request.get_task_status(task_id)
59
- # if you previously already made a request, you can get the status of the last task directly without having to store and pass the task_id
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
- ### Submit to 'get-report'
112
+ #### 4. Download Report
113
+
114
+ Retrieve the generated PDF report:
115
+
64
116
  ```ruby
65
- response = client.request.get_report(task_id)
66
- # if you previously already made a request, you can get the status of the last task directly without having to store and pass the task_id
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 test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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: _YOUR_API_KEY_)
79
- # to get a list of all trackers (just as an example)
80
- client.trackers.all
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
- ## Open development
92
- * Pagination for development
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
 
@@ -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 "errors"
4
+ require_relative "api_errors"
5
+ require_relative "config"
5
6
 
6
7
  module HybiscusPdfReport
7
- # Client handling the Faraday connection to the Hybiscus PDF Reports API
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, :hibiskus_api_url, :adapter, :last_request, :email
10
-
11
- BASE_URL_API = "https://api.hybiscus.dev/api/v1/"
12
-
13
- def initialize(api_key: ENV["HIBISCUS_API_KEY"],
14
- hibiskus_api_url: ENV["HIBISCUS_API_URL"],
15
- timeout: nil,
16
- adapter: nil,
17
- stubs: nil)
18
- @api_key = api_key&.strip
19
- # default to the main Adnexo production account
20
- raise ArgumentError, "No API key defined. Check documentation on how to set the API key." if @api_key.nil?
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 = adapter || Faraday.default_adapter
28
- @stubs = 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 = hibiskus_api_url ## typically the base URL
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.to_s unless api_key.empty?
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
- # Definition of all errors that can be raised by the Hybiscus PDF Reports API
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 = Class.new(StandardError)
6
- BadRequestError = Class.new(ApiError)
7
- UnauthorizedError = Class.new(ApiError)
8
- PaymentRequiredError = Class.new(ApiError)
9
- ForbiddenError = Class.new(ApiError)
10
- ApiRequestsQuotaReachedError = Class.new(ApiError)
11
- NotFoundError = Class.new(ApiError)
12
- UnprocessableEntityError = Class.new(ApiError)
13
- RateLimitError = Class.new(ApiError)
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
- HTTP_OK_CODE = 200
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
- HTTP_BAD_REQUEST_CODE = 400
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
- require "ostruct"
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
- # Base class for all objects returned by the Hybiscus PDF Reports API
7
- class Object
8
- def initialize(attributes)
9
- @attributes = OpenStruct.new(attributes)
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
- class Response < Object
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.merge(
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 = response.body["status"] if last_task_id == task_id
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(satus: response.status, content: Base64.encode64(response_body))
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
- @response = client.connection.public_send(http_method, endpoint, params.merge(body), headers)
80
- raise_error unless response_successful? && no_json_error?
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
- response.body
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 = response.body["task_id"]
90
- @last_task_status = response.body["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
- pretty_print_json_response
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
- case status
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 != HTTP_UNPROCESSABLE_CONTENT_CODE
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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Version information for the Hybiscus PDF Report gem
3
4
  module HybiscusPdfReport
4
- VERSION = "0.1.0"
5
+ VERSION = "0.9.1"
5
6
  end
@@ -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 :Request, "hybiscus_pdf_report/request"
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
@@ -1,4 +1,4 @@
1
1
  module HybiscusPdfReport
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+ # See the writing guide of rbs: t
4
4
  end
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.0
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: 2024-12-10 00:00:00.000000000 Z
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/