lutaml-hal 0.2.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 443ab6a0ccf5927a142a841eb7e7178bb21aaaea9d64e67d84e325250cf23093
4
- data.tar.gz: 9190a8b564e5e375d6605bea71e0ba9d9489536b862aca74272d28d49961c606
3
+ metadata.gz: b352439d0153969cc07791e9e8e471b99fd364f20028c28b49f9b97ee35bfd1b
4
+ data.tar.gz: 4f20e2db09645e6ff127bc5c22e9ba96d8391a4b5158777b5e7224fe741572ab
5
5
  SHA512:
6
- metadata.gz: e0baa1f526af80dd8fdb4366f637ad059d0e7f58f4836b6a1377adca4834f846c29889c3bd6782109dc3da65f93e4387d675477794fbc0b08573b8a03684eeae
7
- data.tar.gz: aabbfd4f298149c3f71b8145e79d17bbff4293eaf9b8db99fbcf1aadb114efeb82b9d2db7ec571ce2e4651aeac0863be424a3a0335595096e104728ffcd65127
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
- # Get HTTP cache configuration hash
77
- def http_cache_config
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[:ttl] || config['ttl'],
109
- max_size: config[:max_size] || config['max_size'],
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
- require_relative 'cache_metadata'
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) # Still fresh
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
- require_relative 'cache_configuration'
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
- if http_aware_cache?
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
- set_refreshed_entry(url, entry)
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
- if cache_store.respond_to?(:cache_info)
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.respond_to?(:size) ? cache_store.size : 'unknown',
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
- # A persistent adapter (filesystem / sqlite) uses lutaml-store's
142
- # CacheStore, which serializes each entry to JSON; CacheEntry knows how
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
- Lutaml::Hal.debug_log("Failed to create cache store: #{e.message}")
159
- create_simple_cache
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 get_from_http_cache(url, key)
199
- # HTTP cache handles conditional requests internally
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
- cached_data.valid?(configuration.effective_ttl) ? cached_data : nil
118
+ raw
216
119
  when Hash
217
- if CacheEntry.storage_format?(cached_data)
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