lutaml-store 0.2.0 → 0.2.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +11 -175
  3. data/README.adoc +233 -1124
  4. data/lib/lutaml/store/adapter/base.rb +4 -0
  5. data/lib/lutaml/store/adapter/memory.rb +8 -0
  6. data/lib/lutaml/store/cache_store.rb +9 -6
  7. data/lib/lutaml/store/format.rb +19 -0
  8. data/lib/lutaml/store/http_cache.rb +3 -13
  9. data/lib/lutaml/store/model_registration.rb +5 -2
  10. data/lib/lutaml/store/model_registry.rb +22 -20
  11. data/lib/lutaml/store/package_store.rb +2 -18
  12. data/lib/lutaml/store/package_transport/base.rb +48 -0
  13. data/lib/lutaml/store/package_transport/directory_transport.rb +196 -0
  14. data/lib/lutaml/store/package_transport/zip_transport.rb +178 -0
  15. data/lib/lutaml/store/package_transport.rb +11 -438
  16. data/lib/lutaml/store/version.rb +1 -1
  17. metadata +12 -77
  18. data/.github/workflows/main.yml +0 -27
  19. data/.gitignore +0 -12
  20. data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +0 -209
  21. data/CORRECTED_HTTP_CACHE_PLAN.md +0 -164
  22. data/Gemfile +0 -15
  23. data/Gemfile.lock +0 -227
  24. data/TODO.impl/0-lutaml-store-self-quality.md +0 -112
  25. data/TODO.impl/1-lutaml-hal-migration.md +0 -96
  26. data/TODO.impl/2-glossarist-migration.md +0 -359
  27. data/TODO.impl/3-lutaml-jsonschema-migration.md +0 -273
  28. data/bin/console +0 -11
  29. data/bin/setup +0 -8
  30. data/demo/Gemfile +0 -15
  31. data/demo/Gemfile.lock +0 -61
  32. data/demo/README.adoc +0 -301
  33. data/demo/data/vcards/co/contact_10_thompson.data +0 -1
  34. data/demo/data/vcards/co/contact_10_thompson.meta +0 -1
  35. data/demo/data/vcards/co/contact_1_doe.data +0 -1
  36. data/demo/data/vcards/co/contact_1_doe.meta +0 -1
  37. data/demo/data/vcards/co/contact_2_smith.data +0 -1
  38. data/demo/data/vcards/co/contact_2_smith.meta +0 -1
  39. data/demo/data/vcards/co/contact_3_johnson.data +0 -1
  40. data/demo/data/vcards/co/contact_3_johnson.meta +0 -1
  41. data/demo/data/vcards/co/contact_4_garcia.data +0 -1
  42. data/demo/data/vcards/co/contact_4_garcia.meta +0 -1
  43. data/demo/data/vcards/co/contact_5_wilson.data +0 -1
  44. data/demo/data/vcards/co/contact_5_wilson.meta +0 -1
  45. data/demo/data/vcards/co/contact_6_brown.data +0 -1
  46. data/demo/data/vcards/co/contact_6_brown.meta +0 -1
  47. data/demo/data/vcards/co/contact_7_davis.data +0 -1
  48. data/demo/data/vcards/co/contact_7_davis.meta +0 -1
  49. data/demo/data/vcards/co/contact_8_anderson.data +0 -1
  50. data/demo/data/vcards/co/contact_8_anderson.meta +0 -1
  51. data/demo/data/vcards/co/contact_9_taylor.data +0 -1
  52. data/demo/data/vcards/co/contact_9_taylor.meta +0 -1
  53. data/demo/data/vcards.db +0 -0
  54. data/demo/pottery_class_demo.rb +0 -164
  55. data/demo/vcard_models.rb +0 -140
  56. data/demo/vcard_store_demo.rb +0 -526
  57. data/lutaml-store.gemspec +0 -36
  58. data/plan.adoc +0 -606
  59. data/spec/lutaml/store/adapter_interface_spec.rb +0 -89
  60. data/spec/lutaml/store/anti_pattern_guard_spec.rb +0 -35
  61. data/spec/lutaml/store/anti_pattern_spec.rb +0 -78
  62. data/spec/lutaml/store/autoload_spec.rb +0 -34
  63. data/spec/lutaml/store/cache_store_spec.rb +0 -271
  64. data/spec/lutaml/store/compression_spec.rb +0 -78
  65. data/spec/lutaml/store/config_enhanced_spec.rb +0 -158
  66. data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +0 -336
  67. data/spec/lutaml/store/custom_serializer_spec.rb +0 -108
  68. data/spec/lutaml/store/database_store_spec.rb +0 -279
  69. data/spec/lutaml/store/file_io_spec.rb +0 -220
  70. data/spec/lutaml/store/format/yamls_spec.rb +0 -80
  71. data/spec/lutaml/store/format_round_trip_spec.rb +0 -110
  72. data/spec/lutaml/store/format_spec.rb +0 -70
  73. data/spec/lutaml/store/http_cache_entry_spec.rb +0 -203
  74. data/spec/lutaml/store/http_cache_hal_integration_spec.rb +0 -404
  75. data/spec/lutaml/store/http_cache_spec.rb +0 -422
  76. data/spec/lutaml/store/http_header_processor_spec.rb +0 -290
  77. data/spec/lutaml/store/import_spec.rb +0 -90
  78. data/spec/lutaml/store/integrity_spec.rb +0 -157
  79. data/spec/lutaml/store/key_collision_serializer_spec.rb +0 -98
  80. data/spec/lutaml/store/load_save_spec.rb +0 -107
  81. data/spec/lutaml/store/lutaml_model_integration_spec.rb +0 -291
  82. data/spec/lutaml/store/model_serializer_spec.rb +0 -140
  83. data/spec/lutaml/store/package_definition_spec.rb +0 -89
  84. data/spec/lutaml/store/package_store_spec.rb +0 -153
  85. data/spec/lutaml/store/package_transport/directory_transport_spec.rb +0 -139
  86. data/spec/lutaml/store/package_transport/zip_transport_spec.rb +0 -85
  87. data/spec/lutaml/store/store_spec.rb +0 -182
  88. data/spec/lutaml/store_spec.rb +0 -21
  89. data/spec/spec_helper.rb +0 -16
  90. data/spec/support/simple_test_model.rb +0 -15
  91. data/spec/support/yamls_test_model.rb +0 -35
