hybiscus_pdf_report 0.1.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8661a6c187761886b7a0e41f3112957a31eff162cc42c3167bfeb2be793dcc4
4
- data.tar.gz: 36465eed4413bcaa80b5d05893cd24cdeea139c1d1795b7526dd03c9d11b6de4
3
+ metadata.gz: 688afeb6ab9c2d4cd50f375c6b3e3817d50f917ba2d489b7860f4afd5bbc1fb5
4
+ data.tar.gz: 57da67ca7e60bbcda279c5c9b23672d230e1116e34acf10a49c260df3edbbb66
5
5
  SHA512:
6
- metadata.gz: '0463680072442b123263ea823606daae30a64489babe71db6078731645a42d12847df20f390d16ddc09becc8400b48490d009ccf2208e5faaf9c108e52de7a64'
7
- data.tar.gz: 690d63a2dd4e4414fc20e526b260c284560d393a33d5c6038f1f708812bddb24ac823c07710a315abc714b9049b7a09d07c2b57bb688cdd4c63b1eedc70fed97
6
+ metadata.gz: 113df65983859e7e7cbb6dca38fe6abf720dd25522c1a1e45ccaf0ffbc5f56a2b78a64eaa29ab1a2b709327b7a707bd0ed88ed967b1837e949fe00d743cabb1c
7
+ data.tar.gz: 5aac878fc6d8ea65f6621e752b3f77edec8fb0ded23e8410473505070c2697212b3cc8836c69f60d3355ff041b875f61c795e396b8b65ab57a0455eee2b18cbf
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
 
Binary file
@@ -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,31 @@
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
8
  # Client handling the Faraday connection to the Hybiscus PDF Reports API
8
9
  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
10
+ attr_reader :api_key, :api_url, :adapter, :last_request
11
+
12
+ # rubocop:disable Metrics/CyclomaticComplexity)
13
+ def initialize(api_key: nil, api_url: nil, timeout: nil, adapter: nil, stubs: nil)
14
+ @api_key = (api_key || config.api_key)&.strip
15
+ if @api_key.nil? || @api_key.empty?
16
+ raise ArgumentError,
17
+ "No API key defined. Set it in config or pass to Client.new."
18
+ end
19
+
20
+ @api_url = api_url || config.api_url
21
+ @timeout = timeout || config.timeout
22
+
25
23
  # param made available for testing purposes: In the rspec tests the following adapter is used: :test
26
24
  # https://www.rubydoc.info/gems/faraday/Faraday/Adapter/Test
27
- @adapter = adapter || Faraday.default_adapter
28
- @stubs = stubs
25
+ @adapter = adapter || config.adapter
26
+ @stubs = stubs || config.stubs
29
27
  end
28
+ # rubocop:enable Metrics/CyclomaticComplexity)
30
29
 
31
30
  def request
32
31
  @request ||= Request.new(self)
@@ -41,16 +40,21 @@ module HybiscusPdfReport
41
40
  # rubocop:disable Metrics/AbcSize
42
41
  def build_connection(header)
43
42
  Faraday.new do |conn|
44
- conn.url_prefix = hibiskus_api_url ## typically the base URL
43
+ conn.url_prefix = api_url ## typically the base URL
45
44
  conn.request :json
46
45
  conn.response :json, content_type: "application/json"
47
46
  conn.adapter adapter, @stubs
48
- conn.headers["X-API-KEY"] = api_key.to_s unless api_key.empty?
47
+ conn.headers["X-API-KEY"] = api_key unless api_key.nil? || api_key.empty?
49
48
  # adds additional header information to the connection
49
+
50
50
  header.each { |key, value| conn.headers[key] = value }
51
51
  conn.options.timeout = @timeout || 10
52
52
  end
53
53
  end
54
54
  # rubocop:enable Metrics/AbcSize
55
+
56
+ def config
57
+ @config ||= HybiscusPdfReport.config
58
+ end
55
59
  end
