lutaml-hal 0.1.8 → 0.1.10

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: 754c6e7a5a61e4b96398c511a8e5f0e7dd721dab154272b5de4ce580fc204f71
4
- data.tar.gz: fa3c07d2deed3ddf2605da05a375412eb2222273c46c7f3338982b5b99e1fefb
3
+ metadata.gz: 4f3ce9deb2f8607008c4b5d6950dd281a79bcba5856b03769c0a764dd0c95228
4
+ data.tar.gz: 0d8bd9e4a6f78dd18ce1c58288276d8e0334a48e4858fcbbfb694fd68dc5deb1
5
5
  SHA512:
6
- metadata.gz: 6aff2792a51f0467f5dd470de4656f8630046bbbe741120dbf54b75cf3120b57c8771a1e300b2790be2cc5bca53beebdda86a28085c3b2e8025c993d49ffc3ea
7
- data.tar.gz: 62886f1267ac6119302234a750258882d9a4e4fce0ad24ad9b6c1b2a7ea89876df8cffe33def528aed4a95d33a69f9e0157f8b84c45c23eff36b5f4a47942a01
6
+ metadata.gz: d29f19722b4aa33f3404cd084394573cfa7dc5af3811fb0d42a5cd78319de4e1ff8e8e71b6de59eb23441b39ff0588a36c02ae35156eee99aa845e90929691b6
7
+ data.tar.gz: 4d00fc04f030bda2eec1558ea616f1c3533b8e7109c3224678a96de41b374f4ddadae7a9cf2fd6cbe807633e77a185e8ab893e944b98dc4433898b1cbb1571cc
data/README.adoc CHANGED
@@ -31,6 +31,7 @@ APIs.
31
31
  * Integration with the `lutaml-model` serialization framework
32
32
  * Error handling and response validation for API interactions
33
33
  * Comprehensive embed support for reducing HTTP requests and improving performance
34
+ * Built-in rate limiting with exponential backoff and retry logic
34
35
 
35
36
  == Installation
36
37
 
@@ -103,30 +104,36 @@ puts category.name
103
104
 
104
105
  == Documentation
105
106
 
106
- === Comprehensive guides
107
+ === Detailed topics
107
108
 
108
- For detailed documentation, see these comprehensive guides:
109
+ For detailed documentation, see these topics:
109
110
 
110
- * **link:docs/getting-started-guide.adoc[Getting started guide]** - Step-by-step
111
- tutorial for your first HAL API client (15 minutes)
111
+ link:docs/getting-started.adoc[Getting started]::
112
+ Get started with your HAL API client (15 minutes)
112
113
 
113
- * **link:docs/data-definition-guide.adoc[Data definition guide]** - Complete
114
- reference for defining HAL resources, model registers, and API endpoints
114
+ link:docs/data-definition.adoc[Data definition]::
115
+ Full reference to define HAL resources, model registers, and API endpoints
115
116
 
116
- * **link:docs/runtime-usage-guide.adoc[Runtime usage guide]** - Patterns for
117
- fetching resources, navigating links, and handling pagination
117
+ link:docs/runtime-usage.adoc[Runtime usage]::
118
+ Patterns to fetch resources, navigate links, and handle pagination
118
119
 
119
- * **link:docs/hal-links-reference.adoc[HAL links reference]** - Advanced
120
- configuration and customization of HAL links
120
+ link:docs/hal-links-reference.adoc[HAL links reference]::
121
+ Advanced configuration of HAL links
121
122
 
122
- * **link:docs/pagination-guide.adoc[Pagination guide]** - Working with
123
- paginated APIs and large datasets
123
+ link:docs/pagination.adoc[Pagination]::
124
+ Working with paginated APIs and large datasets
124
125
 
125
- * **link:docs/complex-path-patterns.adoc[Complex path patterns]** - Advanced
126
- URL patterns and path matching examples
126
+ link:docs/complex-path-patterns.adoc[Complex path patterns]::
127
+ Advanced URL patterns and path matching examples
127
128
 
128
- * **link:docs/embedded-resources.adoc[Embedded resources]** - Implementing and
129
- using embedded resources in HAL APIs
129
+ link:docs/embedded-resources.adoc[Embedded resources]::
130
+ Implementing and using embedded resources in HAL APIs with automatic link realization
131
+
132
+ link:docs/rate-limiting.adoc[Rate limiting]::
133
+ Configuring and using built-in rate limiting functionality
134
+
135
+ link:docs/error-handling.adoc[Error handling]::
136
+ Understanding and handling different types of errors when working with HAL APIs
130
137
 
