lutaml-hal 0.2.1 → 0.2.3
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 +97 -142
- 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/single_flight.rb +63 -0
- data/lib/lutaml/hal/version.rb +1 -1
- data/lib/lutaml/hal.rb +29 -13
- metadata +5 -3
- data/lib/lutaml/hal/cache/simple_cache_store.rb +0 -83
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b352439d0153969cc07791e9e8e471b99fd364f20028c28b49f9b97ee35bfd1b
|
|
4
|
+
data.tar.gz: 4f20e2db09645e6ff127bc5c22e9ba96d8391a4b5158777b5e7224fe741572ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6856e9d221dfdff0fa1df27953848db6983056391a90a146909ac692905cb225b95f780f46923b475df1b92aa8daf26d1e27fff7eadf6f9c6bb772ca7bc01647
|
|
7
|
+
data.tar.gz: f52e887598db0c96c8060d643b4763873cb226a3c540e5018e6fe2537fc9d0b79886fef5c04edf9146b878c78f95c19fdd6bcc70d054f6369c86177c26ebce02
|
|
@@ -5,23 +5,16 @@ require 'lutaml/model'
|
|
|
5
5
|
module Lutaml
|
|
6
6
|
module Hal
|
|
7
7
|
module Cache
|
|
8
|
-
# Represents cache configuration with validation and defaults
|
|
9
8
|
class CacheConfiguration < Lutaml::Model::Serializable
|
|
10
9
|
attribute :adapter_type, :string
|
|
11
10
|
attribute :adapter_config, :hash
|
|
12
11
|
attribute :ttl, :integer
|
|
13
12
|
attribute :max_size, :integer
|
|
14
|
-
attribute :http_aware, :boolean
|
|
15
|
-
attribute :respect_http_headers, :boolean
|
|
16
|
-
attribute :enable_conditional_requests, :boolean
|
|
17
|
-
attribute :ignore_query_params, :string
|
|
18
13
|
|
|
19
|
-
# Default configuration values
|
|
20
14
|
DEFAULT_TTL = 3600
|
|
21
15
|
DEFAULT_MAX_SIZE = 1000
|
|
22
16
|
DEFAULT_ADAPTER_TYPE = 'memory'
|
|
23
17
|
|
|
24
|
-
# Create configuration from hash or symbol
|
|
25
18
|
def self.from_config(config)
|
|
26
19
|
return new if config.nil?
|
|
27
20
|
|
|
@@ -35,7 +28,6 @@ module Lutaml
|
|
|
35
28
|
end
|
|
36
29
|
end
|
|
37
30
|
|
|
38
|
-
# Validate the configuration
|
|
39
31
|
def validate!
|
|
40
32
|
validate_adapter_type!
|
|
41
33
|
validate_ttl!
|
|
@@ -43,80 +35,48 @@ module Lutaml
|
|
|
43
35
|
validate_adapter_config!
|
|
44
36
|
end
|
|
45
37
|
|
|
46
|
-
# Check if HTTP-aware caching should be used
|
|
47
|
-
#
|
|
48
|
-
# HTTP-aware caching (conditional requests backed by a response cache)
|
|
49
|
-
# is opt-in: it only applies when explicitly enabled and the
|
|
50
|
-
# lutaml-store HTTP cache backend is available. By default the register
|
|
51
|
-
# uses the basic object cache, which stores realized models directly.
|
|
52
|
-
def http_aware?
|
|
53
|
-
http_aware == true && http_cache_available?
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Check if basic caching should be used
|
|
57
|
-
def basic_cache?
|
|
58
|
-
!http_aware?
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Get the effective TTL (with fallback to default)
|
|
62
38
|
def effective_ttl
|
|
63
39
|
ttl || DEFAULT_TTL
|
|
64
40
|
end
|
|
65
41
|
|
|
66
|
-
# Get the effective max size (with fallback to default)
|
|
67
42
|
def effective_max_size
|
|
68
43
|
max_size || DEFAULT_MAX_SIZE
|
|
69
44
|
end
|
|
70
45
|
|
|
71
|
-
# Get the effective adapter type (with fallback to default)
|
|
72
46
|
def effective_adapter_type
|
|
73
47
|
adapter_type || DEFAULT_ADAPTER_TYPE
|
|
74
48
|
end
|
|
75
49
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
adapter_type: effective_adapter_type.to_sym,
|
|
80
|
-
default_ttl: effective_ttl,
|
|
81
|
-
max_entries: effective_max_size,
|
|
82
|
-
respect_http_headers: respect_http_headers != false,
|
|
83
|
-
enable_conditional_requests: enable_conditional_requests != false,
|
|
84
|
-
ignore_query_params: parse_ignore_query_params
|
|
85
|
-
}.merge(adapter_config || {})
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Get basic cache configuration hash
|
|
89
|
-
def basic_cache_config
|
|
90
|
-
{
|
|
91
|
-
adapter: adapter_config || { type: effective_adapter_type.to_sym },
|
|
50
|
+
def to_cache_store_config
|
|
51
|
+
base = {
|
|
52
|
+
adapter: { type: effective_adapter_type.to_sym },
|
|
92
53
|
default_ttl: effective_ttl,
|
|
93
54
|
max_size: effective_max_size
|
|
94
55
|
}
|
|
56
|
+
options = adapter_config&.dig(:options) || adapter_config&.dig('options')
|
|
57
|
+
base[:adapter_options] = options if options
|
|
58
|
+
base
|
|
95
59
|
end
|
|
96
60
|
|
|
97
61
|
private
|
|
98
62
|
|
|
99
63
|
def self.from_hash(config)
|
|
100
64
|
adapter_info = config[:adapter] || config['adapter'] || {}
|
|
101
|
-
|
|
102
|
-
# Handle direct adapter_type specification
|
|
103
|
-
adapter_type = config[:adapter_type] || config['adapter_type'] || extract_adapter_type(adapter_info)
|
|
65
|
+
adapter_type = config_value(config, :adapter_type) || extract_adapter_type(adapter_info)
|
|
104
66
|
|
|
105
67
|
new(
|
|
106
68
|
adapter_type: adapter_type,
|
|
107
69
|
adapter_config: adapter_info.is_a?(Hash) ? adapter_info : nil,
|
|
108
|
-
ttl: config
|
|
109
|
-
max_size: config
|
|
110
|
-
http_aware: config.key?(:http_aware) ? config[:http_aware] : config['http_aware'],
|
|
111
|
-
respect_http_headers: config.key?(:respect_http_headers) ? config[:respect_http_headers] : config['respect_http_headers'],
|
|
112
|
-
enable_conditional_requests: config.key?(:enable_conditional_requests) ? config[:enable_conditional_requests] : config['enable_conditional_requests'],
|
|
113
|
-
ignore_query_params: config[:ignore_query_params] || config['ignore_query_params']
|
|
70
|
+
ttl: config_value(config, :ttl),
|
|
71
|
+
max_size: config_value(config, :max_size)
|
|
114
72
|
)
|
|
115
73
|
end
|
|
74
|
+
private_class_method :from_hash
|
|
116
75
|
|
|
117
76
|
def self.from_simple_config(config)
|
|
118
77
|
new(adapter_type: config.to_s)
|
|
119
78
|
end
|
|
79
|
+
private_class_method :from_simple_config
|
|
120
80
|
|
|
121
81
|
def self.extract_adapter_type(adapter_info)
|
|
122
82
|
case adapter_info
|
|
@@ -127,6 +87,12 @@ module Lutaml
|
|
|
127
87
|
adapter_info.to_s
|
|
128
88
|
end
|
|
129
89
|
end
|
|
90
|
+
private_class_method :extract_adapter_type
|
|
91
|
+
|
|
92
|
+
def self.config_value(config, key)
|
|
93
|
+
config[key] || config[key.to_s]
|
|
94
|
+
end
|
|
95
|
+
private_class_method :config_value
|
|
130
96
|
|
|
131
97
|
def validate_adapter_type!
|
|
132
98
|
valid_types = %w[memory filesystem sqlite]
|
|
@@ -155,30 +121,6 @@ module Lutaml
|
|
|
155
121
|
|
|
156
122
|
raise ArgumentError, "Adapter config must be a hash, got: #{adapter_config.class}"
|
|
157
123
|
end
|
|
158
|
-
|
|
159
|
-
# Override the setter to validate before assignment
|
|
160
|
-
def adapter_config=(value)
|
|
161
|
-
raise ArgumentError, "Adapter config must be a hash, got: #{value.class}" if value && !value.is_a?(Hash)
|
|
162
|
-
|
|
163
|
-
super(value)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def http_cache_available?
|
|
167
|
-
defined?(::Lutaml::Store::HttpCache)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def parse_ignore_query_params
|
|
171
|
-
return [] unless ignore_query_params
|
|
172
|
-
|
|
173
|
-
case ignore_query_params
|
|
174
|
-
when String
|
|
175
|
-
ignore_query_params.split(',').map(&:strip)
|
|
176
|
-
when Array
|
|
177
|
-
ignore_query_params
|
|
178
|
-
else
|
|
179
|
-
[]
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
124
|
end
|
|
183
125
|
end
|
|
184
126
|
end
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
|
|
4
|
+
require 'time'
|
|
5
5
|
|
|
6
6
|
module Lutaml
|
|
7
7
|
module Hal
|
|
8
8
|
module Cache
|
|
9
|
-
# Represents a complete cached entry with metadata and HAL resource
|
|
10
9
|
class CacheEntry
|
|
11
10
|
attr_accessor :url, :cached_at, :metadata, :hal_resource
|
|
12
11
|
|
|
@@ -17,9 +16,6 @@ module Lutaml
|
|
|
17
16
|
@hal_resource = hal_resource
|
|
18
17
|
end
|
|
19
18
|
|
|
20
|
-
# Plain-hash representation suitable for JSON persistence. The HAL
|
|
21
|
-
# resource and its class are recorded so the model can be rebuilt; the
|
|
22
|
-
# metadata is kept as its own JSON document.
|
|
23
19
|
def to_storage_h
|
|
24
20
|
{
|
|
25
21
|
'url' => url,
|
|
@@ -30,21 +26,15 @@ module Lutaml
|
|
|
30
26
|
}
|
|
31
27
|
end
|
|
32
28
|
|
|
33
|
-
# Called by lutaml-store's CacheStore when serializing a persisted entry.
|
|
34
29
|
def to_json(*_args)
|
|
35
30
|
JSON.generate(to_storage_h)
|
|
36
31
|
end
|
|
37
32
|
|
|
38
|
-
# True if the given hash looks like a to_storage_h document (as opposed
|
|
39
|
-
# to a legacy in-memory cache hash holding a live :realized_model).
|
|
40
33
|
def self.storage_format?(hash)
|
|
41
34
|
hash.key?('model') || hash.key?(:model) ||
|
|
42
35
|
hash.key?('model_class') || hash.key?(:model_class)
|
|
43
36
|
end
|
|
44
37
|
|
|
45
|
-
# Rebuild a CacheEntry from a to_storage_h document. Tolerates string or
|
|
46
|
-
# symbol keys, since lutaml-store parses persisted JSON with
|
|
47
|
-
# symbolize_names.
|
|
48
38
|
def self.from_storage_h(hash)
|
|
49
39
|
h = hash.transform_keys(&:to_s)
|
|
50
40
|
new(
|
|
@@ -63,7 +53,6 @@ module Lutaml
|
|
|
63
53
|
nil
|
|
64
54
|
end
|
|
65
55
|
|
|
66
|
-
# Create a cache entry from a URL, response, and realized HAL resource
|
|
67
56
|
def self.create(url, response, hal_resource)
|
|
68
57
|
new(
|
|
69
58
|
url: url,
|
|
@@ -73,7 +62,6 @@ module Lutaml
|
|
|
73
62
|
)
|
|
74
63
|
end
|
|
75
64
|
|
|
76
|
-
# Check if the cache entry is still valid based on TTL
|
|
77
65
|
def valid?(default_ttl)
|
|
78
66
|
return false unless cached_at
|
|
79
67
|
|
|
@@ -84,35 +72,29 @@ module Lutaml
|
|
|
84
72
|
age < ttl
|
|
85
73
|
end
|
|
86
74
|
|
|
87
|
-
# Check if the entry is expired and needs revalidation
|
|
88
75
|
def expired?(default_ttl)
|
|
89
76
|
!valid?(default_ttl)
|
|
90
77
|
end
|
|
91
78
|
|
|
92
|
-
# Check if the entry can be revalidated with conditional requests
|
|
93
79
|
def revalidatable?
|
|
94
80
|
return false unless metadata
|
|
95
81
|
|
|
96
82
|
!!(metadata.etag || metadata.last_modified)
|
|
97
83
|
end
|
|
98
84
|
|
|
99
|
-
# Get conditional headers for revalidation
|
|
100
85
|
def conditional_headers
|
|
101
86
|
metadata&.conditional_headers || {}
|
|
102
87
|
end
|
|
103
88
|
|
|
104
|
-
# Check if the response is cacheable based on metadata
|
|
105
89
|
def cacheable?
|
|
106
90
|
metadata&.cacheable? != false
|
|
107
91
|
end
|
|
108
92
|
|
|
109
|
-
# Update the cache entry with fresh metadata (for 304 responses)
|
|
110
93
|
def refresh_metadata(response)
|
|
111
94
|
self.cached_at = Time.now.to_s
|
|
112
95
|
self.metadata = CacheMetadata.from_response(response)
|
|
113
96
|
end
|
|
114
97
|
|
|
115
|
-
# Get cache age in seconds
|
|
116
98
|
def age
|
|
117
99
|
return 0 unless cached_at
|
|
118
100
|
|
|
@@ -120,17 +102,14 @@ module Lutaml
|
|
|
120
102
|
Time.now - cached_time
|
|
121
103
|
end
|
|
122
104
|
|
|
123
|
-
# Check if entry should be served stale (useful for error scenarios)
|
|
124
105
|
def serve_stale?(max_stale = nil)
|
|
125
106
|
return false unless max_stale
|
|
126
|
-
return false if valid?(Float::INFINITY)
|
|
107
|
+
return false if valid?(Float::INFINITY)
|
|
127
108
|
|
|
128
109
|
cached_time = cached_at.is_a?(String) ? Time.parse(cached_at) : cached_at
|
|
129
110
|
current_age = Time.now - cached_time
|
|
130
111
|
ttl = metadata&.max_age || 0
|
|
131
112
|
|
|
132
|
-
# Entry is stale if current_age > ttl
|
|
133
|
-
# But we can serve it if the staleness is within the max_stale window
|
|
134
113
|
staleness = current_age - ttl
|
|
135
114
|
current_age > ttl && staleness < max_stale
|
|
136
115
|
end
|
|
@@ -1,31 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative 'cache_entry'
|
|
5
|
-
require_relative 'cache_metadata'
|
|
6
|
-
require_relative 'simple_cache_store'
|
|
7
|
-
|
|
8
|
-
# Try to require lutaml-store. Requiring the entry point (rather than the
|
|
9
|
-
# individual files) sets up the autoloads it relies on internally, e.g.
|
|
10
|
-
# Lutaml::Store::HttpCacheConfig referenced from HttpCache#initialize.
|
|
11
|
-
begin
|
|
12
|
-
require 'lutaml/store'
|
|
13
|
-
CACHE_STORE_AVAILABLE = true
|
|
14
|
-
rescue LoadError
|
|
15
|
-
CACHE_STORE_AVAILABLE = false
|
|
16
|
-
end
|
|
3
|
+
require 'lutaml/store'
|
|
17
4
|
|
|
18
5
|
module Lutaml
|
|
19
6
|
module Hal
|
|
20
7
|
module Cache
|
|
21
|
-
# Manages all cache operations with a clean, unified interface
|
|
22
8
|
class CacheManager
|
|
23
9
|
attr_reader :configuration, :cache_store
|
|
24
10
|
|
|
25
|
-
# @param config cache configuration (see CacheConfiguration.from_config)
|
|
26
|
-
# @param client [Lutaml::Hal::Client, nil] used to canonicalize relative
|
|
27
|
-
# URLs so that a resource fetched by endpoint path and the same
|
|
28
|
-
# resource realized from an absolute link href share a cache entry.
|
|
29
11
|
def initialize(config = nil, client: nil)
|
|
30
12
|
@client = client
|
|
31
13
|
@configuration = CacheConfiguration.from_config(config)
|
|
@@ -37,20 +19,16 @@ module Lutaml
|
|
|
37
19
|
@cache_store = create_cache_store
|
|
38
20
|
end
|
|
39
21
|
|
|
40
|
-
# Get a cache entry by URL
|
|
41
22
|
def get(url)
|
|
42
23
|
return nil unless cache_store
|
|
43
24
|
|
|
44
25
|
key = cache_key(url)
|
|
26
|
+
raw = cache_store.get(key)
|
|
27
|
+
return nil unless raw
|
|
45
28
|
|
|
46
|
-
|
|
47
|
-
get_from_http_cache(url, key)
|
|
48
|
-
else
|
|
49
|
-
get_from_basic_cache(key)
|
|
50
|
-
end
|
|
29
|
+
deserialize_entry(raw)
|
|
51
30
|
end
|
|
52
31
|
|
|
53
|
-
# Store a cache entry
|
|
54
32
|
def set(url, response, hal_resource)
|
|
55
33
|
return unless cache_store
|
|
56
34
|
|
|
@@ -58,17 +36,10 @@ module Lutaml
|
|
|
58
36
|
return unless entry.cacheable?
|
|
59
37
|
|
|
60
38
|
key = cache_key(url)
|
|
61
|
-
|
|
62
|
-
if http_aware_cache?
|
|
63
|
-
set_in_http_cache(key, entry, response)
|
|
64
|
-
else
|
|
65
|
-
set_in_basic_cache(key, entry)
|
|
66
|
-
end
|
|
67
|
-
|
|
39
|
+
cache_store.set(key, entry.to_storage_h)
|
|
68
40
|
entry
|
|
69
41
|
end
|
|
70
42
|
|
|
71
|
-
# Make a conditional request using cached metadata
|
|
72
43
|
def conditional_request_headers(url)
|
|
73
44
|
entry = get(url)
|
|
74
45
|
return {} unless entry&.revalidatable?
|
|
@@ -76,16 +47,15 @@ module Lutaml
|
|
|
76
47
|
entry.conditional_headers
|
|
77
48
|
end
|
|
78
49
|
|
|
79
|
-
# Update cache entry after a 304 Not Modified response
|
|
80
50
|
def refresh_entry(url, response)
|
|
81
51
|
entry = get(url)
|
|
82
52
|
return unless entry
|
|
83
53
|
|
|
84
54
|
entry.refresh_metadata(response)
|
|
85
|
-
|
|
55
|
+
key = cache_key(url)
|
|
56
|
+
cache_store.set(key, entry.to_storage_h)
|
|
86
57
|
end
|
|
87
58
|
|
|
88
|
-
# Remove a specific cache entry
|
|
89
59
|
def invalidate(url)
|
|
90
60
|
return unless cache_store
|
|
91
61
|
|
|
@@ -93,100 +63,47 @@ module Lutaml
|
|
|
93
63
|
cache_store.delete(key)
|
|
94
64
|
end
|
|
95
65
|
|
|
96
|
-
# Clear all cache entries
|
|
97
66
|
def clear
|
|
98
67
|
return unless cache_store
|
|
99
68
|
|
|
100
69
|
cache_store.clear
|
|
101
70
|
end
|
|
102
71
|
|
|
103
|
-
# Get cache statistics
|
|
104
72
|
def stats
|
|
105
73
|
return {} unless cache_store
|
|
106
74
|
|
|
107
|
-
|
|
108
|
-
cache_store.cache_info
|
|
109
|
-
elsif cache_store.respond_to?(:stats)
|
|
110
|
-
cache_store.stats
|
|
111
|
-
else
|
|
112
|
-
{}
|
|
113
|
-
end
|
|
75
|
+
cache_store.cache_info
|
|
114
76
|
end
|
|
115
77
|
|
|
116
|
-
# Get cache information
|
|
117
78
|
def info
|
|
118
79
|
return nil unless cache_store
|
|
119
80
|
|
|
120
81
|
{
|
|
121
82
|
adapter_type: cache_store.class.name,
|
|
122
83
|
configuration: configuration,
|
|
123
|
-
current_size: cache_store.
|
|
84
|
+
current_size: cache_store.size,
|
|
124
85
|
stats: stats
|
|
125
86
|
}
|
|
126
87
|
end
|
|
127
88
|
|
|
128
|
-
# Check if cache is available and configured
|
|
129
89
|
def available?
|
|
130
90
|
!cache_store.nil?
|
|
131
91
|
end
|
|
132
92
|
|
|
133
|
-
# Check if using HTTP-aware cache
|
|
134
|
-
def http_aware_cache?
|
|
135
|
-
configuration.http_aware? && cache_store.respond_to?(:fetch)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
93
|
private
|
|
139
94
|
|
|
140
95
|
def create_cache_store
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# to round-trip itself (and rebuild its HAL model) for that path.
|
|
144
|
-
#
|
|
145
|
-
# The default in-memory adapter uses SimpleCacheStore, which keeps live
|
|
146
|
-
# CacheEntry objects so cache hits avoid any serialization cost.
|
|
147
|
-
#
|
|
148
|
-
# NOTE: Backing the HTTP-aware mode with lutaml-store's HttpCache
|
|
149
|
-
# response cache is deferred until realized models can be
|
|
150
|
-
# reconstructed from a cached response (requires the resource class);
|
|
151
|
-
# the create_http_cache / *_http_cache helpers remain as scaffolding.
|
|
152
|
-
if CACHE_STORE_AVAILABLE && persistent_adapter?
|
|
153
|
-
create_basic_cache
|
|
154
|
-
else
|
|
155
|
-
create_simple_cache
|
|
156
|
-
end
|
|
96
|
+
store_config = @configuration.to_cache_store_config
|
|
97
|
+
::Lutaml::Store::CacheStore.new(store_config)
|
|
157
98
|
rescue StandardError => e
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Whether the configured adapter persists beyond the process.
|
|
163
|
-
def persistent_adapter?
|
|
164
|
-
%w[filesystem sqlite].include?(configuration.effective_adapter_type)
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def create_http_cache
|
|
168
|
-
return nil unless defined?(::Lutaml::Store::HttpCache)
|
|
169
|
-
|
|
170
|
-
::Lutaml::Store::HttpCache.new(configuration.http_cache_config)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def create_basic_cache
|
|
174
|
-
return nil unless defined?(::Lutaml::Store::CacheStore)
|
|
175
|
-
|
|
176
|
-
::Lutaml::Store::CacheStore.new(configuration.basic_cache_config)
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def create_simple_cache
|
|
180
|
-
SimpleCacheStore.new(configuration.effective_max_size)
|
|
99
|
+
Hal.debug_log("Failed to create cache store: #{e.message}")
|
|
100
|
+
nil
|
|
181
101
|
end
|
|
182
102
|
|
|
183
103
|
def cache_key(url)
|
|
184
104
|
"hal_resource:#{canonical_url(url)}"
|
|
185
105
|
end
|
|
186
106
|
|
|
187
|
-
# Normalize a URL to an absolute form so that the same resource is
|
|
188
|
-
# cached under one key regardless of whether it was reached by a
|
|
189
|
-
# relative endpoint path (fetch) or an absolute link href (realize).
|
|
190
107
|
def canonical_url(url)
|
|
191
108
|
url = url.to_s
|
|
192
109
|
return url if url.start_with?('http')
|
|
@@ -195,137 +112,12 @@ module Lutaml
|
|
|
195
112
|
"#{@client.api_url}#{url}"
|
|
196
113
|
end
|
|
197
114
|
|
|
198
|
-
def
|
|
199
|
-
|
|
200
|
-
cached_response = cache_store.get(key)
|
|
201
|
-
return nil unless cached_response
|
|
202
|
-
|
|
203
|
-
# Convert HTTP cache response back to CacheEntry
|
|
204
|
-
convert_http_response_to_entry(url, cached_response)
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def get_from_basic_cache(key)
|
|
208
|
-
cached_data = cache_store.get(key)
|
|
209
|
-
return nil unless cached_data
|
|
210
|
-
|
|
211
|
-
# In-memory stores keep a live CacheEntry; persistent stores return a
|
|
212
|
-
# plain hash that we rebuild (with its HAL model) here.
|
|
213
|
-
case cached_data
|
|
115
|
+
def deserialize_entry(raw)
|
|
116
|
+
case raw
|
|
214
117
|
when CacheEntry
|
|
215
|
-
|
|
118
|
+
raw
|
|
216
119
|
when Hash
|
|
217
|
-
if CacheEntry.storage_format?(
|
|
218
|
-
entry = CacheEntry.from_storage_h(cached_data)
|
|
219
|
-
entry&.valid?(configuration.effective_ttl) ? entry : nil
|
|
220
|
-
else
|
|
221
|
-
# Legacy in-memory hash format support
|
|
222
|
-
convert_legacy_cache_data(cached_data)
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def set_in_http_cache(key, entry, response)
|
|
228
|
-
# Convert CacheEntry to HTTP cache format
|
|
229
|
-
http_response = convert_entry_to_http_response(entry, response)
|
|
230
|
-
cache_store.set(key, http_response)
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def set_in_basic_cache(key, entry)
|
|
234
|
-
cache_store.set(key, entry)
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def set_refreshed_entry(url, entry)
|
|
238
|
-
key = cache_key(url)
|
|
239
|
-
|
|
240
|
-
if http_aware_cache?
|
|
241
|
-
# For HTTP cache, we need to update the stored response
|
|
242
|
-
http_response = convert_entry_to_http_response(entry, nil)
|
|
243
|
-
cache_store.set(key, http_response)
|
|
244
|
-
else
|
|
245
|
-
cache_store.set(key, entry)
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def convert_http_response_to_entry(url, http_response)
|
|
250
|
-
# Extract HAL resource and metadata from HTTP cache response
|
|
251
|
-
body = http_response[:body] || http_response['body']
|
|
252
|
-
headers = http_response[:headers] || http_response['headers'] || {}
|
|
253
|
-
|
|
254
|
-
# Parse the body back to get the HAL resource
|
|
255
|
-
# Note: This is a simplified conversion - in practice, we might need
|
|
256
|
-
# to store the HAL resource separately or use serialization
|
|
257
|
-
hal_resource = parse_hal_resource_from_body(body)
|
|
258
|
-
|
|
259
|
-
CacheEntry.new(
|
|
260
|
-
url: url,
|
|
261
|
-
cached_at: Time.now, # HTTP cache manages its own timestamps
|
|
262
|
-
metadata: CacheMetadata.from_response(headers),
|
|
263
|
-
hal_resource: hal_resource
|
|
264
|
-
)
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def convert_entry_to_http_response(entry, _original_response)
|
|
268
|
-
{
|
|
269
|
-
status_code: entry.metadata&.status_code || 200,
|
|
270
|
-
headers: extract_headers_from_metadata(entry.metadata),
|
|
271
|
-
body: serialize_hal_resource(entry.hal_resource)
|
|
272
|
-
}
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def convert_legacy_cache_data(cached_data)
|
|
276
|
-
# Support for legacy cache format
|
|
277
|
-
return nil unless cached_data[:realized_model] && cached_data[:cached_at]
|
|
278
|
-
|
|
279
|
-
CacheEntry.new(
|
|
280
|
-
url: cached_data[:url],
|
|
281
|
-
cached_at: cached_data[:cached_at],
|
|
282
|
-
metadata: create_metadata_from_legacy(cached_data),
|
|
283
|
-
hal_resource: cached_data[:realized_model]
|
|
284
|
-
)
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
def create_metadata_from_legacy(cached_data)
|
|
288
|
-
CacheMetadata.new(
|
|
289
|
-
etag: cached_data[:etag],
|
|
290
|
-
last_modified: cached_data[:last_modified],
|
|
291
|
-
status_code: 200
|
|
292
|
-
)
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def extract_headers_from_metadata(metadata)
|
|
296
|
-
return {} unless metadata
|
|
297
|
-
|
|
298
|
-
{
|
|
299
|
-
'etag' => metadata.etag,
|
|
300
|
-
'last-modified' => metadata.last_modified,
|
|
301
|
-
'cache-control' => metadata.cache_control,
|
|
302
|
-
'expires' => metadata.expires,
|
|
303
|
-
'content-type' => metadata.content_type,
|
|
304
|
-
'date' => metadata.date,
|
|
305
|
-
'vary' => metadata.vary
|
|
306
|
-
}.compact
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def parse_hal_resource_from_body(body)
|
|
310
|
-
# This is a placeholder - in practice, we might need to store
|
|
311
|
-
# the HAL resource class information to properly deserialize
|
|
312
|
-
case body
|
|
313
|
-
when String
|
|
314
|
-
JSON.parse(body)
|
|
315
|
-
else
|
|
316
|
-
body
|
|
317
|
-
end
|
|
318
|
-
rescue JSON::ParserError
|
|
319
|
-
body
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def serialize_hal_resource(hal_resource)
|
|
323
|
-
# Serialize HAL resource for storage
|
|
324
|
-
case hal_resource
|
|
325
|
-
when ->(r) { r.respond_to?(:to_json) }
|
|
326
|
-
hal_resource.to_json
|
|
327
|
-
else
|
|
328
|
-
hal_resource.to_s
|
|
120
|
+
CacheEntry.from_storage_h(raw) if CacheEntry.storage_format?(raw)
|
|
329
121
|
end
|
|
330
122
|
end
|
|
331
123
|
end
|