lutaml-hal 0.2.2 → 0.2.4
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/lib/lutaml/hal/cache/cache_configuration.rb +17 -75
- data/lib/lutaml/hal/cache/cache_entry.rb +2 -23
- data/lib/lutaml/hal/cache/cache_manager.rb +17 -225
- data/lib/lutaml/hal/cache/cache_metadata.rb +3 -46
- data/lib/lutaml/hal/cache/response_adapter.rb +33 -0
- data/lib/lutaml/hal/cache.rb +13 -0
- data/lib/lutaml/hal/client.rb +8 -20
- data/lib/lutaml/hal/global_register.rb +2 -13
- data/lib/lutaml/hal/link.rb +4 -32
- data/lib/lutaml/hal/link_class_factory.rb +0 -7
- data/lib/lutaml/hal/link_set.rb +0 -2
- data/lib/lutaml/hal/link_set_class_factory.rb +0 -4
- data/lib/lutaml/hal/model_register.rb +17 -79
- data/lib/lutaml/hal/page.rb +1 -59
- data/lib/lutaml/hal/rate_limiter.rb +12 -28
- data/lib/lutaml/hal/resource.rb +3 -44
- data/lib/lutaml/hal/version.rb +1 -1
- data/lib/lutaml/hal.rb +29 -13
- metadata +6 -5
- data/lib/lutaml/hal/cache/simple_cache_store.rb +0 -83
|
@@ -5,7 +5,6 @@ require 'lutaml/model'
|
|
|
5
5
|
module Lutaml
|
|
6
6
|
module Hal
|
|
7
7
|
module Cache
|
|
8
|
-
# Represents HTTP response metadata from the request that created the HAL resource
|
|
9
8
|
class CacheMetadata < Lutaml::Model::Serializable
|
|
10
9
|
attribute :etag, :string
|
|
11
10
|
attribute :last_modified, :string
|
|
@@ -16,23 +15,21 @@ module Lutaml
|
|
|
16
15
|
attribute :date, :string
|
|
17
16
|
attribute :vary, :string
|
|
18
17
|
|
|
19
|
-
# Extract metadata from HTTP response headers
|
|
20
18
|
def self.from_response(response)
|
|
21
|
-
headers =
|
|
19
|
+
headers = ResponseAdapter.headers(response)
|
|
22
20
|
|
|
23
21
|
new(
|
|
24
22
|
etag: headers['etag'],
|
|
25
23
|
last_modified: headers['last-modified'],
|
|
26
24
|
cache_control: headers['cache-control'],
|
|
27
25
|
expires: headers['expires'],
|
|
28
|
-
status_code:
|
|
26
|
+
status_code: ResponseAdapter.status_code(response),
|
|
29
27
|
content_type: headers['content-type'],
|
|
30
28
|
date: headers['date'],
|
|
31
29
|
vary: headers['vary']
|
|
32
30
|
)
|
|
33
31
|
end
|
|
34
32
|
|
|
35
|
-
# Generate conditional request headers for cache validation
|
|
36
33
|
def conditional_headers
|
|
37
34
|
headers = {}
|
|
38
35
|
headers['If-None-Match'] = etag if etag
|
|
@@ -40,7 +37,6 @@ module Lutaml
|
|
|
40
37
|
headers
|
|
41
38
|
end
|
|
42
39
|
|
|
43
|
-
# Check if the metadata indicates the response is cacheable
|
|
44
40
|
def cacheable?
|
|
45
41
|
return false if cache_control&.include?('no-cache')
|
|
46
42
|
return false if cache_control&.include?('no-store')
|
|
@@ -49,7 +45,6 @@ module Lutaml
|
|
|
49
45
|
true
|
|
50
46
|
end
|
|
51
47
|
|
|
52
|
-
# Extract TTL from cache-control header
|
|
53
48
|
def max_age
|
|
54
49
|
return nil unless cache_control
|
|
55
50
|
|
|
@@ -57,46 +52,8 @@ module Lutaml
|
|
|
57
52
|
match ? match[1].to_i : nil
|
|
58
53
|
end
|
|
59
54
|
|
|
60
|
-
# Check if the response can be revalidated with conditional requests
|
|
61
55
|
def revalidatable?
|
|
62
|
-
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def self.extract_headers(response)
|
|
66
|
-
case response
|
|
67
|
-
when Hash
|
|
68
|
-
# Response is already a hash, extract headers directly
|
|
69
|
-
response.select { |k, _| k.is_a?(String) && k.match?(/^[a-z-]+$/) }
|
|
70
|
-
when ->(r) { r.respond_to?(:headers) }
|
|
71
|
-
# Response has headers method
|
|
72
|
-
response.headers.to_h
|
|
73
|
-
when ->(r) { r.respond_to?(:[]) }
|
|
74
|
-
# Response is hash-like, try to extract common headers
|
|
75
|
-
{
|
|
76
|
-
'etag' => response['etag'],
|
|
77
|
-
'last-modified' => response['last-modified'],
|
|
78
|
-
'cache-control' => response['cache-control'],
|
|
79
|
-
'expires' => response['expires'],
|
|
80
|
-
'content-type' => response['content-type'],
|
|
81
|
-
'date' => response['date'],
|
|
82
|
-
'vary' => response['vary']
|
|
83
|
-
}.compact
|
|
84
|
-
else
|
|
85
|
-
{}
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def self.extract_status_code(response)
|
|
90
|
-
case response
|
|
91
|
-
when Hash
|
|
92
|
-
response['status'] || response[:status] || 200
|
|
93
|
-
when ->(r) { r.respond_to?(:status) }
|
|
94
|
-
response.status
|
|
95
|
-
when ->(r) { r.respond_to?(:code) }
|
|
96
|
-
response.code.to_i
|
|
97
|
-
else
|
|
98
|
-
200
|
|
99
|
-
end
|
|
56
|
+
!!(etag && !etag.empty?) || !!(last_modified && !last_modified.empty?)
|
|
100
57
|
end
|
|
101
58
|
end
|
|
102
59
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Hal
|
|
5
|
+
module Cache
|
|
6
|
+
module ResponseAdapter
|
|
7
|
+
def self.headers(response)
|
|
8
|
+
case response
|
|
9
|
+
when Hash
|
|
10
|
+
response.select { |k, _| k.is_a?(String) && k.match?(/^[a-z-]+$/) }
|
|
11
|
+
when nil
|
|
12
|
+
{}
|
|
13
|
+
else
|
|
14
|
+
response.headers.to_h
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.status_code(response)
|
|
19
|
+
case response
|
|
20
|
+
when Hash
|
|
21
|
+
response['status'] || response[:status] || 200
|
|
22
|
+
when nil
|
|
23
|
+
200
|
|
24
|
+
else
|
|
25
|
+
response.status
|
|
26
|
+
end
|
|
27
|
+
rescue NoMethodError
|
|
28
|
+
200
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Hal
|
|
5
|
+
module Cache
|
|
6
|
+
autoload :CacheConfiguration, 'lutaml/hal/cache/cache_configuration'
|
|
7
|
+
autoload :CacheEntry, 'lutaml/hal/cache/cache_entry'
|
|
8
|
+
autoload :CacheManager, 'lutaml/hal/cache/cache_manager'
|
|
9
|
+
autoload :CacheMetadata, 'lutaml/hal/cache/cache_metadata'
|
|
10
|
+
autoload :ResponseAdapter, 'lutaml/hal/cache/response_adapter'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/lutaml/hal/client.rb
CHANGED
|
@@ -4,12 +4,9 @@ require 'faraday'
|
|
|
4
4
|
require 'faraday/follow_redirects'
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'rainbow'
|
|
7
|
-
require_relative 'errors'
|
|
8
|
-
require_relative 'rate_limiter'
|
|
9
7
|
|
|
10
8
|
module Lutaml
|
|
11
9
|
module Hal
|
|
12
|
-
# HAL Client for making HTTP requests to HAL APIs
|
|
13
10
|
class Client
|
|
14
11
|
attr_reader :last_response, :api_url, :connection, :rate_limiter
|
|
15
12
|
|
|
@@ -19,46 +16,38 @@ module Lutaml
|
|
|
19
16
|
@params_default = options[:params_default] || {}
|
|
20
17
|
@debug = options[:debug] || !ENV['DEBUG_API'].nil?
|
|
21
18
|
@rate_limiter = options[:rate_limiter] || RateLimiter.new(options[:rate_limiting] || {})
|
|
22
|
-
|
|
23
19
|
@api_url = strip_api_url(@api_url)
|
|
24
20
|
end
|
|
25
21
|
|
|
26
|
-
# Strip any trailing slash from the API URL
|
|
27
22
|
def strip_api_url(url)
|
|
28
23
|
url.sub(%r{/\Z}, '')
|
|
29
24
|
end
|
|
30
25
|
|
|
31
|
-
# Get a resource by its full URL
|
|
32
26
|
def get_by_url(url, params = {})
|
|
33
|
-
# Strip API endpoint if it's included
|
|
34
27
|
path = strip_api_url(url)
|
|
35
28
|
get(path, params)
|
|
36
29
|
end
|
|
37
30
|
|
|
38
|
-
# Get a resource by its full URL with custom headers (e.g. conditional
|
|
39
|
-
# request headers for cache revalidation)
|
|
40
31
|
def get_by_url_with_headers(url, headers = {})
|
|
41
32
|
path = strip_api_url(url)
|
|
42
33
|
get_with_headers(path, headers)
|
|
43
34
|
end
|
|
44
35
|
|
|
45
|
-
# Make a GET request to the API
|
|
46
36
|
def get(url, params = {})
|
|
47
37
|
@rate_limiter.with_rate_limiting do
|
|
48
38
|
@last_response = @connection.get(url, params)
|
|
49
39
|
handle_response(@last_response, url)
|
|
50
40
|
end
|
|
51
41
|
rescue Faraday::ConnectionFailed => e
|
|
52
|
-
raise
|
|
42
|
+
raise ConnectionError, "Connection failed: #{e.message}"
|
|
53
43
|
rescue Faraday::TimeoutError => e
|
|
54
|
-
raise
|
|
44
|
+
raise TimeoutError, "Request timed out: #{e.message}"
|
|
55
45
|
rescue Faraday::ParsingError => e
|
|
56
|
-
raise
|
|
46
|
+
raise ParsingError, "Response parsing error: #{e.message}"
|
|
57
47
|
rescue Faraday::Adapter::Test::Stubs::NotFound => e
|
|
58
|
-
raise
|
|
48
|
+
raise LinkResolutionError, "Resource not found: #{e.message}"
|
|
59
49
|
end
|
|
60
50
|
|
|
61
|
-
# Make a GET request with custom headers
|
|
62
51
|
def get_with_headers(url, headers = {})
|
|
63
52
|
@rate_limiter.with_rate_limiting do
|
|
64
53
|
@last_response = @connection.get(url) do |req|
|
|
@@ -67,13 +56,13 @@ module Lutaml
|
|
|
67
56
|
handle_response(@last_response, url)
|
|
68
57
|
end
|
|
69
58
|
rescue Faraday::ConnectionFailed => e
|
|
70
|
-
raise
|
|
59
|
+
raise ConnectionError, "Connection failed: #{e.message}"
|
|
71
60
|
rescue Faraday::TimeoutError => e
|
|
72
|
-
raise
|
|
61
|
+
raise TimeoutError, "Request timed out: #{e.message}"
|
|
73
62
|
rescue Faraday::ParsingError => e
|
|
74
|
-
raise
|
|
63
|
+
raise ParsingError, "Response parsing error: #{e.message}"
|
|
75
64
|
rescue Faraday::Adapter::Test::Stubs::NotFound => e
|
|
76
|
-
raise
|
|
65
|
+
raise LinkResolutionError, "Resource not found: #{e.message}"
|
|
77
66
|
end
|
|
78
67
|
|
|
79
68
|
private
|
|
@@ -124,7 +113,6 @@ module Lutaml
|
|
|
124
113
|
puts "URL: #{url}"
|
|
125
114
|
puts "Status: #{response.status}"
|
|
126
115
|
|
|
127
|
-
# Format headers as JSON
|
|
128
116
|
puts "\nHeaders:"
|
|
129
117
|
headers_hash = response.headers.to_h
|
|
130
118
|
puts JSON.pretty_generate(headers_hash)
|
|
@@ -4,14 +4,6 @@ require 'singleton'
|
|
|
4
4
|
|
|
5
5
|
module Lutaml
|
|
6
6
|
module Hal
|
|
7
|
-
# Global register for model registers
|
|
8
|
-
# This class is a singleton that manages the registration and retrieval of model registers.
|
|
9
|
-
# It ensures that each model register is unique and provides a way to access them globally.
|
|
10
|
-
#
|
|
11
|
-
# @example
|
|
12
|
-
# global_register = GlobalRegister.instance
|
|
13
|
-
# global_register.register(:example, ExampleModelRegister.new)
|
|
14
|
-
# example_register = global_register.get(:example)
|
|
15
7
|
class GlobalRegister
|
|
16
8
|
include Singleton
|
|
17
9
|
|
|
@@ -44,17 +36,14 @@ module Lutaml
|
|
|
44
36
|
delete(name)
|
|
45
37
|
end
|
|
46
38
|
|
|
47
|
-
# Cache management methods for all registered model registers
|
|
48
39
|
def clear_all_caches
|
|
49
|
-
@model_registers.each_value
|
|
50
|
-
register.clear_cache if register.respond_to?(:clear_cache)
|
|
51
|
-
end
|
|
40
|
+
@model_registers.each_value(&:clear_cache)
|
|
52
41
|
end
|
|
53
42
|
|
|
54
43
|
def cache_stats
|
|
55
44
|
stats = {}
|
|
56
45
|
@model_registers.each do |name, register|
|
|
57
|
-
stats[name] = register.cache_info
|
|
46
|
+
stats[name] = register.cache_info
|
|
58
47
|
end
|
|
59
48
|
stats
|
|
60
49
|
end
|
data/lib/lutaml/hal/link.rb
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'lutaml/model'
|
|
4
|
-
require_relative 'model_register'
|
|
5
4
|
|
|
6
5
|
module Lutaml
|
|
7
6
|
module Hal
|
|
8
|
-
# HAL Link representation with realization capability
|
|
9
7
|
class Link < Lutaml::Model::Serializable
|
|
10
|
-
|
|
11
|
-
# will be used to resolve unless overriden in resource#realize()
|
|
12
|
-
attr_accessor :_global_register_id
|
|
13
|
-
|
|
14
|
-
# Store reference to parent resource for automatic embedded content detection
|
|
15
|
-
attr_accessor :parent_resource
|
|
8
|
+
attr_accessor :_global_register_id, :parent_resource
|
|
16
9
|
|
|
17
10
|
attribute :href, :string
|
|
18
11
|
attribute :title, :string
|
|
@@ -23,25 +16,17 @@ module Lutaml
|
|
|
23
16
|
attribute :profile, :string
|
|
24
17
|
attribute :lang, :string
|
|
25
18
|
|
|
26
|
-
# Fetch the actual resource this link points to.
|
|
27
|
-
# This method will use the global register according to the source of the Link object.
|
|
28
|
-
# If the Link does not have a register, a register needs to be provided explicitly
|
|
29
|
-
# via the `register:` parameter.
|
|
30
19
|
def realize(register: nil, parent_resource: nil, force_refresh: false)
|
|
31
|
-
# Use provided parent_resource or fall back to stored parent_resource
|
|
32
20
|
effective_parent = parent_resource || @parent_resource
|
|
33
21
|
|
|
34
22
|
register = find_register(register)
|
|
35
23
|
raise "No register provided for link resolution (class: #{self.class}, href: #{href})" if register.nil?
|
|
36
24
|
|
|
37
|
-
# Priority 1: Check embedded content first (unless force_refresh)
|
|
38
25
|
if !force_refresh && effective_parent && (embedded_content = check_embedded_content(effective_parent, register))
|
|
39
|
-
# Cache embedded content too, so later lookups by href are served locally
|
|
40
26
|
register.cache_manager&.set(href, nil, embedded_content)
|
|
41
27
|
return embedded_content
|
|
42
28
|
end
|
|
43
29
|
|
|
44
|
-
# Force refresh bypasses any cached entry for this href
|
|
45
30
|
register.cache_manager&.invalidate(href) if force_refresh
|
|
46
31
|
|
|
47
32
|
Hal.debug_log "Resolving link href: #{href} using register"
|
|
@@ -51,17 +36,12 @@ module Lutaml
|
|
|
51
36
|
private
|
|
52
37
|
|
|
53
38
|
def check_embedded_content(parent_resource, register = nil)
|
|
54
|
-
return nil unless parent_resource.
|
|
39
|
+
return nil unless parent_resource.is_a?(Resource) && parent_resource.embedded_data
|
|
55
40
|
|
|
56
|
-
# Try to find embedded content that matches this link
|
|
57
|
-
# Look for embedded content by matching the link's key or href pattern
|
|
58
41
|
embedded_data = parent_resource.embedded_data
|
|
59
42
|
return nil unless embedded_data
|
|
60
43
|
|
|
61
|
-
# Try to find matching embedded content
|
|
62
|
-
# This is a simplified approach - in practice, you might need more sophisticated matching
|
|
63
44
|
embedded_data.each_value do |content|
|
|
64
|
-
# If content is an array, check if any item matches this link
|
|
65
45
|
if content.is_a?(Array)
|
|
66
46
|
matching_item = content.find { |item| matches_embedded_item?(item) }
|
|
67
47
|
return create_embedded_resource(matching_item, parent_resource, register) if matching_item
|
|
@@ -76,25 +56,17 @@ module Lutaml
|
|
|
76
56
|
def matches_embedded_item?(item)
|
|
77
57
|
return false unless item.is_a?(Hash)
|
|
78
58
|
|
|
79
|
-
|
|
80
|
-
item_self_link = item.dig('_links', 'self', 'href')
|
|
81
|
-
return true if item_self_link == href
|
|
82
|
-
|
|
83
|
-
# Additional matching logic could be added here
|
|
84
|
-
false
|
|
59
|
+
item.dig('_links', 'self', 'href') == href
|
|
85
60
|
end
|
|
86
61
|
|
|
87
62
|
def create_embedded_resource(embedded_item, _parent_resource, register = nil)
|
|
88
|
-
# Get the register to determine the appropriate model class
|
|
89
63
|
register = find_register(register)
|
|
90
64
|
return nil unless register
|
|
91
65
|
|
|
92
|
-
# Try to find the model class for this href
|
|
93
66
|
href_path = href.sub(register.client.api_url, '') if register.client
|
|
94
67
|
model_class = register.find_matching_model_class(href_path)
|
|
95
68
|
return nil unless model_class
|
|
96
69
|
|
|
97
|
-
# Create the resource from embedded data
|
|
98
70
|
resource = model_class.from_embedded(embedded_item, _global_register_id)
|
|
99
71
|
register.mark_model_links_with_register(resource)
|
|
100
72
|
resource
|
|
@@ -106,7 +78,7 @@ module Lutaml
|
|
|
106
78
|
register_id = _global_register_id
|
|
107
79
|
return nil if register_id.nil?
|
|
108
80
|
|
|
109
|
-
register =
|
|
81
|
+
register = GlobalRegister.instance.get(register_id)
|
|
110
82
|
if register.nil?
|
|
111
83
|
raise 'GlobalRegister in use but unable to find the register. '\
|
|
112
84
|
'Please provide a register to the `#realize` method to resolve the link'
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'link'
|
|
4
|
-
require_relative 'type_resolver'
|
|
5
|
-
|
|
6
3
|
module Lutaml
|
|
7
4
|
module Hal
|
|
8
|
-
# Factory class responsible for creating dynamic Link classes
|
|
9
5
|
class LinkClassFactory
|
|
10
6
|
def self.create_for(resource_class, realize_class_name)
|
|
11
7
|
new(resource_class, realize_class_name).create
|
|
@@ -74,8 +70,6 @@ module Lutaml
|
|
|
74
70
|
def register_constant(klass, class_names)
|
|
75
71
|
parent_klass = resolve_parent_class(class_names[:parent_namespace])
|
|
76
72
|
|
|
77
|
-
# Avoid registering constants on Object in test scenarios
|
|
78
|
-
# This prevents conflicts with test mocks that expect specific const_set calls
|
|
79
73
|
return if parent_klass == Object && in_test_environment?
|
|
80
74
|
|
|
81
75
|
parent_klass.const_set(class_names[:child_name], klass)
|
|
@@ -92,7 +86,6 @@ module Lutaml
|
|
|
92
86
|
end
|
|
93
87
|
|
|
94
88
|
def in_test_environment?
|
|
95
|
-
# Check if we're in a test environment by looking for RSpec or test-related constants
|
|
96
89
|
defined?(RSpec) || defined?(Test::Unit) || ENV['RAILS_ENV'] == 'test' || ENV['RACK_ENV'] == 'test'
|
|
97
90
|
end
|
|
98
91
|
end
|
data/lib/lutaml/hal/link_set.rb
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'lutaml/model'
|
|
4
|
-
require_relative 'model_register'
|
|
5
4
|
|
|
6
5
|
module Lutaml
|
|
7
6
|
module Hal
|
|
8
|
-
# HAL Link representation with realization capability
|
|
9
7
|
class LinkSet < Lutaml::Model::Serializable
|
|
10
8
|
attr_accessor :_global_register_id
|
|
11
9
|
end
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'link_set'
|
|
4
|
-
|
|
5
3
|
module Lutaml
|
|
6
4
|
module Hal
|
|
7
|
-
# Factory class responsible for creating dynamic LinkSet classes
|
|
8
5
|
class LinkSetClassFactory
|
|
9
6
|
def self.create_for(resource_class)
|
|
10
7
|
new(resource_class).create
|
|
@@ -79,7 +76,6 @@ module Lutaml
|
|
|
79
76
|
end
|
|
80
77
|
|
|
81
78
|
def setup_resource_mapping(link_set_class)
|
|
82
|
-
# Define the LinkSet class with mapping inside the resource class
|
|
83
79
|
@resource_class.class_eval do
|
|
84
80
|
attribute :links, link_set_class
|
|
85
81
|
key_value do
|