131
138
 
132
139
  === Architecture overview
@@ -213,20 +220,6 @@ register.add_endpoint(
213
220
  For complex path pattern examples, see
214
221
  link:docs/complex-path-patterns.adoc[Complex Path Patterns].
215
222
 
216
- == Error handling
217
-
218
- The library provides structured error handling:
219
-
220
- [source,ruby]
221
- ----
222
- begin
223
- product = register.fetch(:product_resource, id: '123')
224
- rescue Lutaml::Hal::Errors::NotFoundError => e
225
- puts "Product not found: #{e.message}"
226
- rescue Lutaml::Hal::Errors::ApiError => e
227
- puts "API Error: #{e.message}"
228
- end
229
- ----
230
223
 
231
224
  == Contributing
232
225
 
@@ -5,12 +5,13 @@ require 'faraday/follow_redirects'
5
5
  require 'json'
6
6
  require 'rainbow'
7
7
  require_relative 'errors'
8
+ require_relative 'rate_limiter'
8
9
 
9
10
  module Lutaml
10
11
  module Hal
11
12
  # HAL Client for making HTTP requests to HAL APIs
12
13
  class Client
13
- attr_reader :last_response, :api_url, :connection
14
+ attr_reader :last_response, :api_url, :connection, :rate_limiter
14
15
 
15
16
  def initialize(options = {})
16
17
  @api_url = options[:api_url] || raise(ArgumentError, 'api_url is required')
@@ -19,6 +20,7 @@ module Lutaml
19
20
  @debug = options[:debug] || !ENV['DEBUG_API'].nil?
20
21
  @cache = options[:cache] || {}
21
22
  @cache_enabled = options[:cache_enabled] || false
23
+ @rate_limiter = options[:rate_limiter] || RateLimiter.new(options[:rate_limiting] || {})
22
24
 
23
25
  @api_url = strip_api_url(@api_url)
24
26
  end
@@ -41,20 +43,21 @@ module Lutaml
41
43
 
42
44
  return @cache[cache_key] if @cache_enabled && @cache.key?(cache_key)
43
45
 
44
- @last_response = @connection.get(url, params)
45
-
46
- response = handle_response(@last_response, url)
46
+ response = @rate_limiter.with_rate_limiting do
47
+ @last_response = @connection.get(url, params)
48
+ handle_response(@last_response, url)
49
+ end
47
50
 
48
51
  @cache[cache_key] = response if @cache_enabled
49
52
  response
50
53
  rescue Faraday::ConnectionFailed => e
51
- raise ConnectionError, "Connection failed: #{e.message}"
54
+ raise Lutaml::Hal::ConnectionError, "Connection failed: #{e.message}"
52
55
  rescue Faraday::TimeoutError => e
53
- raise TimeoutError, "Request timed out: #{e.message}"
56
+ raise Lutaml::Hal::TimeoutError, "Request timed out: #{e.message}"
54
57
  rescue Faraday::ParsingError => e
55
- raise ParsingError, "Response parsing error: #{e.message}"
58
+ raise Lutaml::Hal::ParsingError, "Response parsing error: #{e.message}"
56
59
  rescue Faraday::Adapter::Test::Stubs::NotFound => e
57
- raise LinkResolutionError, "Resource not found: #{e.message}"
60
+ raise Lutaml::Hal::LinkResolutionError, "Resource not found: #{e.message}"
58
61
  end
59
62
 
60
63
  # Make a GET request with custom headers
@@ -63,22 +66,23 @@ module Lutaml
63
66
 
64
67
  return @cache[cache_key] if @cache_enabled && @cache.key?(cache_key)
65
68
 
66
- @last_response = @connection.get(url) do |req|
67
- headers.each { |key, value| req.headers[key] = value }
69
+ response = @rate_limiter.with_rate_limiting do
70
+ @last_response = @connection.get(url) do |req|
71
+ headers.each { |key, value| req.headers[key] = value }
72
+ end
73
+ handle_response(@last_response, url)
68
74
  end
69
75
 
70
- response = handle_response(@last_response, url)
71
-
72
76
  @cache[cache_key] = response if @cache_enabled
73
77
  response
74
78
  rescue Faraday::ConnectionFailed => e
75
- raise ConnectionError, "Connection failed: #{e.message}"
79
+ raise Lutaml::Hal::ConnectionError, "Connection failed: #{e.message}"
76
80
  rescue Faraday::TimeoutError => e
77
- raise TimeoutError, "Request timed out: #{e.message}"
81
+ raise Lutaml::Hal::TimeoutError, "Request timed out: #{e.message}"
78
82
  rescue Faraday::ParsingError => e
79
- raise ParsingError, "Response parsing error: #{e.message}"
83
+ raise Lutaml::Hal::ParsingError, "Response parsing error: #{e.message}"
80
84
  rescue Faraday::Adapter::Test::Stubs::NotFound => e
81
- raise LinkResolutionError, "Resource not found: #{e.message}"
85
+ raise Lutaml::Hal::LinkResolutionError, "Resource not found: #{e.message}"
82
86
  end
83
87
 
84
88
  private
@@ -104,8 +108,16 @@ module Lutaml
104
108
  raise UnauthorizedError, response_message(response)
105
109
  when 404
106
110
  raise NotFoundError, response_message(response)
111
+ when 429
112
+ TooManyRequestsError.new(response_message(response)).tap do |error|
113
+ error.define_singleton_method(:response) { { status: response.status, headers: response.headers } }
114
+ raise error
115
+ end
107
116
  when 500..599
108
- raise ServerError, response_message(response)
117
+ ServerError.new(response_message(response)).tap do |error|
118
+ error.define_singleton_method(:response) { { status: response.status, headers: response.headers } }
119
+ raise error
120
+ end
109
121
  else
110
122
  raise Error, response_message(response)
111
123
  end
@@ -9,5 +9,8 @@ module Lutaml
9
9
  class ServerError < Error; end
10
10
  class LinkResolutionError < Error; end
11
11
  class ParsingError < Error; end
12
+ class ConnectionError < Error; end
13
+ class TimeoutError < Error; end
14
+ class TooManyRequestsError < Error; end
12
15
  end
13
16
  end
@@ -11,6 +11,9 @@ module Lutaml
11
11
  # will be used to resolve unless overriden in resource#realize()
12
12
  attr_accessor Hal::REGISTER_ID_ATTR_NAME.to_sym
13
13
 
14
+ # Store reference to parent resource for automatic embedded content detection
15
+ attr_accessor :parent_resource
16
+
14
17
  attribute :href, :string
15
18
  attribute :title, :string
16
19
  attribute :name, :string
@@ -25,8 +28,11 @@ module Lutaml
25
28
  # If the Link does not have a register, a register needs to be provided explicitly
26
29
  # via the `register:` parameter.
27
30
  def realize(register: nil, parent_resource: nil)
31
+ # Use provided parent_resource or fall back to stored parent_resource
32
+ effective_parent = parent_resource || @parent_resource
33
+
28
34
  # First check if embedded content is available
29
- if parent_resource && (embedded_content = check_embedded_content(parent_resource, register))
35
+ if effective_parent && (embedded_content = check_embedded_content(effective_parent, register))
30
36
  return embedded_content
31
37
  end
32
38
 
@@ -134,7 +134,12 @@ module Lutaml
134
134
 
135
135
  # Handle both array and single values with the same logic
136
136
  values = value.is_a?(Array) ? value : [value]
137
- values.each { |item| mark_model_links_with_register(item) }
137
+ values.each do |item|
138
+ mark_model_links_with_register(item)
139
+
140
+ # If this is a Link, set the parent resource for automatic embedded content detection
141
+ item.parent_resource = inspecting_model if item.is_a?(Lutaml::Hal::Link)
142
+ end
138
143
  end
139
144
 
140
145
  inspecting_model
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Hal
5
+ # Rate limiter to handle API rate limiting with exponential backoff
6
+ class RateLimiter
7
+ DEFAULT_MAX_RETRIES = 3
8
+ DEFAULT_BASE_DELAY = 1.0
9
+ DEFAULT_MAX_DELAY = 60.0
10
+ DEFAULT_BACKOFF_FACTOR = 2.0
11
+
12
+ attr_reader :max_retries, :base_delay, :max_delay, :backoff_factor
13
+
14
+ def initialize(options = {})
15
+ @max_retries = options[:max_retries] || DEFAULT_MAX_RETRIES
16
+ @base_delay = options[:base_delay] || DEFAULT_BASE_DELAY
17
+ @max_delay = options[:max_delay] || DEFAULT_MAX_DELAY
18
+ @backoff_factor = options[:backoff_factor] || DEFAULT_BACKOFF_FACTOR
19
+ @enabled = options[:enabled] != false # Default to enabled unless explicitly disabled
20
+ end
21
+
22
+ # Execute a block with rate limiting and retry logic
23
+ def with_rate_limiting
24
+ return yield unless @enabled
25
+
26
+ attempt = 0
27
+ begin
28
+ attempt += 1
29
+ yield
30
+ rescue TooManyRequestsError, ServerError => e
31
+ raise unless should_retry?(e, attempt)
32
+
33
+ delay = calculate_delay(attempt, e)
34
+ sleep(delay)
35
+ retry
36
+ end
37
+ end
38
+
39
+ # Check if we should retry based on the error and attempt count
40
+ def should_retry?(error, attempt)
41
+ return false if attempt > @max_retries
42
+
43
+ case error
44
+ when TooManyRequestsError
45
+ true
46
+ when ServerError
47
+ # Always retry on server errors
48
+ true
49
+ else
50
+ false
51
+ end
52
+ end
53
+
54
+ # Calculate delay with exponential backoff
55
+ def calculate_delay(attempt, error = nil)
56
+ # Check for Retry-After header if it's a rate limit error
57
+ if error.is_a?(TooManyRequestsError) && error.respond_to?(:response) && error.response
58
+ retry_after = extract_retry_after(error.response)
59
+ return retry_after if retry_after
60
+ end
61
+
62
+ # Exponential backoff: base_delay * (backoff_factor ^ (attempt - 1))
63
+ delay = @base_delay * (@backoff_factor**(attempt - 1))
64
+
65
+ # Cap at max_delay
66
+ [delay, @max_delay].min
67
+ end
68
+
69
+ # Extract Retry-After header value
70
+ def extract_retry_after(response)
71
+ headers = response[:headers] || {}
72
+ retry_after = headers['retry-after'] || headers['Retry-After']
73
+ return nil unless retry_after
74
+
75
+ # Retry-After can be in seconds (integer) or HTTP date
76
+ if retry_after.match?(/^\d+$/)
77
+ retry_after.to_i
78
+ else
79
+ # Parse HTTP date and calculate seconds from now
80
+ begin
81
+ retry_time = Time.parse(retry_after)
82
+ [retry_time - Time.now, 0].max
83
+ rescue ArgumentError
84
+ nil
85
+ end
86
+ end
87
+ end
88
+
89
+ # Enable rate limiting
90
+ def enable!
91
+ @enabled = true
92
+ end
93
+
94
+ # Disable rate limiting
95
+ def disable!
96
+ @enabled = false
97
+ end
98
+
99
+ # Check if rate limiting is enabled
100
+ def enabled?
101
+ @enabled
102
+ end
103
+ end
104
+ end
105
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Hal
5
- VERSION = '0.1.8'
5
+ VERSION = '0.1.10'
6
6
  end
7
7
  end
data/lib/lutaml/hal.rb CHANGED
@@ -15,6 +15,7 @@ end
15
15
 
16
16
  require_relative 'hal/version'
17
17
  require_relative 'hal/errors'
18
+ require_relative 'hal/rate_limiter'
18
19
  require_relative 'hal/endpoint_parameter'
19
20
  require_relative 'hal/link'
20
21
  require_relative 'hal/link_set'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-hal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-08 00:00:00.000000000 Z
11
+ date: 2025-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -89,6 +89,7 @@ files:
89
89
  - lib/lutaml/hal/link_set_class_factory.rb
90
90
  - lib/lutaml/hal/model_register.rb
91
91
  - lib/lutaml/hal/page.rb
92
+ - lib/lutaml/hal/rate_limiter.rb
92
93
  - lib/lutaml/hal/resource.rb
93
94
  - lib/lutaml/hal/type_resolver.rb
94
95
  - lib/lutaml/hal/version.rb