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,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tmpdir"
5
+
6
+ module IntegrationTestModels
7
+ class TestDocument < Lutaml::Model::Serializable
8
+ attribute :id, :string
9
+ attribute :title, :string
10
+ attribute :content, :string
11
+ attribute :created_at, :string
12
+
13
+ key_value do
14
+ map :id, to: :id
15
+ map :title, to: :title
16
+ map :content, to: :content
17
+ map :created_at, to: :created_at
18
+ end
19
+ end
20
+
21
+ class TestAuthor < Lutaml::Model::Serializable
22
+ attribute :name, :string
23
+ attribute :email, :string
24
+ attribute :bio, :string
25
+
26
+ key_value do
27
+ map :name, to: :name
28
+ map :email, to: :email
29
+ map :bio, to: :bio
30
+ end
31
+ end
32
+
33
+ class TestBook < Lutaml::Model::Serializable
34
+ attribute :isbn, :string
35
+ attribute :title, :string
36
+ attribute :author, :string
37
+ attribute :published_year, :integer
38
+ attribute :genre, :string
39
+
40
+ key_value do
41
+ map :isbn, to: :isbn
42
+ map :title, to: :title
43
+ map :author, to: :author
44
+ map :published_year, to: :published_year
45
+ map :genre, to: :genre
46
+ end
47
+ end
48
+ end
49
+
50
+ RSpec.describe "Lutaml::Store with Lutaml::Model::Serializable integration" do
51
+ let(:doc_model) { { model: IntegrationTestModels::TestDocument, key: :id, dir: "documents" } }
52
+ let(:author_model) { { model: IntegrationTestModels::TestAuthor, key: :name, dir: "authors" } }
53
+ let(:book_model) { { model: IntegrationTestModels::TestBook, key: :isbn, dir: "books" } }
54
+
55
+ describe "single model CRUD" do
56
+ let(:store) { Lutaml::Store.new(adapter: :memory, models: [doc_model]) }
57
+ let(:document) do
58
+ IntegrationTestModels::TestDocument.new(
59
+ id: "doc1", title: "Test Document",
60
+ content: "This is a test", created_at: "2024-01-01"
61
+ )
62
+ end
63
+
64
+ it "saves and fetches a Lutaml::Model::Serializable instance" do
65
+ store.save(document)
66
+
67
+ retrieved = store.fetch(model: IntegrationTestModels::TestDocument, id: "doc1")
68
+ expect(retrieved).to be_a(IntegrationTestModels::TestDocument)
69
+ expect(retrieved.id).to eq("doc1")
70
+ expect(retrieved.title).to eq("Test Document")
71
+ expect(retrieved.content).to eq("This is a test")
72
+ expect(retrieved.created_at).to eq("2024-01-01")
73
+ end
74
+
75
+ it "returns nil for non-existent key" do
76
+ expect(store.fetch(model: IntegrationTestModels::TestDocument, id: "missing")).to be_nil
77
+ end
78
+
79
+ it "updates a model with hash attributes" do
80
+ store.save(document)
81
+
82
+ updated = store.update(
83
+ model: IntegrationTestModels::TestDocument,
84
+ id: "doc1",
85
+ attributes: { title: "Updated Title" }
86
+ )
87
+ expect(updated.title).to eq("Updated Title")
88
+ expect(updated.content).to eq("This is a test")
89
+ end
90
+
91
+ it "updates a model with block" do
92
+ store.save(document)
93
+
94
+ updated = store.update(model: IntegrationTestModels::TestDocument, id: "doc1") do |model|
95
+ model.content = "New content"
96
+ model
97
+ end
98
+ expect(updated.content).to eq("New content")
99
+ end
100
+
101
+ it "deletes a model" do
102
+ store.save(document)
103
+ expect(store.exists?(model: IntegrationTestModels::TestDocument, id: "doc1")).to be true
104
+
105
+ result = store.destroy(model: IntegrationTestModels::TestDocument, id: "doc1")
106
+ expect(result).to be true
107
+ expect(store.fetch(model: IntegrationTestModels::TestDocument, id: "doc1")).to be_nil
108
+ end
109
+
110
+ it "reports count and exists" do
111
+ expect(store.count(model: IntegrationTestModels::TestDocument)).to eq(0)
112
+
113
+ store.save(document)
114
+ expect(store.count(model: IntegrationTestModels::TestDocument)).to eq(1)
115
+ expect(store.exists?(model: IntegrationTestModels::TestDocument, id: "doc1")).to be true
116
+ end
117
+ end
118
+
119
+ describe "model type validation" do
120
+ let(:store) { Lutaml::Store.new(adapter: :memory, models: [doc_model]) }
121
+
122
+ it "raises when saving an unregistered model" do
123
+ author = IntegrationTestModels::TestAuthor.new(name: "John Doe", email: "john@example.com")
124
+
125
+ expect { store.save(author) }.to raise_error(Lutaml::Store::ModelNotRegisteredError)
126
+ end
127
+ end
128
+
129
+ describe "collection operations" do
130
+ let(:store) { Lutaml::Store.new(adapter: :memory, models: [author_model]) }
131
+ let(:authors) do
132
+ [
133
+ IntegrationTestModels::TestAuthor.new(name: "Alice Johnson", email: "alice@example.com", bio: "Fiction writer"),
134
+ IntegrationTestModels::TestAuthor.new(name: "Bob Wilson", email: "bob@example.com", bio: "Technical writer"),
135
+ IntegrationTestModels::TestAuthor.new(name: "Carol Davis", email: "carol@example.com", bio: "Science writer")
136
+ ]
137
+ end
138
+
139
+ it "stores multiple models and retrieves all" do
140
+ authors.each { |a| store.save(a) }
141
+
142
+ all_authors = store.all(model: IntegrationTestModels::TestAuthor)
143
+ expect(all_authors.size).to eq(3)
144
+ expect(all_authors.map(&:name).sort).to eq(["Alice Johnson", "Bob Wilson", "Carol Davis"])
145
+ end
146
+
147
+ it "queries with where" do
148
+ authors.each { |a| store.save(a) }
149
+
150
+ fiction = store.where(model: IntegrationTestModels::TestAuthor, bio: "Fiction writer")
151
+ expect(fiction.size).to eq(1)
152
+ expect(fiction.first.name).to eq("Alice Johnson")
153
+ end
154
+
155
+ it "updates a model and persists the change" do
156
+ authors.each { |a| store.save(a) }
157
+
158
+ store.update(model: IntegrationTestModels::TestAuthor, name: "Alice Johnson") do |model|
159
+ model.bio = "Updated bio: Fiction and mystery writer"
160
+ model
161
+ end
162
+
163
+ retrieved = store.fetch(model: IntegrationTestModels::TestAuthor, name: "Alice Johnson")
164
+ expect(retrieved.bio).to eq("Updated bio: Fiction and mystery writer")
165
+ end
166
+ end
167
+
168
+ describe "multi-model store" do
169
+ let(:store) { Lutaml::Store.new(adapter: :memory, models: [doc_model, author_model, book_model]) }
170
+
171
+ it "stores and retrieves different model types independently" do
172
+ document = IntegrationTestModels::TestDocument.new(id: "doc1", title: "Test Doc")
173
+ author = IntegrationTestModels::TestAuthor.new(name: "John Doe", email: "john@example.com")
174
+ book = IntegrationTestModels::TestBook.new(isbn: "978-123", title: "Test Book", author: "John Doe",
175
+ published_year: 2024, genre: "Fiction")
176
+
177
+ store.save(document)
178
+ store.save(author)
179
+ store.save(book)
180
+
181
+ expect(store.count(model: IntegrationTestModels::TestDocument)).to eq(1)
182
+ expect(store.count(model: IntegrationTestModels::TestAuthor)).to eq(1)
183
+ expect(store.count(model: IntegrationTestModels::TestBook)).to eq(1)
184
+
185
+ fetched_doc = store.fetch(model: IntegrationTestModels::TestDocument, id: "doc1")
186
+ fetched_author = store.fetch(model: IntegrationTestModels::TestAuthor, name: "John Doe")
187
+ fetched_book = store.fetch(model: IntegrationTestModels::TestBook, isbn: "978-123")
188
+
189
+ expect(fetched_doc).to be_a(IntegrationTestModels::TestDocument)
190
+ expect(fetched_author).to be_a(IntegrationTestModels::TestAuthor)
191
+ expect(fetched_book).to be_a(IntegrationTestModels::TestBook)
192
+ end
193
+
194
+ it "isolates where queries by model type" do
195
+ store.save(IntegrationTestModels::TestDocument.new(id: "d1", title: "Ruby Guide"))
196
+ store.save(IntegrationTestModels::TestBook.new(isbn: "b1", title: "Ruby Guide", author: "Author"))
197
+
198
+ docs = store.where(model: IntegrationTestModels::TestDocument, title: "Ruby Guide")
199
+ books = store.where(model: IntegrationTestModels::TestBook, title: "Ruby Guide")
200
+
201
+ expect(docs.size).to eq(1)
202
+ expect(docs.first).to be_a(IntegrationTestModels::TestDocument)
203
+ expect(books.size).to eq(1)
204
+ expect(books.first).to be_a(IntegrationTestModels::TestBook)
205
+ end
206
+ end
207
+
208
+ describe "file I/O round-trip" do
209
+ let(:tmpdir) { Dir.mktmpdir }
210
+ after { FileUtils.rm_rf(tmpdir) }
211
+
212
+ let(:store) { Lutaml::Store.new(adapter: :memory, models: [book_model]) }
213
+ let(:books) do
214
+ [
215
+ IntegrationTestModels::TestBook.new(isbn: "978-0123456789", title: "Ruby Programming", author: "Jane Smith",
216
+ published_year: 2023, genre: "Programming"),
217
+ IntegrationTestModels::TestBook.new(isbn: "978-9876543210", title: "Go Programming", author: "Bob Jones",
218
+ published_year: 2024, genre: "Programming")
219
+ ]
220
+ end
221
+
222
+ it "round-trips through YAML separate layout" do
223
+ store.save_all(books, path: tmpdir, format: :yaml, layout: :separate)
224
+
225
+ fresh_store = Lutaml::Store.new(adapter: :memory, models: [book_model])
226
+ loaded = fresh_store.load_all(IntegrationTestModels::TestBook, path: tmpdir, format: :yaml, layout: :separate)
227
+
228
+ expect(loaded.size).to eq(2)
229
+ expect(loaded.map(&:title).sort).to eq(["Go Programming", "Ruby Programming"])
230
+ end
231
+
232
+ it "round-trips through JSON separate layout" do
233
+ store.save_all(books, path: tmpdir, format: :json, layout: :separate)
234
+
235
+ fresh_store = Lutaml::Store.new(adapter: :memory, models: [book_model])
236
+ loaded = fresh_store.load_all(IntegrationTestModels::TestBook, path: tmpdir, format: :json, layout: :separate)
237
+
238
+ expect(loaded.size).to eq(2)
239
+ expect(loaded.map(&:author).sort).to eq(["Bob Jones", "Jane Smith"])
240
+ end
241
+ end
242
+
243
+ describe "import_all workflow" do
244
+ let(:tmpdir) { Dir.mktmpdir }
245
+ after { FileUtils.rm_rf(tmpdir) }
246
+
247
+ let(:store) { Lutaml::Store.new(adapter: :memory, models: [doc_model]) }
248
+ let(:documents) do
249
+ [
250
+ IntegrationTestModels::TestDocument.new(id: "doc1", title: "First Document", content: "Content 1"),
251
+ IntegrationTestModels::TestDocument.new(id: "doc2", title: "Second Document", content: "Content 2")
252
+ ]
253
+ end
254
+
255
+ it "loads from directory and makes models queryable" do
256
+ store.save_all(documents, path: tmpdir, format: :yaml, layout: :separate)
257
+
258
+ fresh_store = Lutaml::Store.new(adapter: :memory, models: [doc_model])
259
+ loaded = fresh_store.import_all(IntegrationTestModels::TestDocument, path: tmpdir, format: :yaml,
260
+ layout: :separate)
261
+
262
+ expect(loaded.size).to eq(2)
263
+
264
+ # Queryable via fetch
265
+ doc1 = fresh_store.fetch(model: IntegrationTestModels::TestDocument, id: "doc1")
266
+ expect(doc1).not_to be_nil
267
+ expect(doc1.title).to eq("First Document")
268
+
269
+ # Queryable via where
270
+ second = fresh_store.where(model: IntegrationTestModels::TestDocument, title: "Second Document")
271
+ expect(second.size).to eq(1)
272
+ expect(second.first.content).to eq("Content 2")
273
+
274
+ # Queryable via count
275
+ expect(fresh_store.count(model: IntegrationTestModels::TestDocument)).to eq(2)
276
+ end
277
+ end
278
+
279
+ describe "statistics" do
280
+ let(:store) { Lutaml::Store.new(adapter: :memory, models: [doc_model]) }
281
+
282
+ it "reports registered models and counts" do
283
+ store.save(IntegrationTestModels::TestDocument.new(id: "doc1", title: "Test"))
284
+
285
+ stats = store.stats
286
+ expect(stats[:models_registered]).to eq(1)
287
+ expect(stats[:total_models]).to eq(1)
288
+ expect(stats[:registered_models]).to include("IntegrationTestModels::TestDocument")
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module ModelSerializerTestModels
6
+ class SerialBook < Lutaml::Model::Serializable
7
+ attribute :isbn, :string
8
+ attribute :title, :string
9
+ attribute :author, :string
10
+ end
11
+
12
+ class SerialEbook < SerialBook
13
+ attribute :format, :string
14
+ end
15
+ end
16
+
17
+ RSpec.describe Lutaml::Store::ModelSerializer do
18
+ subject(:serializer) { described_class.new }
19
+
20
+ describe "#serialize" do
21
+ it "serializes a model to a hash with class metadata" do
22
+ book = ModelSerializerTestModels::SerialBook.new(isbn: "978-123", title: "Test Book", author: "Author")
23
+ result = serializer.serialize(book)
24
+
25
+ expect(result).to be_a(Hash)
26
+ expect(result["_class"]).to eq("ModelSerializerTestModels::SerialBook")
27
+ expect(result["isbn"]).to eq("978-123")
28
+ expect(result["title"]).to eq("Test Book")
29
+ expect(result["author"]).to eq("Author")
30
+ end
31
+
32
+ it "serializes a subclass with correct class name" do
33
+ ebook = ModelSerializerTestModels::SerialEbook.new(isbn: "978-456", title: "Digital Book", format: "epub")
34
+ result = serializer.serialize(ebook)
35
+
36
+ expect(result["_class"]).to eq("ModelSerializerTestModels::SerialEbook")
37
+ expect(result["format"]).to eq("epub")
38
+ end
39
+ end
40
+
41
+ describe "#deserialize" do
42
+ it "deserializes a hash to the correct model class" do
43
+ data = {
44
+ "_class" => "ModelSerializerTestModels::SerialBook",
45
+ "isbn" => "978-123",
46
+ "title" => "Test Book",
47
+ "author" => "Author"
48
+ }
49
+
50
+ result = serializer.deserialize(data, ModelSerializerTestModels::SerialBook)
51
+
52
+ expect(result).to be_a(ModelSerializerTestModels::SerialBook)
53
+ expect(result.isbn).to eq("978-123")
54
+ expect(result.title).to eq("Test Book")
55
+ end
56
+
57
+ it "deserializes to a subclass when class metadata indicates it" do
58
+ data = {
59
+ "_class" => "ModelSerializerTestModels::SerialEbook",
60
+ "isbn" => "978-456",
61
+ "title" => "Digital",
62
+ "format" => "epub"
63
+ }
64
+
65
+ result = serializer.deserialize(data, ModelSerializerTestModels::SerialBook)
66
+
67
+ expect(result).to be_a(ModelSerializerTestModels::SerialEbook)
68
+ expect(result.format).to eq("epub")
69
+ end
70
+
71
+ it "rejects incompatible polymorphic types" do
72
+ data = {
73
+ "_class" => "ModelSerializerTestModels::SerialBook",
74
+ "isbn" => "978-123"
75
+ }
76
+
77
+ expect do
78
+ serializer.deserialize(data, ModelSerializerTestModels::SerialEbook)
79
+ end.to raise_error(Lutaml::Store::PolymorphicUpdateError, /not compatible/)
80
+ end
81
+
82
+ it "raises on invalid data (missing _class)" do
83
+ data = { "isbn" => "978-123" }
84
+
85
+ expect do
86
+ serializer.deserialize(data, ModelSerializerTestModels::SerialBook)
87
+ end.to raise_error(Lutaml::Store::CompositeModelError, /Invalid serialized data/)
88
+ end
89
+
90
+ it "raises on invalid data (not a Hash)" do
91
+ expect do
92
+ serializer.deserialize("not a hash", ModelSerializerTestModels::SerialBook)
93
+ end.to raise_error(Lutaml::Store::CompositeModelError, /Invalid serialized data/)
94
+ end
95
+
96
+ it "raises on unresolvable class name" do
97
+ data = { "_class" => "NonExistentClass", "isbn" => "123" }
98
+
99
+ expect do
100
+ serializer.deserialize(data, ModelSerializerTestModels::SerialBook)
101
+ end.to raise_error(Lutaml::Store::CompositeModelError, /Cannot resolve class/)
102
+ end
103
+
104
+ it "strips internal metadata keys from model data" do
105
+ data = {
106
+ "_class" => "ModelSerializerTestModels::SerialBook",
107
+ "_composite_models" => { "studio" => { "storage_key" => "key1" } },
108
+ "isbn" => "978-123",
109
+ "title" => "Test"
110
+ }
111
+
112
+ result = serializer.deserialize(data, ModelSerializerTestModels::SerialBook)
113
+
114
+ expect(result.isbn).to eq("978-123")
115
+ expect(result.title).to eq("Test")
116
+ end
117
+ end
118
+
119
+ describe "round-trip" do
120
+ it "serializes and deserializes preserving all attributes" do
121
+ book = ModelSerializerTestModels::SerialBook.new(isbn: "978-789", title: "Round Trip", author: "Traveler")
122
+ serialized = serializer.serialize(book)
123
+ deserialized = serializer.deserialize(serialized, ModelSerializerTestModels::SerialBook)
124
+
125
+ expect(deserialized.isbn).to eq("978-789")
126
+ expect(deserialized.title).to eq("Round Trip")
127
+ expect(deserialized.author).to eq("Traveler")
128
+ end
129
+
130
+ it "preserves subclass identity through round-trip" do
131
+ ebook = ModelSerializerTestModels::SerialEbook.new(isbn: "978-000", title: "E-Book", author: "Digital",
132
+ format: "mobi")
133
+ serialized = serializer.serialize(ebook)
134
+ deserialized = serializer.deserialize(serialized, ModelSerializerTestModels::SerialBook)
135
+
136
+ expect(deserialized).to be_a(ModelSerializerTestModels::SerialEbook)
137
+ expect(deserialized.format).to eq("mobi")
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Lutaml::Store::BasicStore do
6
+ let(:store) { described_class.new }
7
+
8
+ describe "initialization" do
9
+ it "creates a store with default memory adapter" do
10
+ expect(store.adapter).to be_a(Lutaml::Store::Adapter::Memory)
11
+ end
12
+
13
+ it "creates a store with configuration hash" do
14
+ config = {
15
+ "adapter" => { "type" => "memory" },
16
+ "cache" => { "enabled" => false }
17
+ }
18
+ store = described_class.new(config)
19
+ expect(store.cache).to be_nil
20
+ end
21
+
22
+ it "creates a store with direct adapter instance" do
23
+ adapter = Lutaml::Store::Adapter::Memory.new
24
+ store = described_class.new(adapter)
25
+ expect(store.adapter).to eq(adapter)
26
+ end
27
+ end
28
+
29
+ describe "basic operations" do
30
+ it "stores and retrieves values" do
31
+ store.set("key1", "value1")
32
+ expect(store.get("key1")).to eq("value1")
33
+ end
34
+
35
+ it "returns nil for non-existent keys" do
36
+ expect(store.get("nonexistent")).to be_nil
37
+ end
38
+
39
+ it "deletes values" do
40
+ store.set("key1", "value1")
41
+ expect(store.delete("key1")).to be true
42
+ expect(store.get("key1")).to be_nil
43
+ end
44
+
45
+ it "checks key existence" do
46
+ store.set("key1", "value1")
47
+ expect(store.exists?("key1")).to be true
48
+ expect(store.exists?("nonexistent")).to be false
49
+ end
50
+
51
+ it "returns all data" do
52
+ store.set("key1", "value1")
53
+ store.set("key2", "value2")
54
+ expect(store.all).to eq("key1" => "value1", "key2" => "value2")
55
+ end
56
+
57
+ it "clears all data" do
58
+ store.set("key1", "value1")
59
+ store.clear
60
+ expect(store.size).to eq(0)
61
+ end
62
+
63
+ it "returns correct size" do
64
+ expect(store.size).to eq(0)
65
+ store.set("key1", "value1")
66
+ expect(store.size).to eq(1)
67
+ end
68
+ end
69
+
70
+ describe "caching" do
71
+ let(:config) do
72
+ {
73
+ "adapter" => { "type" => "memory" },
74
+ "cache" => { "enabled" => true, "max_size" => 10 }
75
+ }
76
+ end
77
+ let(:store) { described_class.new(config) }
78
+
79
+ it "caches retrieved values" do
80
+ store.set("key1", "value1")
81
+
82
+ # First get should hit backend
83
+ value1 = store.get("key1")
84
+ expect(value1).to eq("value1")
85
+
86
+ # Second get should hit cache
87
+ value2 = store.get("key1")
88
+ expect(value2).to eq("value1")
89
+
90
+ # Cache should contain the value
91
+ expect(store.cache_stats[:size]).to eq(1)
92
+ end
93
+
94
+ it "invalidates cache on delete" do
95
+ store.set("key1", "value1")
96
+ store.get("key1") # Cache the value
97
+
98
+ store.delete("key1")
99
+ expect(store.cache_stats[:size]).to eq(0)
100
+ end
101
+ end
102
+
103
+ describe "events" do
104
+ let(:events) { [] }
105
+
106
+ before do
107
+ store.on(:set) { |data| events << data }
108
+ store.on(:get) { |data| events << data }
109
+ store.on(:delete) { |data| events << data }
110
+ end
111
+
112
+ it "emits events for operations" do
113
+ store.set("key1", "value1")
114
+ store.get("key1")
115
+ store.delete("key1")
116
+
117
+ expect(events.size).to eq(3)
118
+ expect(events[0][:event]).to eq(:set)
119
+ expect(events[1][:event]).to eq(:get)
120
+ expect(events[2][:event]).to eq(:delete)
121
+ end
122
+ end
123
+
124
+ describe "monitoring" do
125
+ let(:config) do
126
+ {
127
+ "adapter" => { "type" => "memory" },
128
+ "monitoring" => { "enabled" => true }
129
+ }
130
+ end
131
+ let(:store) { described_class.new(config) }
132
+
133
+ it "tracks operation statistics" do
134
+ store.set("key1", "value1")
135
+ store.get("key1")
136
+
137
+ stats = store.stats
138
+ monitor_stats = stats[:monitor_stats]
139
+ expect(monitor_stats[:operations][:set]).to eq(1)
140
+ expect(monitor_stats[:operations][:get]).to eq(1)
141
+ # NOTE: total_operations includes the size call from stats method
142
+ expect(monitor_stats[:total_operations]).to be >= 2
143
+ end
144
+ end
145
+
146
+ describe "configuration from file" do
147
+ let(:config_file) { "test_config.yml" }
148
+ let(:config_content) do
149
+ <<~YAML
150
+ lutaml_store:
151
+ adapter:
152
+ type: memory
153
+ cache:
154
+ enabled: true
155
+ max_size: 500
156
+ monitoring:
157
+ enabled: true
158
+ YAML
159
+ end
160
+
161
+ before do
162
+ File.write(config_file, config_content)
163
+ end
164
+
165
+ after do
166
+ File.delete(config_file) if File.exist?(config_file)
167
+ end
168
+
169
+ it "loads configuration from YAML file" do
170
+ store = described_class.from_file(config_file)
171
+ expect(store.adapter).to be_a(Lutaml::Store::Adapter::Memory)
172
+ expect(store.cache).not_to be_nil
173
+ expect(store.monitor).not_to be_nil
174
+ end
175
+ end
176
+
177
+ describe "#close" do
178
+ it "closes resources properly" do
179
+ expect { store.close }.not_to raise_error
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Lutaml::Store do
4
+ it "has a version number" do
5
+ expect(Lutaml::Store::VERSION).not_to be nil
6
+ end
7
+
8
+ it "provides access to Store and DatabaseStore classes" do
9
+ expect(Lutaml::Store::BasicStore).to be_a(Class)
10
+ expect(Lutaml::Store::DatabaseStore).to be_a(Class)
11
+ end
12
+
13
+ it "returns a DatabaseStore from .new" do
14
+ test_model = Struct.new(:id, :name)
15
+ store = Lutaml::Store.new(
16
+ adapter: :memory,
17
+ models: [{ model: test_model, key: :id }]
18
+ )
19
+ expect(store).to be_a(Lutaml::Store::DatabaseStore)
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require "lutaml/store"
5
+
6
+ RSpec.configure do |config|
7
+ # Enable flags like --only-failures and --next-failure
8
+ config.example_status_persistence_file_path = ".rspec_status"
9
+
10
+ # Disable RSpec exposing methods globally on `Module` and `main`
11
+ config.disable_monkey_patching!
12
+
13
+ config.expect_with :rspec do |c|
14
+ c.syntax = :expect
15
+ end
16
+ end