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,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/store/http_cache"
5
+
6
+ # Mock W3C API components for testing
7
+ module MockW3cApi
8
+ class MockClient
9
+ def get(url)
10
+ { "data" => "test response", "url" => url }
11
+ end
12
+
13
+ def get_with_headers(url, headers)
14
+ response = { "data" => "test response with headers", "url" => url }
15
+ # Simulate ETag header
16
+ response["etag"] = '"test-etag-123"' if headers["If-None-Match"]
17
+ response
18
+ end
19
+
20
+ def get_by_url(url)
21
+ { "data" => "test response by url", "url" => url }
22
+ end
23
+
24
+ def api_url
25
+ "https://api.w3.org"
26
+ end
27
+ end
28
+
29
+ class MockModel
30
+ def self.from_json(json)
31
+ data = JSON.parse(json)
32
+ new(data)
33
+ end
34
+
35
+ def initialize(data)
36
+ @data = data
37
+ end
38
+
39
+ attr_reader :data
40
+
41
+ def to_json(*_args)
42
+ @data.to_json
43
+ end
44
+ end
45
+
46
+ class MockParameter
47
+ attr_reader :name, :location, :required, :default_value
48
+
49
+ def initialize(name, location: :query, required: false, default_value: nil)
50
+ @name = name.to_s
51
+ @location = location
52
+ @required = required
53
+ @default_value = default_value
54
+ end
55
+
56
+ def validate!
57
+ raise ArgumentError, "Parameter name cannot be empty" if @name.nil? || @name.empty?
58
+ end
59
+
60
+ def path_parameter?
61
+ @location == :path
62
+ end
63
+
64
+ def query_parameter?
65
+ @location == :query
66
+ end
67
+
68
+ def validate_value(value)
69
+ !value.nil?
70
+ end
71
+ end
72
+ end
73
+
74
+ RSpec.describe "Corrected HTTP Cache Integration", skip: "WIP: HTTP cache integration not yet implemented" do
75
+ let(:cache_config) do
76
+ {
77
+ adapter_type: :memory,
78
+ default_ttl: 3600,
79
+ respect_http_headers: true,
80
+ enable_conditional_requests: true
81
+ }
82
+ end
83
+
84
+ let(:cache) { Lutaml::Store::HttpCache.new(cache_config) }
85
+ let(:client) { MockW3cApi::MockClient.new }
86
+ let(:register) { Lutaml::Hal::ModelRegister.new(name: :test, client: client, cache: cache_config) }
87
+
88
+ before do
89
+ # Configure the register with HTTP cache
90
+ register.cache_store = cache
91
+ end
92
+
93
+ describe "transparent cache integration" do
94
+ it "integrates cache at ModelRegister.fetch() level" do
95
+ # Add a test endpoint
96
+ register.add_endpoint(
97
+ id: :test_endpoint,
98
+ type: :index,
99
+ url: "/test",
100
+ model: MockW3cApi::MockModel,
101
+ parameters: []
102
+ )
103
+
104
+ # First request should hit the API
105
+ expect(client).to receive(:get).with("https://api.w3.org/test").and_call_original
106
+ result1 = register.fetch(:test_endpoint)
107
+
108
+ # Second request should hit the cache
109
+ expect(client).not_to receive(:get)
110
+ result2 = register.fetch(:test_endpoint)
111
+
112
+ # Results should be equivalent
113
+ expect(result1.data["url"]).to eq(result2.data["url"])
114
+ end
115
+
116
+ it "supports HTTP-aware caching with conditional requests" do
117
+ # Add a test endpoint
118
+ register.add_endpoint(
119
+ id: :test_conditional,
120
+ type: :index,
121
+ url: "/test-conditional",
122
+ model: MockW3cApi::MockModel,
123
+ parameters: []
124
+ )
125
+
126
+ # First request
127
+ result1 = register.fetch(:test_conditional)
128
+ expect(result1).to be_a(MockW3cApi::MockModel)
129
+
130
+ # Second request should use conditional headers
131
+ result2 = register.fetch(:test_conditional)
132
+ expect(result2).to be_a(MockW3cApi::MockModel)
133
+ end
134
+
135
+ it "handles query parameters correctly" do
136
+ # Add endpoint with query parameters
137
+ register.add_endpoint(
138
+ id: :test_with_params,
139
+ type: :index,
140
+ url: "/test-params",
141
+ model: MockW3cApi::MockModel,
142
+ parameters: [
143
+ MockW3cApi::MockParameter.new("page", location: :query),
144
+ MockW3cApi::MockParameter.new("items", location: :query)
145
+ ]
146
+ )
147
+
148
+ # Test with different parameters
149
+ result1 = register.fetch(:test_with_params, page: 1, items: 10)
150
+ result2 = register.fetch(:test_with_params, page: 2, items: 10)
151
+ result3 = register.fetch(:test_with_params, page: 1, items: 10) # Should hit cache
152
+
153
+ expect(result1.data["url"]).to include("page=1")
154
+ expect(result2.data["url"]).to include("page=2")
155
+ expect(result3.data["url"]).to eq(result1.data["url"])
156
+ end
157
+
158
+ it "handles path parameters correctly" do
159
+ # Add endpoint with path parameters
160
+ register.add_endpoint(
161
+ id: :test_with_path,
162
+ type: :resource,
163
+ url: "/test/{id}",
164
+ model: MockW3cApi::MockModel,
165
+ parameters: [
166
+ MockW3cApi::MockParameter.new("id", location: :path)
167
+ ]
168
+ )
169
+
170
+ result = register.fetch(:test_with_path, id: "test-123")
171
+ expect(result.data["url"]).to include("/test/test-123")
172
+ end
173
+ end
174
+
175
+ describe "cache configuration methods" do
176
+ let(:mock_hal) do
177
+ hal = double("Hal")
178
+ mock_register = double("Register")
179
+ allow(hal).to receive(:register).and_return(mock_register)
180
+ allow(mock_register).to receive(:cache_store=)
181
+ allow(mock_register).to receive(:cache_info).and_return({
182
+ adapter_type: "Memory",
183
+ current_size: 5,
184
+ default_ttl: 3600
185
+ })
186
+ allow(mock_register).to receive(:clear_cache)
187
+ allow(mock_register).to receive(:cache_stats).and_return({
188
+ hits: 10,
189
+ misses: 5,
190
+ hit_rate: 0.67
191
+ })
192
+ hal
193
+ end
194
+
195
+ it "provides configure_cache method" do
196
+ expect(mock_hal.register).to receive(:cache_store=).with(cache)
197
+
198
+ # Simulate the configure_cache method
199
+ mock_hal.register.cache_store = cache
200
+ end
201
+
202
+ it "provides cache_info method" do
203
+ info = mock_hal.register.cache_info
204
+ expect(info).to include(:adapter_type, :current_size, :default_ttl)
205
+ end
206
+
207
+ it "provides clear_cache method" do
208
+ expect(mock_hal.register).to receive(:clear_cache)
209
+ mock_hal.register.clear_cache
210
+ end
211
+
212
+ it "provides cache_stats method" do
213
+ stats = mock_hal.register.cache_stats
214
+ expect(stats).to include(:hits, :misses, :hit_rate)
215
+ end
216
+ end
217
+
218
+ describe "HTTP semantics preservation" do
219
+ it "respects HTTP headers" do
220
+ expect(cache.config.respect_http_headers).to be true
221
+ end
222
+
223
+ it "enables conditional requests" do
224
+ expect(cache.config.enable_conditional_requests).to be true
225
+ end
226
+
227
+ it "handles ETags correctly" do
228
+ # This would be tested with actual HTTP responses
229
+ # For now, we verify the cache is configured to handle them
230
+ expect(cache.config.respect_http_headers).to be true
231
+ end
232
+ end
233
+
234
+ describe "zero API changes requirement" do
235
+ it "maintains existing ModelRegister interface" do
236
+ # Verify that cache integration doesn't break existing interface
237
+ expect(register).to respond_to(:fetch)
238
+ expect(register).to respond_to(:add_endpoint)
239
+ expect(register).to respond_to(:models)
240
+ expect(register).to respond_to(:client)
241
+ end
242
+
243
+ it "supports optional cache configuration" do
244
+ # Register should work without cache
245
+ no_cache_register = Lutaml::Hal::ModelRegister.new(name: :no_cache, client: client)
246
+ expect(no_cache_register.cache_store).to be_nil
247
+
248
+ # And with cache
249
+ cached_register = Lutaml::Hal::ModelRegister.new(name: :cached, client: client, cache: cache_config)
250
+ expect(cached_register.cache_store).not_to be_nil
251
+ end
252
+ end
253
+
254
+ describe "architectural correctness" do
255
+ it "integrates at the correct layer (ModelRegister.fetch)" do
256
+ # Verify that cache is checked in fetch method
257
+ register.add_endpoint(
258
+ id: :arch_test,
259
+ type: :index,
260
+ url: "/arch-test",
261
+ model: MockW3cApi::MockModel,
262
+ parameters: []
263
+ )
264
+
265
+ # Mock the cache to verify it's being used
266
+ expect(cache).to receive(:fetch).and_call_original
267
+ register.fetch(:arch_test)
268
+ end
269
+
270
+ it "does not bypass w3c_api gem architecture" do
271
+ # This test ensures we're not creating direct HTTP clients
272
+ # Instead, we use the existing client through ModelRegister
273
+ expect(register.client).to be_a(MockW3cApi::MockClient)
274
+ expect(register.client).to respond_to(:get)
275
+ expect(register.client).to respond_to(:get_with_headers)
276
+ end
277
+ end
278
+
279
+ describe "performance benefits" do
280
+ it "provides measurable cache improvements" do
281
+ register.add_endpoint(
282
+ id: :perf_test,
283
+ type: :index,
284
+ url: "/perf-test",
285
+ model: MockW3cApi::MockModel,
286
+ parameters: []
287
+ )
288
+
289
+ # First request (cache miss)
290
+ start_time = Time.now
291
+ register.fetch(:perf_test)
292
+ first_duration = Time.now - start_time
293
+
294
+ # Second request (cache hit)
295
+ start_time = Time.now
296
+ register.fetch(:perf_test)
297
+ second_duration = Time.now - start_time
298
+
299
+ # Cache hit should be faster (though in memory it might be negligible)
300
+ expect(second_duration).to be <= first_duration
301
+ end
302
+ end
303
+
304
+ describe "error handling" do
305
+ it "handles cache failures gracefully" do
306
+ # Mock cache failure
307
+ allow(cache).to receive(:fetch).and_raise(StandardError, "Cache error")
308
+
309
+ register.add_endpoint(
310
+ id: :error_test,
311
+ type: :index,
312
+ url: "/error-test",
313
+ model: MockW3cApi::MockModel,
314
+ parameters: []
315
+ )
316
+
317
+ # Should fall back to direct API call
318
+ expect { register.fetch(:error_test) }.not_to raise_error
319
+ end
320
+
321
+ it "handles missing cache gracefully" do
322
+ no_cache_register = Lutaml::Hal::ModelRegister.new(name: :no_cache, client: client)
323
+
324
+ no_cache_register.add_endpoint(
325
+ id: :no_cache_test,
326
+ type: :index,
327
+ url: "/no-cache-test",
328
+ model: MockW3cApi::MockModel,
329
+ parameters: []
330
+ )
331
+
332
+ # Should work without cache
333
+ expect { no_cache_register.fetch(:no_cache_test) }.not_to raise_error
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Lutaml::Store, "custom serializer" do
6
+ let(:custom_serializer) do
7
+ Class.new do
8
+ def serialize(model)
9
+ { "identifier" => model.identifier, "value" => model.value }
10
+ end
11
+
12
+ def deserialize(data, _model_class)
13
+ model = TestCustomModel.new
14
+ model.identifier = data["identifier"]
15
+ model.value = data["value"]
16
+ model
17
+ end
18
+ end.new
19
+ end
20
+
21
+ before do
22
+ stub_const("TestCustomModel", Class.new(Lutaml::Model::Serializable) do
23
+ attribute :identifier, :string
24
+ attribute :value, :string
25
+ attribute :computed, :string
26
+
27
+ def computed
28
+ "#{identifier}-#{value}"
29
+ end
30
+ end)
31
+ end
32
+
33
+ it "uses custom serializer for save and fetch" do
34
+ store = described_class.new(
35
+ adapter: :memory,
36
+ models: [
37
+ { model: TestCustomModel, key: :identifier, serializer: custom_serializer }
38
+ ]
39
+ )
40
+
41
+ model = TestCustomModel.new
42
+ model.identifier = "abc"
43
+ model.value = "hello"
44
+
45
+ store.save(model)
46
+
47
+ fetched = store.fetch(model: TestCustomModel, identifier: "abc")
48
+ expect(fetched.identifier).to eq("abc")
49
+ expect(fetched.value).to eq("hello")
50
+ expect(fetched.computed).to eq("abc-hello")
51
+ end
52
+
53
+ it "uses custom serializer for all" do
54
+ store = described_class.new(
55
+ adapter: :memory,
56
+ models: [
57
+ { model: TestCustomModel, key: :identifier, serializer: custom_serializer }
58
+ ]
59
+ )
60
+
61
+ 3.times do |i|
62
+ model = TestCustomModel.new
63
+ model.identifier = "item-#{i}"
64
+ model.value = "val-#{i}"
65
+ store.save(model)
66
+ end
67
+
68
+ all_items = store.all(model: TestCustomModel)
69
+ expect(all_items.size).to eq(3)
70
+ expect(all_items.map(&:value).sort).to eq(%w[val-0 val-1 val-2])
71
+ end
72
+
73
+ it "uses custom serializer for update" do
74
+ store = described_class.new(
75
+ adapter: :memory,
76
+ models: [
77
+ { model: TestCustomModel, key: :identifier, serializer: custom_serializer }
78
+ ]
79
+ )
80
+
81
+ model = TestCustomModel.new
82
+ model.identifier = "xyz"
83
+ model.value = "original"
84
+ store.save(model)
85
+
86
+ updated = store.update(
87
+ model: TestCustomModel,
88
+ identifier: "xyz",
89
+ attributes: { "value" => "updated" }
90
+ )
91
+ expect(updated.value).to eq("updated")
92
+ end
93
+
94
+ it "falls back to default serialization when no custom serializer" do
95
+ store = described_class.new(
96
+ adapter: :memory,
97
+ models: [{ model: TestCustomModel, key: :identifier }]
98
+ )
99
+
100
+ model = TestCustomModel.new
101
+ model.identifier = "default"
102
+ model.value = "normal"
103
+ store.save(model)
104
+
105
+ fetched = store.fetch(model: TestCustomModel, identifier: "default")
106
+ expect(fetched.identifier).to eq("default")
107
+ end
108
+ end