lutaml-store 0.2.0 → 0.2.2
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 +13 -177
- data/CLAUDE.md +22 -9
- data/README.adoc +326 -1103
- data/lib/lutaml/store/adapter/base.rb +4 -0
- data/lib/lutaml/store/adapter/memory.rb +8 -0
- data/lib/lutaml/store/adapter.rb +17 -0
- data/lib/lutaml/store/basic_store.rb +1 -10
- data/lib/lutaml/store/cache_store.rb +11 -16
- data/lib/lutaml/store/database_store.rb +19 -9
- data/lib/lutaml/store/format/base.rb +4 -0
- data/lib/lutaml/store/format/marshal_format.rb +4 -0
- data/lib/lutaml/store/format/xml.rb +37 -0
- data/lib/lutaml/store/format/yamls.rb +1 -1
- data/lib/lutaml/store/format.rb +22 -1
- data/lib/lutaml/store/format_serializer.rb +44 -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 +60 -0
- data/lib/lutaml/store/package_transport/directory_transport.rb +196 -0
- data/lib/lutaml/store/package_transport/zip_transport.rb +183 -0
- data/lib/lutaml/store/package_transport.rb +11 -438
- data/lib/lutaml/store/storage_key.rb +1 -1
- data/lib/lutaml/store/version.rb +1 -1
- data/lib/lutaml/store.rb +1 -0
- metadata +14 -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
|
@@ -93,6 +93,14 @@ module Lutaml
|
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
def each_key(&block)
|
|
97
|
+
current_keys = @mutex.synchronize do
|
|
98
|
+
cleanup_expired if @ttl_enabled
|
|
99
|
+
@data.keys
|
|
100
|
+
end
|
|
101
|
+
current_keys.each(&block)
|
|
102
|
+
end
|
|
103
|
+
|
|
96
104
|
def close
|
|
97
105
|
@mutex.synchronize do
|
|
98
106
|
@data.clear
|
data/lib/lutaml/store/adapter.rb
CHANGED
|
@@ -7,6 +7,23 @@ module Lutaml
|
|
|
7
7
|
autoload :Memory, "lutaml/store/adapter/memory"
|
|
8
8
|
autoload :FileSystem, "lutaml/store/adapter/filesystem"
|
|
9
9
|
autoload :Sqlite, "lutaml/store/adapter/sqlite"
|
|
10
|
+
|
|
11
|
+
@registry = {
|
|
12
|
+
memory: "Memory",
|
|
13
|
+
filesystem: "FileSystem",
|
|
14
|
+
sqlite: "Sqlite"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def self.resolve(type, options = {})
|
|
18
|
+
entry = @registry[type.to_sym]
|
|
19
|
+
raise ConfigurationError, "Unknown adapter type: #{type}" unless entry
|
|
20
|
+
|
|
21
|
+
const_get(entry).new(options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.register(type, adapter_class)
|
|
25
|
+
@registry[type.to_sym] = adapter_class.name
|
|
26
|
+
end
|
|
10
27
|
end
|
|
11
28
|
end
|
|
12
29
|
end
|
|
@@ -167,16 +167,7 @@ module Lutaml
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
def create_adapter
|
|
170
|
-
|
|
171
|
-
when :memory
|
|
172
|
-
Adapter::Memory.new(@config.adapter_options)
|
|
173
|
-
when :filesystem
|
|
174
|
-
Adapter::FileSystem.new(@config.adapter_options)
|
|
175
|
-
when :sqlite
|
|
176
|
-
Adapter::Sqlite.new(@config.adapter_options)
|
|
177
|
-
else
|
|
178
|
-
raise ConfigurationError, "Unknown adapter type: #{@config.adapter_type}"
|
|
179
|
-
end
|
|
170
|
+
Adapter.resolve(@config.adapter_type, @config.adapter_options)
|
|
180
171
|
end
|
|
181
172
|
|
|
182
173
|
def create_cache
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "time"
|
|
4
5
|
|
|
5
6
|
module Lutaml
|
|
6
7
|
module Store
|
|
@@ -222,15 +223,18 @@ module Lutaml
|
|
|
222
223
|
end
|
|
223
224
|
end
|
|
224
225
|
|
|
225
|
-
def fetch(key, ttl: :default, metadata: {}
|
|
226
|
+
def fetch(key, default = nil, ttl: :default, metadata: {})
|
|
226
227
|
value = get(key)
|
|
227
228
|
return value unless value.nil?
|
|
228
229
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
if block_given?
|
|
231
|
+
value = yield
|
|
232
|
+
set(key, value, ttl: ttl, metadata: metadata)
|
|
233
|
+
value
|
|
234
|
+
elsif !default.nil?
|
|
235
|
+
set(key, default, ttl: ttl, metadata: metadata)
|
|
236
|
+
default
|
|
237
|
+
end
|
|
234
238
|
end
|
|
235
239
|
|
|
236
240
|
def close
|
|
@@ -243,16 +247,7 @@ module Lutaml
|
|
|
243
247
|
adapter_type = config[:adapter]&.dig(:type) || config[:adapter_type] || :memory
|
|
244
248
|
adapter_options = config[:adapter]&.dig(:options) || config[:adapter_options] || {}
|
|
245
249
|
|
|
246
|
-
|
|
247
|
-
when :memory
|
|
248
|
-
Adapter::Memory.new(adapter_options)
|
|
249
|
-
when :filesystem
|
|
250
|
-
Adapter::FileSystem.new(adapter_options)
|
|
251
|
-
when :sqlite
|
|
252
|
-
Adapter::Sqlite.new(adapter_options)
|
|
253
|
-
else
|
|
254
|
-
raise ConfigurationError, "Unknown adapter type: #{adapter_type}"
|
|
255
|
-
end
|
|
250
|
+
Adapter.resolve(adapter_type, adapter_options)
|
|
256
251
|
end
|
|
257
252
|
|
|
258
253
|
def serialize_entry(entry)
|
|
@@ -207,7 +207,7 @@ module Lutaml
|
|
|
207
207
|
|
|
208
208
|
content = fmt.serialize_many(models_array)
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
write_file(path, content, fmt)
|
|
211
211
|
@store.emit_event(:model_export, count: models_array.size, path: path)
|
|
212
212
|
path
|
|
213
213
|
end
|
|
@@ -331,8 +331,8 @@ module Lutaml
|
|
|
331
331
|
Dir.glob(glob).sort.each do |file_path|
|
|
332
332
|
next unless File.file?(file_path)
|
|
333
333
|
|
|
334
|
-
raw =
|
|
335
|
-
next if raw.strip.empty?
|
|
334
|
+
raw = read_file(file_path, fmt)
|
|
335
|
+
next if !fmt.binary? && raw.strip.empty?
|
|
336
336
|
|
|
337
337
|
begin
|
|
338
338
|
model = fmt.deserialize(raw, model_class)
|
|
@@ -351,8 +351,8 @@ module Lutaml
|
|
|
351
351
|
Dir.glob(glob).sort.each do |file_path|
|
|
352
352
|
next unless File.file?(file_path)
|
|
353
353
|
|
|
354
|
-
raw =
|
|
355
|
-
next if raw.strip.empty?
|
|
354
|
+
raw = read_file(file_path, fmt)
|
|
355
|
+
next if !fmt.binary? && raw.strip.empty?
|
|
356
356
|
|
|
357
357
|
begin
|
|
358
358
|
loaded = fmt.deserialize_many(raw, model_class)
|
|
@@ -375,8 +375,7 @@ module Lutaml
|
|
|
375
375
|
key = extract_model_key(model)
|
|
376
376
|
filename = key || model.class.name.to_s.gsub("::", "_")
|
|
377
377
|
file_path = File.join(dir, "#{filename}#{fmt.extension}")
|
|
378
|
-
|
|
379
|
-
File.write(file_path, content, encoding: "utf-8")
|
|
378
|
+
write_file(file_path, fmt.serialize(model), fmt)
|
|
380
379
|
model
|
|
381
380
|
end
|
|
382
381
|
end
|
|
@@ -391,8 +390,7 @@ module Lutaml
|
|
|
391
390
|
|
|
392
391
|
grouped.map do |key, group|
|
|
393
392
|
file_path = File.join(dir, "#{key}#{fmt.extension}")
|
|
394
|
-
|
|
395
|
-
File.write(file_path, content, encoding: "utf-8")
|
|
393
|
+
write_file(file_path, fmt.serialize_many(group), fmt)
|
|
396
394
|
group
|
|
397
395
|
end.flatten
|
|
398
396
|
end
|
|
@@ -420,6 +418,18 @@ module Lutaml
|
|
|
420
418
|
basename = File.basename(file_path, ".*")
|
|
421
419
|
model.public_send(:"#{registration.key_field}=", basename)
|
|
422
420
|
end
|
|
421
|
+
|
|
422
|
+
def read_file(file_path, fmt)
|
|
423
|
+
fmt.binary? ? File.binread(file_path) : File.read(file_path, encoding: "utf-8")
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def write_file(file_path, content, fmt)
|
|
427
|
+
if fmt.binary?
|
|
428
|
+
File.binwrite(file_path, content)
|
|
429
|
+
else
|
|
430
|
+
File.write(file_path, content, encoding: "utf-8")
|
|
431
|
+
end
|
|
432
|
+
end
|
|
423
433
|
end
|
|
424
434
|
end
|
|
425
435
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Store
|
|
5
|
+
module Format
|
|
6
|
+
class Xml < Base
|
|
7
|
+
def extension
|
|
8
|
+
".xml"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def glob_pattern
|
|
12
|
+
"*.xml"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def serialize(model)
|
|
16
|
+
model.to_xml
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def deserialize(data, model_class)
|
|
20
|
+
model_class.from_xml(data)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def serialize_many(models)
|
|
24
|
+
inner = models.map(&:to_xml).join("\n")
|
|
25
|
+
"<items>\n#{inner}\n</items>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def deserialize_many(data, model_class)
|
|
29
|
+
doc = Moxml.parse(data)
|
|
30
|
+
doc.root.children.select(&:element?).map do |child|
|
|
31
|
+
model_class.from_xml(child.to_xml)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/lutaml/store/format.rb
CHANGED
|
@@ -13,13 +13,15 @@ module Lutaml
|
|
|
13
13
|
autoload :Json, "lutaml/store/format/json"
|
|
14
14
|
autoload :Jsonl, "lutaml/store/format/jsonl"
|
|
15
15
|
autoload :MarshalFormat, "lutaml/store/format/marshal_format"
|
|
16
|
+
autoload :Xml, "lutaml/store/format/xml"
|
|
16
17
|
|
|
17
18
|
FORMATS = {
|
|
18
19
|
yaml: "Yaml",
|
|
19
20
|
yamls: "Yamls",
|
|
20
21
|
json: "Json",
|
|
21
22
|
jsonl: "Jsonl",
|
|
22
|
-
marshal: "MarshalFormat"
|
|
23
|
+
marshal: "MarshalFormat",
|
|
24
|
+
xml: "Xml"
|
|
23
25
|
}.freeze
|
|
24
26
|
|
|
25
27
|
def self.resolve(format)
|
|
@@ -28,6 +30,25 @@ module Lutaml
|
|
|
28
30
|
|
|
29
31
|
const_get(entry).new
|
|
30
32
|
end
|
|
33
|
+
|
|
34
|
+
def self.for_extension(ext)
|
|
35
|
+
extension_map[ext] || extension_map[".#{ext.to_s.sub(/\A\./, "")}"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private_class_method def self.extension_map
|
|
39
|
+
@extension_map ||= begin
|
|
40
|
+
map = {}
|
|
41
|
+
FORMATS.each_value do |class_name|
|
|
42
|
+
fmt = const_get(class_name).new
|
|
43
|
+
ext = fmt.extension
|
|
44
|
+
next if map.key?(ext)
|
|
45
|
+
|
|
46
|
+
map[ext] = fmt
|
|
47
|
+
map[".yml"] = fmt if ext == ".yaml"
|
|
48
|
+
end
|
|
49
|
+
map.freeze
|
|
50
|
+
end
|
|
51
|
+
end
|
|
31
52
|
end
|
|
32
53
|
end
|
|
33
54
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Store
|
|
5
|
+
# Bridges a Format handler to the ModelSerializer interface.
|
|
6
|
+
# Enables DatabaseStore to use any format (yaml, json, xml, yamls, marshal)
|
|
7
|
+
# for key-value storage instead of the default hash serialization.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# serializer = FormatSerializer.new(:yamls)
|
|
11
|
+
# store = DatabaseStore.new(
|
|
12
|
+
# adapter: :sqlite,
|
|
13
|
+
# models: [{ model: ConceptDocument, key: :id, serializer: serializer }]
|
|
14
|
+
# )
|
|
15
|
+
class FormatSerializer
|
|
16
|
+
DATA_KEY = "_data"
|
|
17
|
+
CLASS_KEY = "_class"
|
|
18
|
+
|
|
19
|
+
def initialize(format)
|
|
20
|
+
@format = Format.resolve(format)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def serialize(model)
|
|
24
|
+
{
|
|
25
|
+
DATA_KEY => @format.serialize(model),
|
|
26
|
+
CLASS_KEY => model.class.name
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def deserialize(data, model_class)
|
|
31
|
+
model_class = resolve_class(data[CLASS_KEY]) if data[CLASS_KEY]
|
|
32
|
+
@format.deserialize(data[DATA_KEY], model_class)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def resolve_class(class_name)
|
|
38
|
+
Object.const_get(class_name)
|
|
39
|
+
rescue NameError
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
3
|
module Lutaml
|
|
6
4
|
module Store
|
|
7
5
|
class HttpCache
|
|
@@ -145,9 +143,7 @@ module Lutaml
|
|
|
145
143
|
data = @adapter.get(key)
|
|
146
144
|
next unless data
|
|
147
145
|
|
|
148
|
-
|
|
149
|
-
entry = HttpCacheEntry.from_hash(entry_data)
|
|
150
|
-
entries << entry
|
|
146
|
+
entries << HttpCacheEntry.from_json(data)
|
|
151
147
|
rescue StandardError
|
|
152
148
|
# Skip invalid entries
|
|
153
149
|
end
|
|
@@ -174,20 +170,14 @@ module Lutaml
|
|
|
174
170
|
data = @adapter.get(cache_key)
|
|
175
171
|
return nil unless data
|
|
176
172
|
|
|
177
|
-
|
|
178
|
-
entry_data = data.is_a?(String) ? JSON.parse(data) : data
|
|
179
|
-
HttpCacheEntry.from_hash(entry_data)
|
|
173
|
+
HttpCacheEntry.from_json(data)
|
|
180
174
|
rescue StandardError
|
|
181
|
-
# Log error and continue without cache
|
|
182
175
|
nil
|
|
183
176
|
end
|
|
184
177
|
|
|
185
178
|
def store_entry(cache_key, entry)
|
|
186
|
-
|
|
187
|
-
data = entry.to_hash
|
|
188
|
-
@adapter.set(cache_key, data)
|
|
179
|
+
@adapter.set(cache_key, entry.to_json)
|
|
189
180
|
rescue StandardError
|
|
190
|
-
# Log error but don't fail the request
|
|
191
181
|
false
|
|
192
182
|
end
|
|
193
183
|
|
|
@@ -4,14 +4,17 @@ module Lutaml
|
|
|
4
4
|
module Store
|
|
5
5
|
# Represents a single model registration with its metadata
|
|
6
6
|
class ModelRegistration
|
|
7
|
-
attr_reader :model_class, :key_field, :polymorphic_class_key, :serializer, :dir
|
|
7
|
+
attr_reader :model_class, :key_field, :polymorphic_class_key, :serializer, :dir,
|
|
8
|
+
:composites
|
|
8
9
|
|
|
9
|
-
def initialize(model_class, key_field, polymorphic_class_key: nil, serializer: nil,
|
|
10
|
+
def initialize(model_class, key_field, polymorphic_class_key: nil, serializer: nil,
|
|
11
|
+
dir: nil, composites: [])
|
|
10
12
|
@model_class = model_class
|
|
11
13
|
@key_field = key_field.to_sym
|
|
12
14
|
@polymorphic_class_key = polymorphic_class_key&.to_sym
|
|
13
15
|
@serializer = serializer
|
|
14
16
|
@dir = dir
|
|
17
|
+
@composites = composites.map(&:to_sym)
|
|
15
18
|
validate!
|
|
16
19
|
end
|
|
17
20
|
|
|
@@ -81,34 +81,36 @@ module Lutaml
|
|
|
81
81
|
@registrations.clear
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
# Find
|
|
84
|
+
# Find declared composite models nested within the given model
|
|
85
85
|
def find_composite_models(model)
|
|
86
|
-
|
|
86
|
+
registration = find_registration(model.class)
|
|
87
|
+
return {} unless registration&.composites&.any?
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if attr_value.is_a?(Object) && registered?(attr_value.class)
|
|
93
|
-
add_composite_entry(composite_models, attr_name,
|
|
94
|
-
attr_value)
|
|
95
|
-
end
|
|
89
|
+
registration.composites.each_with_object({}) do |attr_name, result|
|
|
90
|
+
collect_composite(model, attr_name, result)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
96
93
|
|
|
97
|
-
|
|
94
|
+
private
|
|
98
95
|
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
def collect_composite(model, attr_name, result)
|
|
97
|
+
attr_value = model.public_send(attr_name)
|
|
98
|
+
return if attr_value.nil?
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
if attr_value.is_a?(Array)
|
|
101
|
+
collect_array_composites(attr_value, attr_name, result)
|
|
102
|
+
elsif registered?(attr_value.class)
|
|
103
|
+
add_composite_entry(result, attr_name, attr_value)
|
|
104
104
|
end
|
|
105
|
-
|
|
106
|
-
composite_models
|
|
107
|
-
rescue NoMethodError
|
|
108
|
-
{}
|
|
109
105
|
end
|
|
110
106
|
|
|
111
|
-
|
|
107
|
+
def collect_array_composites(values, attr_name, result)
|
|
108
|
+
values.each_with_index do |item, index|
|
|
109
|
+
next unless registered?(item.class)
|
|
110
|
+
|
|
111
|
+
add_composite_entry(result, "#{attr_name}.#{index}", item)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
112
114
|
|
|
113
115
|
def add_composite_entry(composite_models, attr_path, model_instance)
|
|
114
116
|
registration = find_registration(model_instance.class)
|
|
@@ -20,15 +20,13 @@ module Lutaml
|
|
|
20
20
|
|
|
21
21
|
def self.load(definition, path, transport: :directory, format: nil)
|
|
22
22
|
store = new(definition)
|
|
23
|
-
|
|
24
|
-
transporter.read(path, store, format: format)
|
|
23
|
+
PackageTransport.resolve(transport).read(path, store, format: format)
|
|
25
24
|
store
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
def save(path, transport: :directory, format: nil, formats: {})
|
|
29
28
|
resolved_formats = resolve_formats(format, formats)
|
|
30
|
-
|
|
31
|
-
transporter.write(path, self, formats: resolved_formats)
|
|
29
|
+
PackageTransport.resolve(transport).write(path, self, formats: resolved_formats)
|
|
32
30
|
end
|
|
33
31
|
|
|
34
32
|
# ── Model CRUD ──
|
|
@@ -118,22 +116,8 @@ module Lutaml
|
|
|
118
116
|
}
|
|
119
117
|
end
|
|
120
118
|
|
|
121
|
-
# Public access for PackageTransport (avoids instance_variable_get).
|
|
122
|
-
attr_reader :db
|
|
123
|
-
|
|
124
119
|
private
|
|
125
120
|
|
|
126
|
-
def self.resolve_transport(transport)
|
|
127
|
-
case transport
|
|
128
|
-
when :directory, "directory"
|
|
129
|
-
PackageTransport::DirectoryTransport.new
|
|
130
|
-
when :zip, "zip"
|
|
131
|
-
PackageTransport::ZipTransport.new
|
|
132
|
-
else
|
|
133
|
-
raise ConfigurationError, "Unknown transport: #{transport}"
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
121
|
def resolve_formats(global_format, per_model_formats)
|
|
138
122
|
if global_format
|
|
139
123
|
definition.model_entries
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Store
|
|
7
|
+
module PackageTransport
|
|
8
|
+
class Base
|
|
9
|
+
def read(path, package_store, format: nil)
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def write(path, package_store, formats: {})
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def resolve_format(format_name)
|
|
20
|
+
Format.resolve(format_name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def effective_format(entry, formats)
|
|
24
|
+
formats[entry.model] || entry.default_format
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def format_for_file(filename)
|
|
28
|
+
Format.for_extension(File.extname(filename)) || Format.resolve(:yaml)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def extract_key(model, entry)
|
|
32
|
+
model.public_send(entry.key).to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set_key_from_filename(model, filename, entry)
|
|
36
|
+
return if model.public_send(entry.key)
|
|
37
|
+
|
|
38
|
+
basename = File.basename(filename, ".*")
|
|
39
|
+
model.public_send(:"#{entry.key}=", basename)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def sanitize_filename(key)
|
|
43
|
+
key.gsub(%r{[/:#?]}, "_")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def read_file(file_path, fmt)
|
|
47
|
+
fmt.binary? ? File.binread(file_path) : File.read(file_path, encoding: "utf-8")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def write_file(file_path, content, fmt)
|
|
51
|
+
if fmt.binary?
|
|
52
|
+
File.binwrite(file_path, content)
|
|
53
|
+
else
|
|
54
|
+
File.write(file_path, content, encoding: "utf-8")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|