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 +4 -4
- data/README.adoc +23 -30
- data/lib/lutaml/hal/client.rb +29 -17
- data/lib/lutaml/hal/errors.rb +3 -0
- data/lib/lutaml/hal/link.rb +7 -1
- data/lib/lutaml/hal/model_register.rb +6 -1
- data/lib/lutaml/hal/rate_limiter.rb +105 -0
- data/lib/lutaml/hal/version.rb +1 -1
- data/lib/lutaml/hal.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f3ce9deb2f8607008c4b5d6950dd281a79bcba5856b03769c0a764dd0c95228
|
4
|
+
data.tar.gz: 0d8bd9e4a6f78dd18ce1c58288276d8e0334a48e4858fcbbfb694fd68dc5deb1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
===
|
107
|
+
=== Detailed topics
|
107
108
|
|
108
|
-
For detailed documentation, see these
|
109
|
+
For detailed documentation, see these topics:
|
109
110
|
|
110
|
-
|
111
|
-
|
111
|
+
link:docs/getting-started.adoc[Getting started]::
|
112
|
+
Get started with your HAL API client (15 minutes)
|
112
113
|
|
113
|
-
|
114
|
-
|
114
|
+
link:docs/data-definition.adoc[Data definition]::
|
115
|
+
Full reference to define HAL resources, model registers, and API endpoints
|
115
116
|
|
116
|
-
|
117
|
-
|
117
|
+
link:docs/runtime-usage.adoc[Runtime usage]::
|
118
|
+
Patterns to fetch resources, navigate links, and handle pagination
|
118
119
|
|
119
|
-
|
120
|
-
|
120
|
+
link:docs/hal-links-reference.adoc[HAL links reference]::
|
121
|
+
Advanced configuration of HAL links
|
121
122
|
|
122
|
-
|
123
|
-
|
123
|
+
link:docs/pagination.adoc[Pagination]::
|
124
|
+
Working with paginated APIs and large datasets
|
124
125
|
|
125
|
-
|
126
|
-
|
126
|
+
link:docs/complex-path-patterns.adoc[Complex path patterns]::
|
127
|
+
Advanced URL patterns and path matching examples
|
127
128
|
|
128
|
-
|
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
|
|
data/lib/lutaml/hal/client.rb
CHANGED
@@ -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
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
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
|
data/lib/lutaml/hal/errors.rb
CHANGED
data/lib/lutaml/hal/link.rb
CHANGED
@@ -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
|
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
|
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
|
data/lib/lutaml/hal/version.rb
CHANGED
data/lib/lutaml/hal.rb
CHANGED
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.
|
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-
|
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
|