lutaml-store 0.1.1

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.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +27 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +10 -0
  6. data/.rubocop_todo.yml +450 -0
  7. data/CLAUDE.md +57 -0
  8. data/CODE_OF_CONDUCT.md +132 -0
  9. data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +209 -0
  10. data/CORRECTED_HTTP_CACHE_PLAN.md +164 -0
  11. data/Gemfile +15 -0
  12. data/Gemfile.lock +220 -0
  13. data/README.adoc +1430 -0
  14. data/Rakefile +12 -0
  15. data/TODO.impl/0-lutaml-store-self-quality.md +112 -0
  16. data/TODO.impl/1-lutaml-hal-migration.md +60 -0
  17. data/TODO.impl/2-glossarist-migration.md +359 -0
  18. data/TODO.impl/3-lutaml-jsonschema-migration.md +273 -0
  19. data/bin/console +11 -0
  20. data/bin/setup +8 -0
  21. data/demo/Gemfile +15 -0
  22. data/demo/Gemfile.lock +61 -0
  23. data/demo/README.adoc +301 -0
  24. data/demo/data/vcards/co/contact_10_thompson.data +1 -0
  25. data/demo/data/vcards/co/contact_10_thompson.meta +1 -0
  26. data/demo/data/vcards/co/contact_1_doe.data +1 -0
  27. data/demo/data/vcards/co/contact_1_doe.meta +1 -0
  28. data/demo/data/vcards/co/contact_2_smith.data +1 -0
  29. data/demo/data/vcards/co/contact_2_smith.meta +1 -0
  30. data/demo/data/vcards/co/contact_3_johnson.data +1 -0
  31. data/demo/data/vcards/co/contact_3_johnson.meta +1 -0
  32. data/demo/data/vcards/co/contact_4_garcia.data +1 -0
  33. data/demo/data/vcards/co/contact_4_garcia.meta +1 -0
  34. data/demo/data/vcards/co/contact_5_wilson.data +1 -0
  35. data/demo/data/vcards/co/contact_5_wilson.meta +1 -0
  36. data/demo/data/vcards/co/contact_6_brown.data +1 -0
  37. data/demo/data/vcards/co/contact_6_brown.meta +1 -0
  38. data/demo/data/vcards/co/contact_7_davis.data +1 -0
  39. data/demo/data/vcards/co/contact_7_davis.meta +1 -0
  40. data/demo/data/vcards/co/contact_8_anderson.data +1 -0
  41. data/demo/data/vcards/co/contact_8_anderson.meta +1 -0
  42. data/demo/data/vcards/co/contact_9_taylor.data +1 -0
  43. data/demo/data/vcards/co/contact_9_taylor.meta +1 -0
  44. data/demo/data/vcards.db +0 -0
  45. data/demo/pottery_class_demo.rb +164 -0
  46. data/demo/vcard_models.rb +140 -0
  47. data/demo/vcard_store_demo.rb +526 -0
  48. data/lib/lutaml/store/adapter/base.rb +65 -0
  49. data/lib/lutaml/store/adapter/filesystem.rb +288 -0
  50. data/lib/lutaml/store/adapter/memory.rb +225 -0
  51. data/lib/lutaml/store/adapter/sqlite.rb +193 -0
  52. data/lib/lutaml/store/adapter.rb +12 -0
  53. data/lib/lutaml/store/attribute_updater.rb +198 -0
  54. data/lib/lutaml/store/basic_store.rb +190 -0
  55. data/lib/lutaml/store/cache.rb +108 -0
  56. data/lib/lutaml/store/cache_store.rb +282 -0
  57. data/lib/lutaml/store/composite_model_handler.rb +169 -0
  58. data/lib/lutaml/store/compression.rb +137 -0
  59. data/lib/lutaml/store/config.rb +178 -0
  60. data/lib/lutaml/store/database_store.rb +425 -0
  61. data/lib/lutaml/store/events.rb +92 -0
  62. data/lib/lutaml/store/format/base.rb +33 -0
  63. data/lib/lutaml/store/format/json.rb +25 -0
  64. data/lib/lutaml/store/format/jsonl.rb +37 -0
  65. data/lib/lutaml/store/format/marshal_format.rb +37 -0
  66. data/lib/lutaml/store/format/yaml.rb +29 -0
  67. data/lib/lutaml/store/format/yamls.rb +35 -0
  68. data/lib/lutaml/store/format.rb +33 -0
  69. data/lib/lutaml/store/http_cache.rb +279 -0
  70. data/lib/lutaml/store/http_cache_config.rb +53 -0
  71. data/lib/lutaml/store/http_cache_entry.rb +69 -0
  72. data/lib/lutaml/store/http_header_processor.rb +175 -0
  73. data/lib/lutaml/store/integrity.rb +102 -0
  74. data/lib/lutaml/store/model_registration.rb +75 -0
  75. data/lib/lutaml/store/model_registry.rb +123 -0
  76. data/lib/lutaml/store/model_serializer.rb +69 -0
  77. data/lib/lutaml/store/monitor.rb +192 -0
  78. data/lib/lutaml/store/storage_key.rb +40 -0
  79. data/lib/lutaml/store/version.rb +7 -0
  80. data/lib/lutaml/store.rb +41 -0
  81. data/lutaml-store.gemspec +35 -0
  82. data/plan.adoc +606 -0
  83. data/sig/lutaml/store.rbs +6 -0
  84. data/spec/lutaml/store/adapter_interface_spec.rb +89 -0
  85. data/spec/lutaml/store/anti_pattern_guard_spec.rb +35 -0
  86. data/spec/lutaml/store/anti_pattern_spec.rb +78 -0
  87. data/spec/lutaml/store/autoload_spec.rb +34 -0
  88. data/spec/lutaml/store/cache_store_spec.rb +271 -0
  89. data/spec/lutaml/store/compression_spec.rb +78 -0
  90. data/spec/lutaml/store/config_enhanced_spec.rb +158 -0
  91. data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +336 -0
  92. data/spec/lutaml/store/custom_serializer_spec.rb +108 -0
  93. data/spec/lutaml/store/database_store_spec.rb +279 -0
  94. data/spec/lutaml/store/file_io_spec.rb +219 -0
  95. data/spec/lutaml/store/format_round_trip_spec.rb +110 -0
  96. data/spec/lutaml/store/format_spec.rb +70 -0
  97. data/spec/lutaml/store/http_cache_entry_spec.rb +203 -0
  98. data/spec/lutaml/store/http_cache_hal_integration_spec.rb +404 -0
  99. data/spec/lutaml/store/http_cache_spec.rb +422 -0
  100. data/spec/lutaml/store/http_header_processor_spec.rb +290 -0
  101. data/spec/lutaml/store/import_spec.rb +90 -0
  102. data/spec/lutaml/store/integrity_spec.rb +157 -0
  103. data/spec/lutaml/store/key_collision_serializer_spec.rb +98 -0
  104. data/spec/lutaml/store/load_save_spec.rb +107 -0
  105. data/spec/lutaml/store/lutaml_model_integration_spec.rb +291 -0
  106. data/spec/lutaml/store/model_serializer_spec.rb +140 -0
  107. data/spec/lutaml/store/store_spec.rb +182 -0
  108. data/spec/lutaml/store_spec.rb +21 -0
  109. data/spec/spec_helper.rb +16 -0
  110. metadata +166 -0
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Store
5
+ module Format
6
+ class Base
7
+ def extension
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def glob_pattern
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def serialize(model)
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def deserialize(data, model_class)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def serialize_many(models)
24
+ models.map { |m| serialize(m) }.join
25
+ end
26
+
27
+ def deserialize_many(_data, _model_class)
28
+ raise NotImplementedError, "#{self.class} does not support multi-document deserialization"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Store
5
+ module Format
6
+ class Json < Base
7
+ def extension
8
+ ".json"
9
+ end
10
+
11
+ def glob_pattern
12
+ "*.json"
13
+ end
14
+
15
+ def serialize(model)
16
+ model.to_json
17
+ end
18
+
19
+ def deserialize(data, model_class)
20
+ model_class.from_json(data)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Store
5
+ module Format
6
+ class Jsonl < Base
7
+ def extension
8
+ ".jsonl"
9
+ end
10
+
11
+ def glob_pattern
12
+ "*.jsonl"
13
+ end
14
+
15
+ def serialize(model)
16
+ model.to_json
17
+ end
18
+
19
+ def serialize_many(models)
20
+ "#{models.map(&:to_json).join("\n")}\n"
21
+ end
22
+
23
+ def deserialize(data, model_class)
24
+ model_class.from_json(data)
25
+ end
26
+
27
+ def deserialize_many(data, model_class)
28
+ data.lines.filter_map do |line|
29
+ next if line.strip.empty?
30
+
31
+ model_class.from_json(line)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Store
5
+ module Format
6
+ class MarshalFormat < Base
7
+ def extension
8
+ ".bin"
9
+ end
10
+
11
+ def glob_pattern
12
+ "*.bin"
13
+ end
14
+
15
+ def serialize(model)
16
+ hash_data = model.to_hash
17
+ ::Marshal.dump(hash_data)
18
+ end
19
+
20
+ def deserialize(data, model_class)
21
+ hash_data = ::Marshal.load(data)
22
+ model_class.from_hash(hash_data)
23
+ end
24
+
25
+ def serialize_many(models)
26
+ hash_array = models.map(&:to_hash)
27
+ ::Marshal.dump(hash_array)
28
+ end
29
+
30
+ def deserialize_many(data, model_class)
31
+ hash_array = ::Marshal.load(data)
32
+ hash_array.map { |h| model_class.from_hash(h) }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Store
5
+ module Format
6
+ class Yaml < Base
7
+ def extension
8
+ ".yaml"
9
+ end
10
+
11
+ def glob_pattern
12
+ "*.{yaml,yml}"
13
+ end
14
+
15
+ def serialize(model)
16
+ model.to_yaml
17
+ end
18
+
19
+ def deserialize(data, model_class)
20
+ model_class.from_yaml(data)
21
+ end
22
+
23
+ def deserialize_many(data, model_class)
24
+ model_class.from_yamls(data)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Store
5
+ module Format
6
+ class Yamls < Base
7
+ def extension
8
+ ".yaml"
9
+ end
10
+
11
+ def glob_pattern
12
+ "*.{yaml,yml}"
13
+ end
14
+
15
+ def serialize(model)
16
+ model.to_yaml
17
+ end
18
+
19
+ def serialize_many(models)
20
+ models.map(&:to_yaml).join
21
+ end
22
+
23
+ def deserialize(data, model_class)
24
+ docs = model_class.from_yamls(data)
25
+ docs.is_a?(Array) ? docs.first : docs
26
+ end
27
+
28
+ def deserialize_many(data, model_class)
29
+ result = model_class.from_yamls(data)
30
+ result.is_a?(Array) ? result : [result]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Store
5
+ module Format
6
+ class Error < Lutaml::Store::Error; end
7
+ class FormatError < Error; end
8
+ class UnsupportedFormatError < FormatError; end
9
+
10
+ autoload :Base, "lutaml/store/format/base"
11
+ autoload :Yaml, "lutaml/store/format/yaml"
12
+ autoload :Yamls, "lutaml/store/format/yamls"
13
+ autoload :Json, "lutaml/store/format/json"
14
+ autoload :Jsonl, "lutaml/store/format/jsonl"
15
+ autoload :MarshalFormat, "lutaml/store/format/marshal_format"
16
+
17
+ FORMATS = {
18
+ yaml: "Yaml",
19
+ yamls: "Yamls",
20
+ json: "Json",
21
+ jsonl: "Jsonl",
22
+ marshal: "MarshalFormat"
23
+ }.freeze
24
+
25
+ def self.resolve(format)
26
+ entry = FORMATS[format.to_sym]
27
+ raise UnsupportedFormatError, "Unknown format: #{format}" unless entry
28
+
29
+ const_get(entry).new
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Lutaml
6
+ module Store
7
+ class HttpCache
8
+ def initialize(config)
9
+ @config = config.is_a?(HttpCacheConfig) ? config : HttpCacheConfig.new(config)
10
+ @config.validate!
11
+ @adapter = create_adapter
12
+ @stats = {
13
+ cache_hits: 0,
14
+ cache_misses: 0,
15
+ conditional_requests: 0,
16
+ not_modified_responses: 0,
17
+ entries_stored: 0,
18
+ entries_evicted: 0
19
+ }
20
+ end
21
+
22
+ # Main cache interface - fetch with block for cache miss
23
+ def fetch(method, url, headers = {}, &block)
24
+ raise ArgumentError, "Block required for cache miss handling" unless block_given?
25
+
26
+ # Generate cache key considering vary headers
27
+ vary_headers = extract_request_vary_headers(headers)
28
+ cache_key = HttpHeaderProcessor.generate_cache_key(
29
+ method,
30
+ url,
31
+ vary_headers,
32
+ @config.ignore_query_params
33
+ )
34
+
35
+ prefixed_key = @config.cache_key_for(cache_key)
36
+ entry = get_entry(prefixed_key)
37
+
38
+ # Check if entry matches current request (Vary header consideration)
39
+ if entry && cache_entry_matches?(entry, headers)
40
+ if entry.fresh?
41
+ @stats[:cache_hits] += 1
42
+ return create_response_from_entry(entry)
43
+ elsif entry.stale? && @config.enable_conditional_requests
44
+ # Try conditional request
45
+ @stats[:conditional_requests] += 1
46
+ return handle_conditional_request(entry, headers, prefixed_key, &block)
47
+ end
48
+ end
49
+
50
+ # Cache miss or unusable entry - make fresh request
51
+ @stats[:cache_misses] += 1
52
+ response = yield(headers)
53
+ cache_response(prefixed_key, method, url, headers, response)
54
+ end
55
+
56
+ # Get cached entry without making requests
57
+ def get(method, url, headers = {})
58
+ vary_headers = extract_request_vary_headers(headers)
59
+ cache_key = HttpHeaderProcessor.generate_cache_key(
60
+ method,
61
+ url,
62
+ vary_headers,
63
+ @config.ignore_query_params
64
+ )
65
+
66
+ prefixed_key = @config.cache_key_for(cache_key)
67
+ entry = get_entry(prefixed_key)
68
+
69
+ return nil unless entry
70
+ return nil unless cache_entry_matches?(entry, headers)
71
+ return nil unless entry.fresh?
72
+
73
+ create_response_from_entry(entry)
74
+ end
75
+
76
+ # Store response in cache
77
+ def set(method, url, headers, response)
78
+ return response unless should_cache_response?(response)
79
+
80
+ vary_headers = extract_request_vary_headers(headers)
81
+ cache_key = HttpHeaderProcessor.generate_cache_key(
82
+ method,
83
+ url,
84
+ vary_headers,
85
+ @config.ignore_query_params
86
+ )
87
+
88
+ prefixed_key = @config.cache_key_for(cache_key)
89
+ cache_response(prefixed_key, method, url, headers, response)
90
+ end
91
+
92
+ # Delete cached entry
93
+ def delete(method, url, headers = {})
94
+ vary_headers = extract_request_vary_headers(headers)
95
+ cache_key = HttpHeaderProcessor.generate_cache_key(
96
+ method,
97
+ url,
98
+ vary_headers,
99
+ @config.ignore_query_params
100
+ )
101
+
102
+ prefixed_key = @config.cache_key_for(cache_key)
103
+ @adapter.delete(prefixed_key)
104
+ rescue StandardError
105
+ # Log error but don't fail
106
+ false
107
+ end
108
+
109
+ # Clear all cache entries
110
+ def clear
111
+ @adapter.clear
112
+ rescue StandardError
113
+ false
114
+ end
115
+
116
+ # Get cache statistics
117
+ def stats
118
+ total_requests = @stats[:cache_hits] + @stats[:cache_misses]
119
+ hit_ratio = total_requests.positive? ? (@stats[:cache_hits].to_f / total_requests * 100) : 0
120
+
121
+ {
122
+ adapter_type: @config.adapter_type,
123
+ total_entries: @adapter.size,
124
+ cache_hits: @stats[:cache_hits],
125
+ cache_misses: @stats[:cache_misses],
126
+ conditional_requests: @stats[:conditional_requests],
127
+ not_modified_responses: @stats[:not_modified_responses],
128
+ entries_stored: @stats[:entries_stored],
129
+ entries_evicted: @stats[:entries_evicted],
130
+ hit_ratio: hit_ratio,
131
+ total_requests: total_requests,
132
+ config: {
133
+ default_ttl: @config.default_ttl,
134
+ max_entries: @config.max_entries,
135
+ respect_http_headers: @config.respect_http_headers,
136
+ enable_conditional_requests: @config.enable_conditional_requests
137
+ }
138
+ }
139
+ end
140
+
141
+ # Get all cache entries for inspection
142
+ def all_entries
143
+ entries = []
144
+ @adapter.each_key do |key|
145
+ data = @adapter.get(key)
146
+ next unless data
147
+
148
+ entry_data = data.is_a?(String) ? JSON.parse(data) : data
149
+ entry = HttpCacheEntry.from_hash(entry_data)
150
+ entries << entry
151
+ rescue StandardError
152
+ # Skip invalid entries
153
+ end
154
+ entries
155
+ end
156
+
157
+ private
158
+
159
+ def create_adapter
160
+ adapter_config = @config.to_adapter_config
161
+ case adapter_config[:type]
162
+ when :memory
163
+ Adapter::Memory.new(adapter_config)
164
+ when :filesystem
165
+ Adapter::FileSystem.new(adapter_config)
166
+ when :sqlite
167
+ Adapter::Sqlite.new(adapter_config)
168
+ else
169
+ raise ArgumentError, "Unknown adapter type: #{adapter_config[:type]}"
170
+ end
171
+ end
172
+
173
+ def get_entry(cache_key)
174
+ data = @adapter.get(cache_key)
175
+ return nil unless data
176
+
177
+ # Handle both serialized and hash data
178
+ entry_data = data.is_a?(String) ? JSON.parse(data) : data
179
+ HttpCacheEntry.from_hash(entry_data)
180
+ rescue StandardError
181
+ # Log error and continue without cache
182
+ nil
183
+ end
184
+
185
+ def store_entry(cache_key, entry)
186
+ # Serialize entry for storage
187
+ data = entry.to_hash
188
+ @adapter.set(cache_key, data)
189
+ rescue StandardError
190
+ # Log error but don't fail the request
191
+ false
192
+ end
193
+
194
+ def handle_conditional_request(entry, headers, cache_key, &block)
195
+ # Build conditional headers
196
+ conditional_headers = HttpHeaderProcessor.build_conditional_headers(entry, headers)
197
+ response = yield(conditional_headers)
198
+
199
+ if response[:status_code] == 304
200
+ # Not modified - refresh cache timestamp and return cached content
201
+ entry.cached_at = Time.now
202
+ store_entry(cache_key, entry)
203
+ create_response_from_entry(entry)
204
+ else
205
+ # Modified - cache new response
206
+ cache_response(cache_key, entry.method, entry.url, headers, response)
207
+ end
208
+ end
209
+
210
+ def cache_response(cache_key, method, url, request_headers, response)
211
+ if should_cache_response?(response)
212
+ entry = create_cache_entry(cache_key, method, url, request_headers, response)
213
+ @stats[:entries_stored] += 1 if store_entry(cache_key, entry)
214
+ end
215
+
216
+ response
217
+ end
218
+
219
+ def should_cache_response?(response)
220
+ return false unless @config.respect_http_headers
221
+ return false unless response[:status_code]
222
+
223
+ HttpHeaderProcessor.should_cache_response?(response[:status_code], response[:headers] || {})
224
+ end
225
+
226
+ def create_cache_entry(cache_key, method, url, request_headers, response)
227
+ cached_at = Time.now
228
+ response_headers = response[:headers] || {}
229
+
230
+ # Parse cache control and calculate expiry
231
+ cache_control = HttpHeaderProcessor.parse_cache_control(response_headers["cache-control"])
232
+ expires_at = HttpHeaderProcessor.calculate_expiry(response_headers, cached_at, @config.default_ttl)
233
+
234
+ # Parse vary headers
235
+ vary_headers = HttpHeaderProcessor.parse_vary_header(response_headers["vary"])
236
+
237
+ # Extract vary header values from request
238
+ request_vary_headers = {}
239
+ request_vary_headers = HttpHeaderProcessor.extract_vary_headers(request_headers, vary_headers) if vary_headers.any?
240
+
241
+ HttpCacheEntry.new(
242
+ cache_key: cache_key,
243
+ url: url,
244
+ method: method,
245
+ request_headers: request_vary_headers, # Store only vary-relevant headers
246
+ response_body: response[:body] || "",
247
+ response_headers: response_headers,
248
+ status_code: response[:status_code],
249
+ cached_at: cached_at,
250
+ etag: response_headers["etag"],
251
+ last_modified: HttpHeaderProcessor.parse_last_modified(response_headers["last-modified"]),
252
+ expires_at: expires_at,
253
+ max_age: cache_control["max-age"],
254
+ cache_control: cache_control,
255
+ vary_headers: vary_headers
256
+ )
257
+ end
258
+
259
+ def create_response_from_entry(entry)
260
+ {
261
+ status_code: entry.status_code,
262
+ headers: entry.response_headers,
263
+ body: entry.response_body
264
+ }
265
+ end
266
+
267
+ def extract_request_vary_headers(_headers)
268
+ # For now, return empty hash - will be populated when we know vary headers
269
+ {}
270
+ end
271
+
272
+ def cache_entry_matches?(entry, request_headers)
273
+ return true if entry.vary_headers.empty?
274
+
275
+ HttpHeaderProcessor.cache_entry_matches?(entry, request_headers, @config)
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Lutaml
6
+ module Store
7
+ class HttpCacheConfig
8
+ include Lutaml::Model::Serialize
9
+
10
+ attribute :adapter_type, :string, default: "filesystem"
11
+ attribute :adapter_options, :hash, default: {}
12
+ attribute :default_ttl, :integer, default: 3600
13
+ attribute :max_entries, :integer, default: 10_000
14
+ attribute :respect_http_headers, :boolean, default: true
15
+ attribute :enable_conditional_requests, :boolean, default: true
16
+ attribute :enable_compression, :boolean, default: false
17
+ attribute :cache_private_responses, :boolean, default: false
18
+ attribute :ignore_query_params, :string, collection: true, default: []
19
+ attribute :vary_ignore_headers, :string, collection: true, default: []
20
+ attribute :cache_key_prefix, :string, default: "http_cache"
21
+
22
+ def to_adapter_config
23
+ base_config = {
24
+ type: adapter_type.to_sym,
25
+ **adapter_options.transform_keys(&:to_sym)
26
+ }
27
+
28
+ # Add compression if enabled
29
+ base_config[:compression] = true if enable_compression
30
+
31
+ base_config
32
+ end
33
+
34
+ def cache_key_for(key)
35
+ "#{cache_key_prefix}:#{key}"
36
+ end
37
+
38
+ def should_ignore_query_param?(param)
39
+ ignore_query_params.include?(param.to_s)
40
+ end
41
+
42
+ def should_ignore_vary_header?(header)
43
+ vary_ignore_headers.map(&:downcase).include?(header.to_s.downcase)
44
+ end
45
+
46
+ def validate!
47
+ raise ArgumentError, "default_ttl must be positive" if default_ttl <= 0
48
+ raise ArgumentError, "max_entries must be positive" if max_entries <= 0
49
+ raise ArgumentError, "adapter_type cannot be empty" if adapter_type.nil? || adapter_type.empty?
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require "time"
5
+ require "digest"
6
+
7
+ module Lutaml
8
+ module Store
9
+ class HttpCacheEntry
10
+ include Lutaml::Model::Serialize
11
+
12
+ attribute :cache_key, :string
13
+ attribute :url, :string
14
+ attribute :method, :string, default: "GET"
15
+ attribute :request_headers, :hash, default: {}
16
+ attribute :response_body, :string
17
+ attribute :response_headers, :hash, default: {}
18
+ attribute :status_code, :integer
19
+ attribute :cached_at, :time
20
+ attribute :etag, :string
21
+ attribute :last_modified, :time
22
+ attribute :expires_at, :time
23
+ attribute :max_age, :integer
24
+ attribute :cache_control, :hash, default: {}
25
+ attribute :vary_headers, :string, collection: true, default: []
26
+
27
+ def fresh?
28
+ return false if expired?
29
+ return false if must_revalidate?
30
+
31
+ true
32
+ end
33
+
34
+ def expired?
35
+ return true if expires_at && Time.now > expires_at
36
+ return true if max_age && (Time.now - cached_at) > max_age
37
+
38
+ false
39
+ end
40
+
41
+ def must_revalidate?
42
+ !!(cache_control["must-revalidate"] || cache_control["no-cache"])
43
+ end
44
+
45
+ def stale?
46
+ !fresh?
47
+ end
48
+
49
+ def cacheable?
50
+ return false if status_code < 200 || status_code >= 400
51
+ return false if cache_control["no-store"]
52
+ return false if cache_control["private"]
53
+
54
+ true
55
+ end
56
+
57
+ def age
58
+ Time.now - cached_at
59
+ end
60
+
61
+ def remaining_ttl
62
+ return 0 if expired?
63
+ return Float::INFINITY unless expires_at
64
+
65
+ expires_at - Time.now
66
+ end
67
+ end
68
+ end
69
+ end