56
60
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ module HybiscusPdfReport
6
+ class Config
7
+ attr_accessor :api_key, :api_url, :timeout, :adapter, :stubs
8
+
9
+ DEFAULT_API_URL = "https://api.hybiscus.dev/api/v1/".freeze
10
+ DEFAULT_TIMEOUT = 10
11
+
12
+ def initialize
13
+ @api_key = ENV["HYBISCUS_API_KEY"]
14
+ @api_url = ENV["HYBISCUS_API_URL"] || DEFAULT_API_URL
15
+ @timeout = DEFAULT_TIMEOUT
16
+ @adapter = Faraday.default_adapter
17
+ @stubs = nil
18
+ end
19
+ end
20
+
21
+ # yields the global configuration
22
+ def self.configure
23
+ yield(config)
24
+ end
25
+
26
+ # returns the global config instance
27
+ def self.config
28
+ @config ||= Config.new
29
+ end
30
+ 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 +0,0 @@
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 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
24
- end
25
- end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../response_object"
3
4
  module HybiscusPdfReport
4
- class Response < Object
5
+ class Response < HybiscusPdfReport::ResponseObject
5
6
  end
6
7
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HybiscusPdfReport
4
+ # Base class for all objects returned by the Hybiscus PDF Reports API
5
+ class ReportBuilder
6
+ DEFAULT_TEMPLATE_DIR = File.dirname(__FILE__)
7
+
8
+ def initialize(report_name: nil, **template_params)
9
+ # Dynamically set all parameters as instance variables
10
+ @report_name = report_name ||  self.name.demodulize.humanize.titleize
11
+
12
+ template_params.each do |key, value|
13
+ instance_variable_set("@#{key}", value)
14
+ end
15
+ end
16
+
17
+ # Main method to generate the report - similar to a controller action
18
+ def generate
19
+ # Render the JSON template with all instance variables available
20
+ render_json
21
+ end
22
+
23
+ def report_name
24
+ @report_name = @report_name.presence(name.demodulize.humanize.titleize)
25
+ end
26
+
27
+ def template_path
28
+ Pathname.new(template_path || DEFAULT_TEMPLATE_DIR)
29
+ end
30
+
31
+ def template_name
32
+ "#{name.demodulize.underscore}.json.erb"
33
+ end
34
+
35
+ def load_configuration_first?
36
+ # By default, we assume the print doesn't have a configuration.
37
+ # This can be overridden in subclasses and will load the configuration first.
38
+ false
39
+ end
40
+
41
+ private
42
+
43
+ def render_json
44
+ # Use ERB to render the JSON template with all instance variables available
45
+ template_path = File.join(File.dirname(__FILE__), json_template)
46
+ template = ERB.new(File.read(template_path))
47
+
48
+ # Render and return the JSON with all instance variables in scope
49
+ template.result(binding)
50
+ end
51
+ end
52
+ 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,50 @@ 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
+ <<<<<<< HEAD
80
+ =======
81
+ # Return raw body for binary data (no JSON parsing)
82
+ >>>>>>> updates
83
+ @response.body
84
+ end
81
85
 
82
86
  @last_request = Time.now
83
87
 
84
- response.body
88
+ response_body
85
89
  end
86
90
 
87
- def update_last_request_information
91
+ def update_last_request_information(response_body)
88
92
  @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
93
+ @last_task_id = response_body["task_id"]
94
+ @last_task_status = response_body["status"]
96
95
  end
97
96
 
98
97
  def raise_error
99
- pretty_print_json_response
98
+ <<<<<<< HEAD
99
+ logger.debug response.body
100
+ =======
101
+ # logger.debug response.body
102
+ >>>>>>> updates
100
103
 
101
104
  raise error_class(response.status), "Code: #{response.status}, response: #{response.reason_phrase}"
102
105
  end
103
106
 
104
- # rubocop: disable Metrics/Metrics/MethodLength
105
107
  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
108
+ HybiscusPdfReport::HTTP_ERROR_STATUS_CODES[status] || ApiError
124
109
  end
125
- # rubocop: enable Metrics/Metrics/MethodLength
126
110
 
127
111
  def response_successful?
128
112
  response.status == HTTP_OK_CODE
129
113
  end
130
114
 
131
115
  def no_json_error?
132
- response.status != HTTP_UNPROCESSABLE_CONTENT_CODE
116
+ response.status != HybiscusPdfReport::UnprocessableContentError
133
117
  end
