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.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +27 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +10 -0
- data/.rubocop_todo.yml +450 -0
- data/CLAUDE.md +57 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +209 -0
- data/CORRECTED_HTTP_CACHE_PLAN.md +164 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +220 -0
- data/README.adoc +1430 -0
- data/Rakefile +12 -0
- data/TODO.impl/0-lutaml-store-self-quality.md +112 -0
- data/TODO.impl/1-lutaml-hal-migration.md +60 -0
- data/TODO.impl/2-glossarist-migration.md +359 -0
- data/TODO.impl/3-lutaml-jsonschema-migration.md +273 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/demo/Gemfile +15 -0
- data/demo/Gemfile.lock +61 -0
- data/demo/README.adoc +301 -0
- data/demo/data/vcards/co/contact_10_thompson.data +1 -0
- data/demo/data/vcards/co/contact_10_thompson.meta +1 -0
- data/demo/data/vcards/co/contact_1_doe.data +1 -0
- data/demo/data/vcards/co/contact_1_doe.meta +1 -0
- data/demo/data/vcards/co/contact_2_smith.data +1 -0
- data/demo/data/vcards/co/contact_2_smith.meta +1 -0
- data/demo/data/vcards/co/contact_3_johnson.data +1 -0
- data/demo/data/vcards/co/contact_3_johnson.meta +1 -0
- data/demo/data/vcards/co/contact_4_garcia.data +1 -0
- data/demo/data/vcards/co/contact_4_garcia.meta +1 -0
- data/demo/data/vcards/co/contact_5_wilson.data +1 -0
- data/demo/data/vcards/co/contact_5_wilson.meta +1 -0
- data/demo/data/vcards/co/contact_6_brown.data +1 -0
- data/demo/data/vcards/co/contact_6_brown.meta +1 -0
- data/demo/data/vcards/co/contact_7_davis.data +1 -0
- data/demo/data/vcards/co/contact_7_davis.meta +1 -0
- data/demo/data/vcards/co/contact_8_anderson.data +1 -0
- data/demo/data/vcards/co/contact_8_anderson.meta +1 -0
- data/demo/data/vcards/co/contact_9_taylor.data +1 -0
- data/demo/data/vcards/co/contact_9_taylor.meta +1 -0
- data/demo/data/vcards.db +0 -0
- data/demo/pottery_class_demo.rb +164 -0
- data/demo/vcard_models.rb +140 -0
- data/demo/vcard_store_demo.rb +526 -0
- data/lib/lutaml/store/adapter/base.rb +65 -0
- data/lib/lutaml/store/adapter/filesystem.rb +288 -0
- data/lib/lutaml/store/adapter/memory.rb +225 -0
- data/lib/lutaml/store/adapter/sqlite.rb +193 -0
- data/lib/lutaml/store/adapter.rb +12 -0
- data/lib/lutaml/store/attribute_updater.rb +198 -0
- data/lib/lutaml/store/basic_store.rb +190 -0
- data/lib/lutaml/store/cache.rb +108 -0
- data/lib/lutaml/store/cache_store.rb +282 -0
- data/lib/lutaml/store/composite_model_handler.rb +169 -0
- data/lib/lutaml/store/compression.rb +137 -0
- data/lib/lutaml/store/config.rb +178 -0
- data/lib/lutaml/store/database_store.rb +425 -0
- data/lib/lutaml/store/events.rb +92 -0
- data/lib/lutaml/store/format/base.rb +33 -0
- data/lib/lutaml/store/format/json.rb +25 -0
- data/lib/lutaml/store/format/jsonl.rb +37 -0
- data/lib/lutaml/store/format/marshal_format.rb +37 -0
- data/lib/lutaml/store/format/yaml.rb +29 -0
- data/lib/lutaml/store/format/yamls.rb +35 -0
- data/lib/lutaml/store/format.rb +33 -0
- data/lib/lutaml/store/http_cache.rb +279 -0
- data/lib/lutaml/store/http_cache_config.rb +53 -0
- data/lib/lutaml/store/http_cache_entry.rb +69 -0
- data/lib/lutaml/store/http_header_processor.rb +175 -0
- data/lib/lutaml/store/integrity.rb +102 -0
- data/lib/lutaml/store/model_registration.rb +75 -0
- data/lib/lutaml/store/model_registry.rb +123 -0
- data/lib/lutaml/store/model_serializer.rb +69 -0
- data/lib/lutaml/store/monitor.rb +192 -0
- data/lib/lutaml/store/storage_key.rb +40 -0
- data/lib/lutaml/store/version.rb +7 -0
- data/lib/lutaml/store.rb +41 -0
- data/lutaml-store.gemspec +35 -0
- data/plan.adoc +606 -0
- data/sig/lutaml/store.rbs +6 -0
- data/spec/lutaml/store/adapter_interface_spec.rb +89 -0
- data/spec/lutaml/store/anti_pattern_guard_spec.rb +35 -0
- data/spec/lutaml/store/anti_pattern_spec.rb +78 -0
- data/spec/lutaml/store/autoload_spec.rb +34 -0
- data/spec/lutaml/store/cache_store_spec.rb +271 -0
- data/spec/lutaml/store/compression_spec.rb +78 -0
- data/spec/lutaml/store/config_enhanced_spec.rb +158 -0
- data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +336 -0
- data/spec/lutaml/store/custom_serializer_spec.rb +108 -0
- data/spec/lutaml/store/database_store_spec.rb +279 -0
- data/spec/lutaml/store/file_io_spec.rb +219 -0
- data/spec/lutaml/store/format_round_trip_spec.rb +110 -0
- data/spec/lutaml/store/format_spec.rb +70 -0
- data/spec/lutaml/store/http_cache_entry_spec.rb +203 -0
- data/spec/lutaml/store/http_cache_hal_integration_spec.rb +404 -0
- data/spec/lutaml/store/http_cache_spec.rb +422 -0
- data/spec/lutaml/store/http_header_processor_spec.rb +290 -0
- data/spec/lutaml/store/import_spec.rb +90 -0
- data/spec/lutaml/store/integrity_spec.rb +157 -0
- data/spec/lutaml/store/key_collision_serializer_spec.rb +98 -0
- data/spec/lutaml/store/load_save_spec.rb +107 -0
- data/spec/lutaml/store/lutaml_model_integration_spec.rb +291 -0
- data/spec/lutaml/store/model_serializer_spec.rb +140 -0
- data/spec/lutaml/store/store_spec.rb +182 -0
- data/spec/lutaml/store_spec.rb +21 -0
- data/spec/spec_helper.rb +16 -0
- metadata +166 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
class ImportTestItem < Lutaml::Model::Serializable
|
|
7
|
+
attribute :name, :string
|
|
8
|
+
attribute :category, :string
|
|
9
|
+
|
|
10
|
+
key_value do
|
|
11
|
+
map :name, to: :name
|
|
12
|
+
map :category, to: :category
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
RSpec.describe "Lutaml::Store import workflow" do
|
|
17
|
+
let(:tmpdir) { Dir.mktmpdir }
|
|
18
|
+
after { FileUtils.rm_rf(tmpdir) }
|
|
19
|
+
|
|
20
|
+
let(:store) do
|
|
21
|
+
Lutaml::Store.new(
|
|
22
|
+
adapter: :memory,
|
|
23
|
+
models: [{ model: ImportTestItem, key: :name, dir: "items" }]
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
let(:items) do
|
|
28
|
+
[
|
|
29
|
+
ImportTestItem.new(name: "alpha", category: "primary"),
|
|
30
|
+
ImportTestItem.new(name: "beta", category: "secondary"),
|
|
31
|
+
ImportTestItem.new(name: "gamma", category: "primary")
|
|
32
|
+
]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe "#import_all" do
|
|
36
|
+
before do
|
|
37
|
+
store.save_all(items, path: tmpdir, format: :yaml, layout: :separate)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "loads from directory and stores into key-value backend" do
|
|
41
|
+
fresh_store = Lutaml::Store.new(
|
|
42
|
+
adapter: :memory,
|
|
43
|
+
models: [{ model: ImportTestItem, key: :name, dir: "items" }]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
loaded = fresh_store.import_all(ImportTestItem, path: tmpdir, format: :yaml, layout: :separate)
|
|
47
|
+
expect(loaded.size).to eq(3)
|
|
48
|
+
|
|
49
|
+
# Now queryable via fetch
|
|
50
|
+
fetched = fresh_store.fetch(model: ImportTestItem, name: "alpha")
|
|
51
|
+
expect(fetched).not_to be_nil
|
|
52
|
+
expect(fetched.name).to eq("alpha")
|
|
53
|
+
expect(fetched.category).to eq("primary")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "supports where queries after import" do
|
|
57
|
+
fresh_store = Lutaml::Store.new(
|
|
58
|
+
adapter: :memory,
|
|
59
|
+
models: [{ model: ImportTestItem, key: :name, dir: "items" }]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
fresh_store.import_all(ImportTestItem, path: tmpdir, format: :yaml, layout: :separate)
|
|
63
|
+
|
|
64
|
+
primary = fresh_store.where(model: ImportTestItem, category: "primary")
|
|
65
|
+
expect(primary.size).to eq(2)
|
|
66
|
+
expect(primary.map(&:name).sort).to eq(%w[alpha gamma])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "supports count after import" do
|
|
70
|
+
fresh_store = Lutaml::Store.new(
|
|
71
|
+
adapter: :memory,
|
|
72
|
+
models: [{ model: ImportTestItem, key: :name, dir: "items" }]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
fresh_store.import_all(ImportTestItem, path: tmpdir, format: :yaml, layout: :separate)
|
|
76
|
+
expect(fresh_store.count(model: ImportTestItem)).to eq(3)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "supports exists? after import" do
|
|
80
|
+
fresh_store = Lutaml::Store.new(
|
|
81
|
+
adapter: :memory,
|
|
82
|
+
models: [{ model: ImportTestItem, key: :name, dir: "items" }]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
fresh_store.import_all(ImportTestItem, path: tmpdir, format: :yaml, layout: :separate)
|
|
86
|
+
expect(fresh_store.exists?(model: ImportTestItem, name: "beta")).to be true
|
|
87
|
+
expect(fresh_store.exists?(model: ImportTestItem, name: "missing")).to be false
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Lutaml::Store::Integrity do
|
|
6
|
+
describe ".calculate_checksum" do
|
|
7
|
+
let(:test_data) { "Hello, World!" }
|
|
8
|
+
|
|
9
|
+
it "calculates SHA256 checksum by default" do
|
|
10
|
+
checksum = described_class.calculate_checksum(test_data)
|
|
11
|
+
expect(checksum).to eq(Digest::SHA256.hexdigest(test_data))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "calculates MD5 checksum" do
|
|
15
|
+
checksum = described_class.calculate_checksum(test_data, "md5")
|
|
16
|
+
expect(checksum).to eq(Digest::MD5.hexdigest(test_data))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "calculates SHA1 checksum" do
|
|
20
|
+
checksum = described_class.calculate_checksum(test_data, "sha1")
|
|
21
|
+
expect(checksum).to eq(Digest::SHA1.hexdigest(test_data))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "raises error for unsupported algorithm" do
|
|
25
|
+
expect do
|
|
26
|
+
described_class.calculate_checksum(test_data, "unsupported")
|
|
27
|
+
end.to raise_error(ArgumentError, /Unsupported checksum algorithm/)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe ".verify_checksum" do
|
|
32
|
+
let(:test_data) { "Hello, World!" }
|
|
33
|
+
let(:correct_checksum) { Digest::SHA256.hexdigest(test_data) }
|
|
34
|
+
let(:incorrect_checksum) { "incorrect" }
|
|
35
|
+
|
|
36
|
+
it "returns true for correct checksum" do
|
|
37
|
+
expect(described_class.verify_checksum(test_data, correct_checksum)).to be true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "raises ChecksumMismatchError for incorrect checksum" do
|
|
41
|
+
expect do
|
|
42
|
+
described_class.verify_checksum(test_data, incorrect_checksum)
|
|
43
|
+
end.to raise_error(Lutaml::Store::Integrity::ChecksumMismatchError)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe ".create_integrity_metadata" do
|
|
48
|
+
let(:test_data) { "Hello, World!" }
|
|
49
|
+
|
|
50
|
+
it "creates integrity metadata with checksum and size" do
|
|
51
|
+
metadata = described_class.create_integrity_metadata(test_data)
|
|
52
|
+
|
|
53
|
+
expect(metadata).to have_key(:checksum)
|
|
54
|
+
expect(metadata).to have_key(:algorithm)
|
|
55
|
+
expect(metadata).to have_key(:size)
|
|
56
|
+
expect(metadata).to have_key(:created_at)
|
|
57
|
+
expect(metadata).to have_key(:version)
|
|
58
|
+
|
|
59
|
+
expect(metadata[:checksum]).to eq(Digest::SHA256.hexdigest(test_data))
|
|
60
|
+
expect(metadata[:algorithm]).to eq("sha256")
|
|
61
|
+
expect(metadata[:size]).to eq(test_data.bytesize)
|
|
62
|
+
expect(metadata[:version]).to eq("1.0")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "uses specified algorithm" do
|
|
66
|
+
metadata = described_class.create_integrity_metadata(test_data, "md5")
|
|
67
|
+
|
|
68
|
+
expect(metadata[:checksum]).to eq(Digest::MD5.hexdigest(test_data))
|
|
69
|
+
expect(metadata[:algorithm]).to eq("md5")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe ".verify_integrity_metadata" do
|
|
74
|
+
let(:test_data) { "Hello, World!" }
|
|
75
|
+
let(:valid_metadata) do
|
|
76
|
+
{
|
|
77
|
+
checksum: Digest::SHA256.hexdigest(test_data),
|
|
78
|
+
algorithm: "sha256",
|
|
79
|
+
size: test_data.bytesize
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "returns true for valid metadata" do
|
|
84
|
+
expect(described_class.verify_integrity_metadata(test_data, valid_metadata)).to be true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "raises CorruptionError for size mismatch" do
|
|
88
|
+
invalid_metadata = valid_metadata.merge(size: 999)
|
|
89
|
+
|
|
90
|
+
expect do
|
|
91
|
+
described_class.verify_integrity_metadata(test_data, invalid_metadata)
|
|
92
|
+
end.to raise_error(Lutaml::Store::Integrity::CorruptionError, /Size mismatch/)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "raises ChecksumMismatchError for checksum mismatch" do
|
|
96
|
+
invalid_metadata = valid_metadata.merge(checksum: "invalid")
|
|
97
|
+
|
|
98
|
+
expect do
|
|
99
|
+
described_class.verify_integrity_metadata(test_data, invalid_metadata)
|
|
100
|
+
end.to raise_error(Lutaml::Store::Integrity::ChecksumMismatchError)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "returns true when no integrity metadata is present" do
|
|
104
|
+
expect(described_class.verify_integrity_metadata(test_data, {})).to be true
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe ".repair_data" do
|
|
109
|
+
let(:corrupted_data) { "Hello, Wor\x00ld!" }
|
|
110
|
+
let(:backup_data) { "Hello, World!" }
|
|
111
|
+
|
|
112
|
+
it "returns backup data if available and valid" do
|
|
113
|
+
repaired = described_class.repair_data(corrupted_data, backup_data)
|
|
114
|
+
expect(repaired).to eq(backup_data)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "attempts to clean corrupted data" do
|
|
118
|
+
repaired = described_class.repair_data(corrupted_data)
|
|
119
|
+
expect(repaired).to eq("Hello, World!")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "fixes truncated JSON" do
|
|
123
|
+
corrupted_json = '{"key": "value"'
|
|
124
|
+
repaired = described_class.repair_data(corrupted_json)
|
|
125
|
+
expect(repaired).to eq('{"key": "value"}')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "fixes truncated arrays" do
|
|
129
|
+
corrupted_array = '["item1", "item2"'
|
|
130
|
+
repaired = described_class.repair_data(corrupted_array)
|
|
131
|
+
expect(repaired).to eq('["item1", "item2"]')
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe ".valid_data?" do
|
|
136
|
+
it "returns true for valid UTF-8 string" do
|
|
137
|
+
expect(described_class.valid_data?("Hello, World!")).to be true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "returns false for nil data" do
|
|
141
|
+
expect(described_class.valid_data?(nil)).to be false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "returns false for empty data" do
|
|
145
|
+
expect(described_class.valid_data?("")).to be false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "returns false for data with null bytes" do
|
|
149
|
+
expect(described_class.valid_data?("Hello\x00World")).to be false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "returns true for binary data" do
|
|
153
|
+
binary_data = "\xFF\xFE\xFD".dup.force_encoding("ASCII-8BIT")
|
|
154
|
+
expect(described_class.valid_data?(binary_data)).to be true
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Model with a key collision: both id and code serialize to the same "id" key.
|
|
6
|
+
# Demonstrates the custom serializer pattern for preserving all fields.
|
|
7
|
+
class CollisionItem < Lutaml::Model::Serializable
|
|
8
|
+
attribute :id, :string
|
|
9
|
+
attribute :code, :string
|
|
10
|
+
attribute :label, :string
|
|
11
|
+
|
|
12
|
+
key_value do
|
|
13
|
+
map :id, to: :id
|
|
14
|
+
map :code, with: { to: :code_to_yaml, from: :code_from_yaml }
|
|
15
|
+
map :label, to: :label
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def code_to_yaml(model, doc)
|
|
19
|
+
doc["id"] = model.code if model.code
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def code_from_yaml(model, value)
|
|
23
|
+
model.code = value if value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class CollisionSerializer < Lutaml::Store::ModelSerializer
|
|
28
|
+
def serialize(model, _registration = nil)
|
|
29
|
+
{
|
|
30
|
+
"_yaml" => model.to_yaml,
|
|
31
|
+
"_id" => model.id,
|
|
32
|
+
"_code" => model.code
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def deserialize(data, expected_class, _registration = nil)
|
|
37
|
+
model = expected_class.from_yaml(data["_yaml"])
|
|
38
|
+
model.id = data["_id"]
|
|
39
|
+
model.code = data["_code"]
|
|
40
|
+
model
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
RSpec.describe "Custom serializer with key collision workaround" do
|
|
45
|
+
let(:store) do
|
|
46
|
+
Lutaml::Store.new(
|
|
47
|
+
adapter: :memory,
|
|
48
|
+
models: [{
|
|
49
|
+
model: CollisionItem,
|
|
50
|
+
key: :id,
|
|
51
|
+
serializer: CollisionSerializer.new
|
|
52
|
+
}]
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "preserves both id and code through save/fetch" do
|
|
57
|
+
item = CollisionItem.new(id: "item-1", code: "CODE-1", label: "First item")
|
|
58
|
+
store.save(item)
|
|
59
|
+
|
|
60
|
+
fetched = store.fetch(model: CollisionItem, id: "item-1")
|
|
61
|
+
expect(fetched.id).to eq("item-1")
|
|
62
|
+
expect(fetched.code).to eq("CODE-1")
|
|
63
|
+
expect(fetched.label).to eq("First item")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "preserves fields through update" do
|
|
67
|
+
item = CollisionItem.new(id: "item-2", code: "CODE-2", label: "Original")
|
|
68
|
+
store.save(item)
|
|
69
|
+
|
|
70
|
+
updated = store.update(model: CollisionItem, id: "item-2", attributes: { label: "Updated" })
|
|
71
|
+
expect(updated.id).to eq("item-2")
|
|
72
|
+
expect(updated.code).to eq("CODE-2")
|
|
73
|
+
expect(updated.label).to eq("Updated")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "preserves fields through where query" do
|
|
77
|
+
store.save(CollisionItem.new(id: "a1", code: "C1", label: "One"))
|
|
78
|
+
store.save(CollisionItem.new(id: "a2", code: "C2", label: "Two"))
|
|
79
|
+
|
|
80
|
+
found = store.where(model: CollisionItem, code: "C1")
|
|
81
|
+
expect(found.size).to eq(1)
|
|
82
|
+
expect(found.first.id).to eq("a1")
|
|
83
|
+
expect(found.first.code).to eq("C1")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "lists all items with correct fields" do
|
|
87
|
+
3.times do |i|
|
|
88
|
+
store.save(CollisionItem.new(id: "x-#{i}", code: "C-#{i}", label: "Item #{i}"))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
all = store.all(model: CollisionItem)
|
|
92
|
+
expect(all.size).to eq(3)
|
|
93
|
+
all.each do |item|
|
|
94
|
+
expect(item.id).to start_with("x-")
|
|
95
|
+
expect(item.code).to start_with("C-")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
# Test model using lutaml-model
|
|
7
|
+
class TestItem < 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 load_all / save_all" 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: TestItem, key: :name, dir: "items" }]
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
let(:items) do
|
|
29
|
+
[
|
|
30
|
+
TestItem.new(name: "alpha", value: "first"),
|
|
31
|
+
TestItem.new(name: "beta", value: "second"),
|
|
32
|
+
TestItem.new(name: "gamma", value: "third")
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "#save_all with :yaml format" do
|
|
37
|
+
it "writes YAML files to the model directory" do
|
|
38
|
+
store.save_all(items, path: tmpdir, format: :yaml, layout: :separate)
|
|
39
|
+
|
|
40
|
+
items_dir = File.join(tmpdir, "items")
|
|
41
|
+
expect(Dir.exist?(items_dir)).to be true
|
|
42
|
+
|
|
43
|
+
files = Dir.glob(File.join(items_dir, "*.yaml")).sort
|
|
44
|
+
expect(files.size).to eq(3)
|
|
45
|
+
|
|
46
|
+
content = File.read(files.first, encoding: "utf-8")
|
|
47
|
+
expect(content).to include("name:")
|
|
48
|
+
expect(content).to include("value:")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe "#load_all with :yaml format" do
|
|
53
|
+
it "round-trips models through YAML files" do
|
|
54
|
+
store.save_all(items, path: tmpdir, format: :yaml, layout: :separate)
|
|
55
|
+
|
|
56
|
+
loaded = store.load_all(TestItem, path: tmpdir, format: :yaml, layout: :separate)
|
|
57
|
+
|
|
58
|
+
expect(loaded.size).to eq(3)
|
|
59
|
+
names = loaded.map(&:name).sort
|
|
60
|
+
expect(names).to eq(%w[alpha beta gamma])
|
|
61
|
+
values = loaded.map(&:value).sort
|
|
62
|
+
expect(values).to eq(%w[first second third])
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe "#save_all with :grouped layout" do
|
|
67
|
+
it "writes one file per key" do
|
|
68
|
+
store.save_all(items, path: tmpdir, format: :yaml, layout: :grouped)
|
|
69
|
+
|
|
70
|
+
items_dir = File.join(tmpdir, "items")
|
|
71
|
+
files = Dir.glob(File.join(items_dir, "*.yaml")).sort
|
|
72
|
+
expect(files.size).to eq(3)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe "#save_all with :json format" do
|
|
77
|
+
it "writes JSON files" do
|
|
78
|
+
store.save_all(items, path: tmpdir, format: :json, layout: :separate)
|
|
79
|
+
|
|
80
|
+
items_dir = File.join(tmpdir, "items")
|
|
81
|
+
files = Dir.glob(File.join(items_dir, "*.json")).sort
|
|
82
|
+
expect(files.size).to eq(3)
|
|
83
|
+
|
|
84
|
+
content = File.read(files.first, encoding: "utf-8")
|
|
85
|
+
expect(content).to include('"name"')
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe "#export" do
|
|
90
|
+
it "exports all models to a single file" do
|
|
91
|
+
export_path = File.join(tmpdir, "export.jsonl")
|
|
92
|
+
store.export(items, path: export_path, format: :jsonl)
|
|
93
|
+
|
|
94
|
+
expect(File.exist?(export_path)).to be true
|
|
95
|
+
content = File.read(export_path, encoding: "utf-8")
|
|
96
|
+
lines = content.lines.reject(&:empty?)
|
|
97
|
+
expect(lines.size).to eq(3)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe "empty model list" do
|
|
102
|
+
it "returns empty array for save_all" do
|
|
103
|
+
result = store.save_all([], path: tmpdir, format: :yaml, layout: :separate)
|
|
104
|
+
expect(result).to eq([])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|