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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +11 -175
- data/README.adoc +233 -1124
- data/lib/lutaml/store/adapter/base.rb +4 -0
- data/lib/lutaml/store/adapter/memory.rb +8 -0
- data/lib/lutaml/store/cache_store.rb +9 -6
- data/lib/lutaml/store/format.rb +19 -0
- data/lib/lutaml/store/http_cache.rb +3 -13
- data/lib/lutaml/store/model_registration.rb +5 -2
- data/lib/lutaml/store/model_registry.rb +22 -20
- data/lib/lutaml/store/package_store.rb +2 -18
- data/lib/lutaml/store/package_transport/base.rb +48 -0
- data/lib/lutaml/store/package_transport/directory_transport.rb +196 -0
- data/lib/lutaml/store/package_transport/zip_transport.rb +178 -0
- data/lib/lutaml/store/package_transport.rb +11 -438
- data/lib/lutaml/store/version.rb +1 -1
- metadata +12 -77
- data/.github/workflows/main.yml +0 -27
- data/.gitignore +0 -12
- data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +0 -209
- data/CORRECTED_HTTP_CACHE_PLAN.md +0 -164
- data/Gemfile +0 -15
- data/Gemfile.lock +0 -227
- data/TODO.impl/0-lutaml-store-self-quality.md +0 -112
- data/TODO.impl/1-lutaml-hal-migration.md +0 -96
- data/TODO.impl/2-glossarist-migration.md +0 -359
- data/TODO.impl/3-lutaml-jsonschema-migration.md +0 -273
- data/bin/console +0 -11
- data/bin/setup +0 -8
- data/demo/Gemfile +0 -15
- data/demo/Gemfile.lock +0 -61
- data/demo/README.adoc +0 -301
- data/demo/data/vcards/co/contact_10_thompson.data +0 -1
- data/demo/data/vcards/co/contact_10_thompson.meta +0 -1
- data/demo/data/vcards/co/contact_1_doe.data +0 -1
- data/demo/data/vcards/co/contact_1_doe.meta +0 -1
- data/demo/data/vcards/co/contact_2_smith.data +0 -1
- data/demo/data/vcards/co/contact_2_smith.meta +0 -1
- data/demo/data/vcards/co/contact_3_johnson.data +0 -1
- data/demo/data/vcards/co/contact_3_johnson.meta +0 -1
- data/demo/data/vcards/co/contact_4_garcia.data +0 -1
- data/demo/data/vcards/co/contact_4_garcia.meta +0 -1
- data/demo/data/vcards/co/contact_5_wilson.data +0 -1
- data/demo/data/vcards/co/contact_5_wilson.meta +0 -1
- data/demo/data/vcards/co/contact_6_brown.data +0 -1
- data/demo/data/vcards/co/contact_6_brown.meta +0 -1
- data/demo/data/vcards/co/contact_7_davis.data +0 -1
- data/demo/data/vcards/co/contact_7_davis.meta +0 -1
- data/demo/data/vcards/co/contact_8_anderson.data +0 -1
- data/demo/data/vcards/co/contact_8_anderson.meta +0 -1
- data/demo/data/vcards/co/contact_9_taylor.data +0 -1
- data/demo/data/vcards/co/contact_9_taylor.meta +0 -1
- data/demo/data/vcards.db +0 -0
- data/demo/pottery_class_demo.rb +0 -164
- data/demo/vcard_models.rb +0 -140
- data/demo/vcard_store_demo.rb +0 -526
- data/lutaml-store.gemspec +0 -36
- data/plan.adoc +0 -606
- data/spec/lutaml/store/adapter_interface_spec.rb +0 -89
- data/spec/lutaml/store/anti_pattern_guard_spec.rb +0 -35
- data/spec/lutaml/store/anti_pattern_spec.rb +0 -78
- data/spec/lutaml/store/autoload_spec.rb +0 -34
- data/spec/lutaml/store/cache_store_spec.rb +0 -271
- data/spec/lutaml/store/compression_spec.rb +0 -78
- data/spec/lutaml/store/config_enhanced_spec.rb +0 -158
- data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +0 -336
- data/spec/lutaml/store/custom_serializer_spec.rb +0 -108
- data/spec/lutaml/store/database_store_spec.rb +0 -279
- data/spec/lutaml/store/file_io_spec.rb +0 -220
- data/spec/lutaml/store/format/yamls_spec.rb +0 -80
- data/spec/lutaml/store/format_round_trip_spec.rb +0 -110
- data/spec/lutaml/store/format_spec.rb +0 -70
- data/spec/lutaml/store/http_cache_entry_spec.rb +0 -203
- data/spec/lutaml/store/http_cache_hal_integration_spec.rb +0 -404
- data/spec/lutaml/store/http_cache_spec.rb +0 -422
- data/spec/lutaml/store/http_header_processor_spec.rb +0 -290
- data/spec/lutaml/store/import_spec.rb +0 -90
- data/spec/lutaml/store/integrity_spec.rb +0 -157
- data/spec/lutaml/store/key_collision_serializer_spec.rb +0 -98
- data/spec/lutaml/store/load_save_spec.rb +0 -107
- data/spec/lutaml/store/lutaml_model_integration_spec.rb +0 -291
- data/spec/lutaml/store/model_serializer_spec.rb +0 -140
- data/spec/lutaml/store/package_definition_spec.rb +0 -89
- data/spec/lutaml/store/package_store_spec.rb +0 -153
- data/spec/lutaml/store/package_transport/directory_transport_spec.rb +0 -139
- data/spec/lutaml/store/package_transport/zip_transport_spec.rb +0 -85
- data/spec/lutaml/store/store_spec.rb +0 -182
- data/spec/lutaml/store_spec.rb +0 -21
- data/spec/spec_helper.rb +0 -16
- data/spec/support/simple_test_model.rb +0 -15
- 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
|