@@ -1,279 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- module DatabaseStoreTestModels
6
- class TestStudio < Lutaml::Model::Serializable
7
- attribute :studio_key, :string
8
- attribute :name, :string
9
- attribute :location, :string
10
- attribute :_class, :string, default: -> { "TestStudio" }, polymorphic_class: true
11
- end
12
-
13
- class TestCeramicStudio < TestStudio
14
- attribute :clay_type, :string
15
- attribute :_class, :string, default: -> { "TestCeramicStudio" }
16
- end
17
-
18
- class TestPotteryClass < Lutaml::Model::Serializable
19
- attribute :class_id, :string
20
- attribute :description, :string
21
- attribute :studio, TestStudio, polymorphic: true
22
- end
23
- end
24
-
25
- RSpec.describe Lutaml::Store::DatabaseStore do
26
- let(:config) { { adapter_type: :memory } }
27
-
28
- let(:models) do
29
- [
30
- { model: DatabaseStoreTestModels::TestPotteryClass, key: :class_id },
31
- { model: DatabaseStoreTestModels::TestStudio, key: :studio_key, polymorphic_class_key: :_class },
32
- { model: DatabaseStoreTestModels::TestCeramicStudio, key: :studio_key, polymorphic_class_key: :_class }
33
- ]
34
- end
35
-
36
- let(:store) { described_class.new(adapter: :memory, models: models) }
37
-
38
- describe "#initialize" do
39
- it "creates a database store with models" do
40
- expect(store).to be_a(described_class)
41
- expect(store.registry.count).to eq(3)
42
- end
43
-
44
- it "raises error when no models provided" do
45
- expect do
46
- described_class.new(adapter: :memory, models: [])
47
- end.to raise_error(Lutaml::Store::ConfigurationError, /No models registered/)
48
- end
49
- end
50
-
51
- describe "#save and #fetch" do
52
- let(:studio) { DatabaseStoreTestModels::TestStudio.new(studio_key: "test_studio", name: "Test Studio") }
53
- let(:pottery_class) do
54
- DatabaseStoreTestModels::TestPotteryClass.new(
55
- class_id: "pottery_101",
56
- description: "Basic pottery",
57
- studio: studio
58
- )
59
- end
60
-
61
- it "saves and fetches a model with composite relationships" do
62
- store.save(pottery_class)
63
-
64
- fetched = store.fetch(model: DatabaseStoreTestModels::TestPotteryClass, class_id: "pottery_101")
65
- expect(fetched).to be_a(DatabaseStoreTestModels::TestPotteryClass)
66
- expect(fetched.class_id).to eq("pottery_101")
67
- expect(fetched.description).to eq("Basic pottery")
68
- expect(fetched.studio).to be_a(DatabaseStoreTestModels::TestStudio)
69
- expect(fetched.studio.studio_key).to eq("test_studio")
70
- expect(fetched.studio.name).to eq("Test Studio")
71
- end
72
-
73
- it "handles polymorphic models correctly" do
74
- ceramic_studio = DatabaseStoreTestModels::TestCeramicStudio.new(
75
- studio_key: "ceramic_studio",
76
- name: "Ceramic Studio",
77
- clay_type: "Porcelain"
78
- )
79
-
80
- store.save(ceramic_studio)
81
-
82
- fetched = store.fetch(model: DatabaseStoreTestModels::TestStudio, studio_key: "ceramic_studio")
83
- expect(fetched).to be_a(DatabaseStoreTestModels::TestCeramicStudio)
84
- expect(fetched.clay_type).to eq("Porcelain")
85
- end
86
-
87
- it "returns nil for non-existent models" do
88
- result = store.fetch(model: DatabaseStoreTestModels::TestPotteryClass, class_id: "nonexistent")
89
- expect(result).to be_nil
90
- end
91
- end
92
-
93
- describe "#update" do
94
- let(:studio) { DatabaseStoreTestModels::TestStudio.new(studio_key: "test_studio", name: "Test Studio") }
95
- let(:pottery_class) do
96
- DatabaseStoreTestModels::TestPotteryClass.new(
97
- class_id: "pottery_101",
98
- description: "Basic pottery",
99
- studio: studio
100
- )
101
- end
102
-
103
- before { store.save(pottery_class) }
104
-
105
- it "updates with block" do
106
- updated = store.update(model: DatabaseStoreTestModels::TestPotteryClass, class_id: "pottery_101") do |model|
107
- model.description = "Advanced pottery"
108
- model
109
- end
110
-
111
- expect(updated.description).to eq("Advanced pottery")
112
-
113
- fetched = store.fetch(model: DatabaseStoreTestModels::TestPotteryClass, class_id: "pottery_101")
114
- expect(fetched.description).to eq("Advanced pottery")
115
- end
116
-
117
- it "updates with hash including dot notation" do
118
- updated = store.update(
119
- model: DatabaseStoreTestModels::TestPotteryClass,
120
- class_id: "pottery_101",
121
- attributes: { "studio.location" => "Downtown" }
122
- )
123
-
124
- expect(updated.studio.location).to eq("Downtown")
125
-
126
- fetched = store.fetch(model: DatabaseStoreTestModels::TestPotteryClass, class_id: "pottery_101")
127
- expect(fetched.studio.location).to eq("Downtown")
128
- end
129
- end
130
-
131
- describe "#destroy" do
132
- let(:studio) { DatabaseStoreTestModels::TestStudio.new(studio_key: "test_studio", name: "Test Studio") }
133
- let(:pottery_class) do
134
- DatabaseStoreTestModels::TestPotteryClass.new(
135
- class_id: "pottery_101",
136
- description: "Basic pottery",
137
- studio: studio
138
- )
139
- end
140
-
141
- before { store.save(pottery_class) }
142
-
143
- it "destroys a model and its composite relationships" do
144
- result = store.destroy(model: DatabaseStoreTestModels::TestPotteryClass, class_id: "pottery_101")
145
- expect(result).to be true
146
-
147
- fetched = store.fetch(model: DatabaseStoreTestModels::TestPotteryClass, class_id: "pottery_101")
148
- expect(fetched).to be_nil
149
-
150
- studio_fetched = store.fetch(model: DatabaseStoreTestModels::TestStudio, studio_key: "test_studio")
151
- expect(studio_fetched).to be_nil
152
- end
153
-
154
- it "returns false for non-existent models" do
155
- result = store.destroy(model: DatabaseStoreTestModels::TestPotteryClass, class_id: "nonexistent")
156
- expect(result).to be false
157
- end
158
- end
159
-
160
- describe "#where" do
161
- let(:studio1) { DatabaseStoreTestModels::TestStudio.new(studio_key: "studio1", name: "Studio One") }
162
- let(:studio2) { DatabaseStoreTestModels::TestStudio.new(studio_key: "studio2", name: "Studio Two") }
163
- let(:pottery1) do
164
- DatabaseStoreTestModels::TestPotteryClass.new(
165
- class_id: "pottery_101",
166
- description: "Basic pottery",
167
- studio: studio1
168
- )
169
- end
170
- let(:pottery2) do
171
- DatabaseStoreTestModels::TestPotteryClass.new(
172
- class_id: "pottery_201",
173
- description: "Advanced pottery",
174
- studio: studio2
175
- )
176
- end
177
-
178
- before do
179
- store.save([pottery1, pottery2])
180
- end
181
-
182
- it "finds models by criteria" do
183
- results = store.where(model: DatabaseStoreTestModels::TestPotteryClass, description: "Basic pottery")
184
- expect(results.size).to eq(1)
185
- expect(results.first.class_id).to eq("pottery_101")
186
- end
187
- end
188
-
189
- describe "#all" do
190
- let(:studio1) { DatabaseStoreTestModels::TestStudio.new(studio_key: "studio1", name: "Studio One") }
191
- let(:studio2) { DatabaseStoreTestModels::TestStudio.new(studio_key: "studio2", name: "Studio Two") }
192
- let(:pottery1) do
193
- DatabaseStoreTestModels::TestPotteryClass.new(
194
- class_id: "pottery_101",
195
- description: "Basic pottery",
196
- studio: studio1
197
- )
198
- end
199
- let(:pottery2) do
200
- DatabaseStoreTestModels::TestPotteryClass.new(
201
- class_id: "pottery_201",
202
- description: "Advanced pottery",
203
- studio: studio2
204
- )
205
- end
206
-
207
- before do
208
- store.save([pottery1, pottery2])
209
- end
210
-
211
- it "returns all models of a specific type" do
212
- results = store.all(model: DatabaseStoreTestModels::TestPotteryClass)
213
- expect(results.size).to eq(2)
214
- expect(results.map(&:class_id)).to contain_exactly("pottery_101", "pottery_201")
215
- end
216
- end
217
-
218
- describe "#exists?" do
219
- let(:studio) { DatabaseStoreTestModels::TestStudio.new(studio_key: "test_studio", name: "Test Studio") }
220
- let(:pottery_class) do
221
- DatabaseStoreTestModels::TestPotteryClass.new(
222
- class_id: "pottery_101",
223
- description: "Basic pottery",
224
- studio: studio
225
- )
226
- end
227
-
228
- before { store.save(pottery_class) }
229
-
230
- it "returns true for existing models" do
231
- expect(store.exists?(model: DatabaseStoreTestModels::TestPotteryClass, class_id: "pottery_101")).to be true
232
- end
233
-
234
- it "returns false for non-existent models" do
235
- expect(store.exists?(model: DatabaseStoreTestModels::TestPotteryClass, class_id: "nonexistent")).to be false
236
- end
237
- end
238
-
239
- describe "#count" do
240
- let(:studio1) { DatabaseStoreTestModels::TestStudio.new(studio_key: "studio1", name: "Studio One") }
241
- let(:studio2) { DatabaseStoreTestModels::TestStudio.new(studio_key: "studio2", name: "Studio Two") }
242
- let(:pottery1) do
243
- DatabaseStoreTestModels::TestPotteryClass.new(
244
- class_id: "pottery_101",
245
- description: "Basic pottery",
246
- studio: studio1
247
- )
248
- end
249
- let(:pottery2) do
250
- DatabaseStoreTestModels::TestPotteryClass.new(
251
- class_id: "pottery_201",
252
- description: "Advanced pottery",
253
- studio: studio2
254
- )
255
- end
256
-
257
- before do
258
- store.save([pottery1, pottery2])
259
- end
260
-
261
- it "returns count of models" do
262
- expect(store.count(model: DatabaseStoreTestModels::TestPotteryClass)).to eq(2)
263
- expect(store.count(model: DatabaseStoreTestModels::TestStudio)).to eq(2)
264
- end
265
- end
266
-
267
- describe "#stats" do
268
- it "provides statistics" do
269
- stats = store.stats
270
- expect(stats).to include(:models_registered, :registered_models, :total_models)
271
- expect(stats[:models_registered]).to eq(3)
272
- expect(stats[:registered_models]).to include(
273
- "DatabaseStoreTestModels::TestPotteryClass",
274
- "DatabaseStoreTestModels::TestStudio",
275
- "DatabaseStoreTestModels::TestCeramicStudio"
276
- )
277
- end
278
- end
279
- end
@@ -1,220 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "tmpdir"
5
- require "json"
6
-
7
- class FileTestItem < Lutaml::Model::Serializable
8
- attribute :name, :string
9
- attribute :value, :string
10
-
11
- key_value do
12
- map :name, to: :name
13
- map :value, to: :value
14
- end
15
- end
16
-
17
- RSpec.describe "Lutaml::Store file I/O" do
18
- let(:tmpdir) { Dir.mktmpdir }
19
- after { FileUtils.rm_rf(tmpdir) }
20
-
21
- let(:store) do
22
- Lutaml::Store.new(
23
- adapter: :memory,
24
- models: [{ model: FileTestItem, key: :name, dir: "items" }]
25
- )
26
- end
27
-
28
- let(:items) do
29
- [
30
- FileTestItem.new(name: "alpha", value: "first"),
31
- FileTestItem.new(name: "beta", value: "second"),
32
- FileTestItem.new(name: "gamma", value: "third")
33
- ]
34
- end
35
-
36
- def items_dir
37
- File.join(tmpdir, "items")
38
- end
39
-
40
- # ── YAML format ──
41
-
42
- describe "YAML separate layout" do
43
- it "round-trips models" do
44
- store.save_all(items, path: tmpdir, format: :yaml, layout: :separate)
45
-
46
- expect(Dir.glob(File.join(items_dir, "*.{yaml,yml}")).size).to eq(3)
47
-
48
- loaded = store.load_all(FileTestItem, path: tmpdir, format: :yaml, layout: :separate)
49
- expect(loaded.size).to eq(3)
50
- expect(loaded.map(&:name).sort).to eq(%w[alpha beta gamma])
51
- expect(loaded.map(&:value).sort).to eq(%w[first second third])
52
- end
53
-
54
- it "writes one file per model named by key" do
55
- store.save_all(items, path: tmpdir, format: :yaml, layout: :separate)
56
-
57
- expect(File.exist?(File.join(items_dir, "alpha.yaml"))).to be true
58
- expect(File.exist?(File.join(items_dir, "beta.yaml"))).to be true
59
- expect(File.exist?(File.join(items_dir, "gamma.yaml"))).to be true
60
- end
61
- end
62
-
63
- describe "YAML grouped layout" do
64
- it "writes one file per key" do
65
- store.save_all(items, path: tmpdir, format: :yaml, layout: :grouped)
66
-
67
- files = Dir.glob(File.join(items_dir, "*.yaml")).sort
68
- expect(files.size).to eq(3)
69
- end
70
-
71
- it "round-trips models" do
72
- store.save_all(items, path: tmpdir, format: :yaml, layout: :grouped)
73
-
74
- loaded = store.load_all(FileTestItem, path: tmpdir, format: :yaml, layout: :grouped)
75
- expect(loaded.size).to eq(3)
76
- expect(loaded.map(&:name).sort).to eq(%w[alpha beta gamma])
77
- end
78
- end
79
-
80
- # ── JSON format ──
81
-
82
- describe "JSON separate layout" do
83
- it "round-trips models" do
84
- store.save_all(items, path: tmpdir, format: :json, layout: :separate)
85
-
86
- expect(Dir.glob(File.join(items_dir, "*.json")).size).to eq(3)
87
-
88
- loaded = store.load_all(FileTestItem, path: tmpdir, format: :json, layout: :separate)
89
- expect(loaded.size).to eq(3)
90
- expect(loaded.map(&:name).sort).to eq(%w[alpha beta gamma])
91
- end
92
-
93
- it "writes valid JSON files" do
94
- store.save_all(items, path: tmpdir, format: :json, layout: :separate)
95
-
96
- Dir.glob(File.join(items_dir, "*.json")).each do |path|
97
- expect { JSON.parse(File.read(path, encoding: "utf-8")) }.not_to raise_error
98
- end
99
- end
100
- end
101
-
102
- # ── JSONL format ──
103
-
104
- describe "JSONL export/import" do
105
- it "exports models to JSONL file" do
106
- export_path = File.join(tmpdir, "data.jsonl")
107
- store.export(items, path: export_path, format: :jsonl)
108
-
109
- expect(File.exist?(export_path)).to be true
110
- lines = File.read(export_path, encoding: "utf-8").lines.reject { |l| l.strip.empty? }
111
- expect(lines.size).to eq(3)
112
- lines.each { |line| expect { JSON.parse(line) }.not_to raise_error }
113
- end
114
-
115
- it "round-trips through JSONL file" do
116
- export_path = File.join(tmpdir, "data.jsonl")
117
- store.export(items, path: export_path, format: :jsonl)
118
-
119
- fmt = Lutaml::Store::Format.resolve(:jsonl)
120
- data = File.read(export_path, encoding: "utf-8")
121
- loaded = fmt.deserialize_many(data, FileTestItem)
122
-
123
- expect(loaded.size).to eq(3)
124
- expect(loaded.map(&:name).sort).to eq(%w[alpha beta gamma])
125
- end
126
- end
127
-
128
- # ── Marshal format ──
129
-
130
- describe "Marshal separate layout" do
131
- it "round-trips models" do
132
- store.save_all(items, path: tmpdir, format: :marshal, layout: :separate)
133
-
134
- expect(Dir.glob(File.join(items_dir, "*.bin")).size).to eq(3)
135
-
136
- loaded = store.load_all(FileTestItem, path: tmpdir, format: :marshal, layout: :separate)
137
- expect(loaded.size).to eq(3)
138
- expect(loaded.map(&:name).sort).to eq(%w[alpha beta gamma])
139
- end
140
- end
141
-
142
- # ── YAMLS format ──
143
- # Format::Yamls requires models with the yamls DSL (e.g. ConceptDocument).
144
- # For simple models without yamls DSL, use Format::Yaml (:yaml) instead.
145
-
146
- describe "YAML multi-document via grouped layout" do
147
- it "writes multi-document YAML files" do
148
- store.save_all(items, path: tmpdir, format: :yaml, layout: :grouped)
149
-
150
- files = Dir.glob(File.join(items_dir, "*.{yaml,yml}")).sort
151
- expect(files.size).to eq(3)
152
- end
153
-
154
- it "round-trips models" do
155
- store.save_all(items, path: tmpdir, format: :yaml, layout: :grouped)
156
-
157
- loaded = store.load_all(FileTestItem, path: tmpdir, format: :yaml, layout: :grouped)
158
- expect(loaded.size).to eq(3)
159
- expect(loaded.map(&:name).sort).to eq(%w[alpha beta gamma])
160
- end
161
- end
162
-
163
- # ── Edge cases ──
164
-
165
- describe "empty directory" do
166
- it "returns empty array from load_all" do
167
- FileUtils.mkdir_p(items_dir)
168
-
169
- loaded = store.load_all(FileTestItem, path: tmpdir, format: :yaml, layout: :separate)
170
- expect(loaded).to eq([])
171
- end
172
- end
173
-
174
- describe "empty model list" do
175
- it "returns empty array from save_all" do
176
- result = store.save_all([], path: tmpdir, format: :yaml, layout: :separate)
177
- expect(result).to eq([])
178
- end
179
- end
180
-
181
- describe "missing directory" do
182
- it "raises BackendError for load_all" do
183
- expect do
184
- store.load_all(FileTestItem, path: "/nonexistent/path", format: :yaml, layout: :separate)
185
- end.to raise_error(Lutaml::Store::BackendError, /Directory not found/)
186
- end
187
- end
188
-
189
- describe "no path provided" do
190
- it "raises BackendError for save_all without path" do
191
- items_with_no_dir = [
192
- FileTestItem.new(name: "x", value: "y")
193
- ]
194
- # No dir in registration and no path
195
- bare_store = Lutaml::Store.new(
196
- adapter: :memory,
197
- models: [{ model: FileTestItem, key: :name }]
198
- )
199
- expect do
200
- bare_store.save_all(items_with_no_dir, format: :yaml, layout: :separate)
201
- end.to raise_error(Lutaml::Store::BackendError, /No directory specified/)
202
- end
203
- end
204
-
205
- describe "unknown layout" do
206
- it "raises ConfigurationError" do
207
- expect do
208
- store.save_all(items, path: tmpdir, format: :yaml, layout: :unknown)
209
- end.to raise_error(Lutaml::Store::ConfigurationError, /Unknown layout/)
210
- end
211
- end
212
-
213
- describe "unknown format" do
214
- it "raises UnsupportedFormatError" do
215
- expect do
216
- store.save_all(items, path: tmpdir, format: :csv, layout: :separate)
217
- end.to raise_error(Lutaml::Store::Format::UnsupportedFormatError)
218
- end
219
- end
220
- end
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "support/yamls_test_model"
5
-
6
- RSpec.describe Lutaml::Store::Format::Yamls do
7
- let(:fmt) { described_class.new }
8
-
9
- let(:model) do
10
- YamlsTestModel.new(
11
- header: YamlsTestHeader.new(id: "test-1", name: "Test"),
12
- parts: [
13
- YamlsTestPart.new(label: "a", value: "1"),
14
- YamlsTestPart.new(label: "b", value: "2")
15
- ]
16
- )
17
- end
18
-
19
- describe "#extension" do
20
- it { expect(fmt.extension).to eq(".yaml") }
21
- end
22
-
23
- describe "#glob_pattern" do
24
- it { expect(fmt.glob_pattern).to eq("*.{yaml,yml}") }
25
- end
26
-
27
- describe "#serialize" do
28
- it "produces a YAML stream starting with ---" do
29
- result = fmt.serialize(model)
30
- expect(result).to start_with("---")
31
- end
32
-
33
- it "produces multiple --- separators for multi-part models" do
34
- result = fmt.serialize(model)
35
- separators = result.scan(/^---$/).length
36
- expect(separators).to be >= 2
37
- end
38
- end
39
-
40
- describe "#deserialize" do
41
- it "reconstructs the model from a YAML stream" do
42
- yaml = fmt.serialize(model)
43
- loaded = fmt.deserialize(yaml, YamlsTestModel)
44
-
45
- expect(loaded.header.id).to eq("test-1")
46
- expect(loaded.header.name).to eq("Test")
47
- end
48
- end
49
-
50
- describe "round-trip" do
51
- it "preserves model data including parts" do
52
- yaml = fmt.serialize(model)
53
- loaded = fmt.deserialize(yaml, YamlsTestModel)
54
-
55
- expect(loaded.header.id).to eq("test-1")
56
- expect(loaded.header.name).to eq("Test")
57
- expect(loaded.parts.length).to eq(2)
58
- expect(loaded.parts.first.label).to eq("a")
59
- expect(loaded.parts.last.value).to eq("2")
60
- end
61
- end
62
-
63
- describe "#serialize_many" do
64
- it "serializes a single model identically to #serialize" do
65
- # In GCR, each file has exactly one concept (one group per key).
66
- # serialize_many([single]) == serialize(single).
67
- result = fmt.serialize_many([model])
68
- expect(result).to eq(fmt.serialize(model))
69
- end
70
- end
71
-
72
- describe "#deserialize_many" do
73
- it "returns an array from a YAML stream" do
74
- yaml = fmt.serialize(model)
75
- result = fmt.deserialize_many(yaml, YamlsTestModel)
76
- expect(result).to be_an(Array)
77
- expect(result.first).to be_a(YamlsTestModel)
78
- end
79
- end
80
- end
@@ -1,110 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "json"
5
-
6
- class FormatTestItem < Lutaml::Model::Serializable
7
- attribute :name, :string
8
- attribute :value, :string
9
- attribute :count, :integer
10
-
11
- key_value do
12
- map :name, to: :name
13
- map :value, to: :value
14
- map :count, to: :count
15
- end
16
- end
17
-
18
- RSpec.describe "Format handler round-trips" do
19
- let(:item) { FormatTestItem.new(name: "alpha", value: "first", count: 42) }
20
- let(:items) do
21
- [
22
- FormatTestItem.new(name: "alpha", value: "first", count: 1),
23
- FormatTestItem.new(name: "beta", value: "second", count: 2)
24
- ]
25
- end
26
-
27
- shared_examples "single-model round-trip" do |format_sym|
28
- it "round-trips a single model through #{format_sym}" do
29
- fmt = Lutaml::Store::Format.resolve(format_sym)
30
- serialized = fmt.serialize(item)
31
- restored = fmt.deserialize(serialized, FormatTestItem)
32
-
33
- expect(restored.name).to eq("alpha")
34
- expect(restored.value).to eq("first")
35
- expect(restored.count).to eq(42)
36
- end
37
- end
38
-
39
- shared_examples "multi-model round-trip" do |format_sym|
40
- it "round-trips multiple models through #{format_sym}" do
41
- fmt = Lutaml::Store::Format.resolve(format_sym)
42
- serialized = fmt.serialize_many(items)
43
- restored = fmt.deserialize_many(serialized, FormatTestItem)
44
-
45
- expect(restored.size).to eq(2)
46
- expect(restored.map(&:name).sort).to eq(%w[alpha beta])
47
- expect(restored.map(&:count).sort).to eq([1, 2])
48
- end
49
- end
50
-
51
- describe Lutaml::Store::Format::Yaml do
52
- it_behaves_like "single-model round-trip", :yaml
53
- it_behaves_like "multi-model round-trip", :yaml
54
-
55
- it "produces valid YAML" do
56
- fmt = described_class.new
57
- output = fmt.serialize(item)
58
- expect(output).to include("name:")
59
- expect(output).to include("value:")
60
- end
61
- end
62
-
63
- describe Lutaml::Store::Format::Yamls do
64
- # Format::Yamls requires models with the yamls DSL (multi-document YAML stream).
65
- # Tested separately in spec/lutaml/store/format/yamls_spec.rb with proper yamls models.
66
-
67
- it "produces multi-document YAML stream" do
68
- fmt = described_class.new
69
- output = fmt.serialize_many(items)
70
- expect(output.scan(/^---/).size).to be >= 2
71
- end
72
- end
73
-
74
- describe Lutaml::Store::Format::Json do
75
- it_behaves_like "single-model round-trip", :json
76
-
77
- it "produces valid JSON" do
78
- fmt = described_class.new
79
- output = fmt.serialize(item)
80
- parsed = JSON.parse(output)
81
- expect(parsed["name"]).to eq("alpha")
82
- expect(parsed["count"]).to eq(42)
83
- end
84
- end
85
-
86
- describe Lutaml::Store::Format::Jsonl do
87
- it_behaves_like "single-model round-trip", :jsonl
88
- it_behaves_like "multi-model round-trip", :jsonl
89
-
90
- it "produces line-delimited JSON" do
91
- fmt = described_class.new
92
- output = fmt.serialize_many(items)
93
- lines = output.lines.reject { |l| l.strip.empty? }
94
- expect(lines.size).to eq(2)
95
- lines.each { |line| expect { JSON.parse(line) }.not_to raise_error }
96
- end
97
- end
98
-
99
- describe Lutaml::Store::Format::MarshalFormat do
100
- it_behaves_like "single-model round-trip", :marshal
101
- it_behaves_like "multi-model round-trip", :marshal
102
-
103
- it "produces binary data" do
104
- fmt = described_class.new
105
- output = fmt.serialize(item)
106
- expect(output).to be_a(String)
107
- expect(output.encoding).to eq(Encoding::ASCII_8BIT)
108
- end
109
- end
110
- end