134
118
 
135
119
  def pretty_print_json_response
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ <<<<<<< HEAD
4
+ =======
5
+ require "faraday"
6
+ require_relative "api_errors"
7
+
8
+ >>>>>>> updates
9
+ module HybiscusPdfReport
10
+ # RequestRetryWrapper provides automatic retry logic for transient errors
11
+ # when communicating with the Hybiscus API (e.g., rate limits, timeouts).
12
+ #
13
+ # Usage:
14
+ # wrapper = HybiscusPdfReport::RequestRetryWrapper.new(max_attempts: 3)
15
+ # wrapper.with_retries do
16
+ # api_client.perform_request
17
+ # end
18
+ #
19
+ # Retries will apply exponential backoff (1s, 2s, 4s, etc.)
20
+ # and will log retry attempts if a logger is provided.
21
+ class RequestRetryWrapper
22
+ DEFAULT_RETRY_ERRORS = [
23
+ RateLimitError,
24
+ Faraday::TimeoutError,
25
+ Faraday::ConnectionFailed
26
+ ].freeze
27
+
28
+ def initialize(max_attempts: 5, base_delay: 1, logger: nil)
29
+ @max_attempts = max_attempts
30
+ @base_delay = base_delay
31
+ @logger = logger
32
+ end
33
+
34
+ def with_retries
35
+ attempts = 0
36
+
37
+ begin
38
+ yield
39
+ rescue *DEFAULT_RETRY_ERRORS => e
40
+ attempts += 1
41
+ handle_retry_or_raise(e, attempts)
42
+ retry
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def handle_retry_or_raise(error, attempts)
49
+ if attempts >= @max_attempts
50
+ log_retry_exhausted(error, attempts)
51
+ raise error
52
+ else
53
+ wait_time = compute_delay(attempts)
54
+ log_retry_attempt(error, attempts, wait_time)
55
+ sleep(wait_time)
56
+ end
57
+ end
58
+
59
+ def compute_delay(attempts)
60
+ @base_delay * (2**(attempts - 1))
61
+ end
62
+
63
+ def log_retry_attempt(error, attempts, wait_time)
64
+ log("Retry ##{attempts} in #{wait_time}s due to #{error.class}: #{error.message}")
65
+ end
66
+
67
+ def log_retry_exhausted(error, attempts)
68
+ log("Retries exhausted after #{attempts} attempts: #{error.class} - #{error.message}")
69
+ end
70
+
71
+ def log(message)
72
+ @logger&.warn(message) || puts("[HybiscusRetry] #{message}")
73
+ end
74
+ end
75
+ 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,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HybiscusPdfReport
4
- VERSION = "0.1.0"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -4,8 +4,27 @@ 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
+ # Module-level configuration
23
+ def self.config
24
+ @config ||= Config.new
25
+ end
26
+
27
+ def self.configure
28
+ yield(config) if block_given?
29
+ end
11
30
  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.0
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
@@ -66,13 +66,19 @@ files:
66
66
  - LICENSE.txt
67
67
  - README.md
68
68
  - Rakefile
69
+ - hybiscus_pdf_report.gem
69
70
  - hybiscus_pdf_report.gemspec
70
71
  - lib/hybiscus_pdf_report.rb
72
+ - lib/hybiscus_pdf_report/api_errors.rb
71
73
  - lib/hybiscus_pdf_report/client.rb
74
+ - lib/hybiscus_pdf_report/config.rb
72
75
  - lib/hybiscus_pdf_report/errors.rb
73
76
  - lib/hybiscus_pdf_report/object.rb
74
77
  - lib/hybiscus_pdf_report/objects/response.rb
78
+ - lib/hybiscus_pdf_report/report_builder.rb
75
79
  - lib/hybiscus_pdf_report/request.rb
80
+ - lib/hybiscus_pdf_report/request_retry_wrapper.rb
81
+ - lib/hybiscus_pdf_report/response_object.rb
76
82
  - lib/hybiscus_pdf_report/version.rb
77
83
  - sig/hybiscus_pdf_report.rbs
78
84
  homepage: https://hybiscus.dev/