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
|
@@ -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
|
|
@@ -222,15 +222,18 @@ module Lutaml
|
|
|
222
222
|
end
|
|
223
223
|
end
|
|
224
224
|
|
|
225
|
-
def fetch(key, ttl: :default, metadata: {}
|
|
225
|
+
def fetch(key, default = nil, ttl: :default, metadata: {})
|
|
226
226
|
value = get(key)
|
|
227
227
|
return value unless value.nil?
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
229
|
+
if block_given?
|
|
230
|
+
value = yield
|
|
231
|
+
set(key, value, ttl: ttl, metadata: metadata)
|
|
232
|
+
value
|
|
233
|
+
elsif !default.nil?
|
|
234
|
+
set(key, default, ttl: ttl, metadata: metadata)
|
|
235
|
+
default
|
|
236
|
+
end
|
|
234
237
|
end
|
|
235
238
|
|
|
236
239
|
def close
|
data/lib/lutaml/store/format.rb
CHANGED
|
@@ -28,6 +28,25 @@ module Lutaml
|
|
|
28
28
|
|
|
29
29
|
const_get(entry).new
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
def self.for_extension(ext)
|
|
33
|
+
extension_map[ext] || extension_map[".#{ext.to_s.sub(/\A\./, "")}"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private_class_method def self.extension_map
|
|
37
|
+
@extension_map ||= begin
|
|
38
|
+
map = {}
|
|
39
|
+
FORMATS.each_value do |class_name|
|
|
40
|
+
fmt = const_get(class_name).new
|
|
41
|
+
ext = fmt.extension
|
|
42
|
+
next if map.key?(ext)
|
|
43
|
+
|
|
44
|
+
map[ext] = fmt
|
|
45
|
+
map[".yml"] = fmt if ext == ".yaml"
|
|
46
|
+
end
|
|
47
|
+
map.freeze
|
|
48
|
+
end
|
|
49
|
+
end
|
|
31
50
|
end
|
|
32
51
|
end
|
|
33
52
|
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,48 @@
|
|
|
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
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Store
|
|
5
|
+
module PackageTransport
|
|
6
|
+
class DirectoryTransport < Base
|
|
7
|
+
def read(path, package_store, format: nil)
|
|
8
|
+
definition = package_store.definition
|
|
9
|
+
global_format = format
|
|
10
|
+
|
|
11
|
+
read_metadata(path, package_store)
|
|
12
|
+
|
|
13
|
+
definition.model_entries.each do |entry|
|
|
14
|
+
fmt_name = global_format || entry.default_format
|
|
15
|
+
read_model_entry(path, entry, package_store, fmt_name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
definition.asset_entries.each do |entry|
|
|
19
|
+
read_asset_entry(path, entry, package_store)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def write(path, package_store, formats: {})
|
|
24
|
+
definition = package_store.definition
|
|
25
|
+
FileUtils.mkdir_p(path)
|
|
26
|
+
|
|
27
|
+
write_metadata(path, package_store)
|
|
28
|
+
|
|
29
|
+
definition.model_entries.each do |entry|
|
|
30
|
+
fmt_name = effective_format(entry, formats)
|
|
31
|
+
fmt = resolve_format(fmt_name)
|
|
32
|
+
write_model_entry(path, entry, package_store, fmt)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
definition.asset_entries.each do |entry|
|
|
36
|
+
write_asset_entry(path, entry, package_store)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def read_metadata(path, package_store)
|
|
43
|
+
definition = package_store.definition
|
|
44
|
+
return unless definition.metadata_model && definition.metadata_file
|
|
45
|
+
|
|
46
|
+
file_path = File.join(path, definition.metadata_file)
|
|
47
|
+
return unless File.exist?(file_path)
|
|
48
|
+
|
|
49
|
+
fmt = format_for_file(definition.metadata_file)
|
|
50
|
+
raw = File.read(file_path, encoding: "utf-8")
|
|
51
|
+
metadata = fmt.deserialize(raw, definition.metadata_model)
|
|
52
|
+
package_store.metadata = metadata
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def write_metadata(path, package_store)
|
|
56
|
+
return unless package_store.metadata
|
|
57
|
+
|
|
58
|
+
definition = package_store.definition
|
|
59
|
+
fmt = format_for_file(definition.metadata_file)
|
|
60
|
+
content = fmt.serialize(package_store.metadata)
|
|
61
|
+
file_path = File.join(path, definition.metadata_file)
|
|
62
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
63
|
+
File.write(file_path, content, encoding: "utf-8")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def read_model_entry(base_path, entry, package_store, fmt_name)
|
|
67
|
+
if entry.file
|
|
68
|
+
read_single_file_model(base_path, entry, package_store, fmt_name)
|
|
69
|
+
else
|
|
70
|
+
read_directory_models(base_path, entry, package_store, fmt_name)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def read_single_file_model(base_path, entry, package_store, fmt_name)
|
|
75
|
+
file_path = File.join(base_path, entry.file)
|
|
76
|
+
return unless File.exist?(file_path)
|
|
77
|
+
|
|
78
|
+
fmt = resolve_format(fmt_name)
|
|
79
|
+
raw = File.read(file_path, encoding: "utf-8")
|
|
80
|
+
model = fmt.deserialize(raw, entry.model)
|
|
81
|
+
package_store.add_model(model)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def read_directory_models(base_path, entry, package_store, fmt_name)
|
|
85
|
+
dir = entry.dir ? File.join(base_path, entry.dir) : base_path
|
|
86
|
+
return unless Dir.exist?(dir)
|
|
87
|
+
|
|
88
|
+
fmt = resolve_format(fmt_name)
|
|
89
|
+
glob = File.join(dir, fmt.glob_pattern)
|
|
90
|
+
|
|
91
|
+
Dir.glob(glob).sort.each do |file_path|
|
|
92
|
+
next unless File.file?(file_path)
|
|
93
|
+
|
|
94
|
+
raw = File.read(file_path, encoding: "utf-8")
|
|
95
|
+
next if raw.strip.empty?
|
|
96
|
+
|
|
97
|
+
begin
|
|
98
|
+
case entry.layout
|
|
99
|
+
when :grouped
|
|
100
|
+
fmt.deserialize_many(raw, entry.model).each do |m|
|
|
101
|
+
set_key_from_filename(m, file_path, entry)
|
|
102
|
+
package_store.add_model(m)
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
model = fmt.deserialize(raw, entry.model)
|
|
106
|
+
set_key_from_filename(model, file_path, entry)
|
|
107
|
+
package_store.add_model(model)
|
|
108
|
+
end
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
warn "PackageStore: failed to load #{file_path}: #{e.message}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def write_model_entry(base_path, entry, package_store, fmt)
|
|
116
|
+
models = package_store.models_for(entry.model)
|
|
117
|
+
return if models.empty?
|
|
118
|
+
|
|
119
|
+
if entry.file
|
|
120
|
+
write_single_file_model(base_path, entry, models, fmt)
|
|
121
|
+
else
|
|
122
|
+
write_directory_models(base_path, entry, models, fmt)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def write_single_file_model(base_path, entry, models, fmt)
|
|
127
|
+
content = entry.layout == :grouped ? fmt.serialize_many(models) : fmt.serialize(models.first)
|
|
128
|
+
file_path = File.join(base_path, entry.file)
|
|
129
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
130
|
+
File.write(file_path, content, encoding: "utf-8")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def write_directory_models(base_path, entry, models, fmt)
|
|
134
|
+
dir = entry.dir ? File.join(base_path, entry.dir) : base_path
|
|
135
|
+
FileUtils.mkdir_p(dir)
|
|
136
|
+
|
|
137
|
+
case entry.layout
|
|
138
|
+
when :grouped
|
|
139
|
+
models.group_by { |m| extract_key(m, entry) }.each do |key, group|
|
|
140
|
+
file_path = File.join(dir, "#{sanitize_filename(key)}#{fmt.extension}")
|
|
141
|
+
File.write(file_path, fmt.serialize_many(group), encoding: "utf-8")
|
|
142
|
+
end
|
|
143
|
+
else
|
|
144
|
+
models.each do |model|
|
|
145
|
+
key = extract_key(model, entry)
|
|
146
|
+
file_path = File.join(dir, "#{sanitize_filename(key)}#{fmt.extension}")
|
|
147
|
+
File.write(file_path, fmt.serialize(model), encoding: "utf-8")
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def read_asset_entry(base_path, entry, package_store)
|
|
153
|
+
full_path = File.join(base_path, entry.path)
|
|
154
|
+
case entry.type
|
|
155
|
+
when :file
|
|
156
|
+
return unless File.exist?(full_path)
|
|
157
|
+
|
|
158
|
+
package_store.add_asset(entry.path, File.binread(full_path))
|
|
159
|
+
when :directory
|
|
160
|
+
return unless Dir.exist?(full_path)
|
|
161
|
+
|
|
162
|
+
Dir.glob(File.join(full_path, "**", "*")).each do |file|
|
|
163
|
+
next unless File.file?(file)
|
|
164
|
+
|
|
165
|
+
relative = file.sub(%r{\A#{Regexp.escape(base_path)}/}, "")
|
|
166
|
+
package_store.add_asset(relative, File.binread(file))
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def write_asset_entry(base_path, entry, package_store)
|
|
172
|
+
case entry.type
|
|
173
|
+
when :file
|
|
174
|
+
content = package_store.asset(entry.path)
|
|
175
|
+
return unless content
|
|
176
|
+
|
|
177
|
+
file_path = File.join(base_path, entry.path)
|
|
178
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
179
|
+
File.binwrite(file_path, content)
|
|
180
|
+
when :directory
|
|
181
|
+
package_store.asset_paths
|
|
182
|
+
.select { |p| p.start_with?("#{entry.path}/") }
|
|
183
|
+
.each do |asset_path|
|
|
184
|
+
content = package_store.asset(asset_path)
|
|
185
|
+
next unless content
|
|
186
|
+
|
|
187
|
+
file_path = File.join(base_path, asset_path)
|
|
188
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
189
|
+
File.binwrite(file_path, content)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zip"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Store
|
|
7
|
+
module PackageTransport
|
|
8
|
+
class ZipTransport < Base
|
|
9
|
+
def read(path, package_store, format: nil)
|
|
10
|
+
definition = package_store.definition
|
|
11
|
+
global_format = format
|
|
12
|
+
|
|
13
|
+
Zip::File.open(path) do |zip_file|
|
|
14
|
+
read_metadata_zip(zip_file, package_store)
|
|
15
|
+
|
|
16
|
+
definition.model_entries.each do |entry|
|
|
17
|
+
fmt_name = global_format || entry.default_format
|
|
18
|
+
read_model_entry_zip(zip_file, entry, package_store, fmt_name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
definition.asset_entries.each do |entry|
|
|
22
|
+
read_asset_entry_zip(zip_file, entry, package_store)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def write(path, package_store, formats: {})
|
|
28
|
+
definition = package_store.definition
|
|
29
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
30
|
+
|
|
31
|
+
Zip::File.open(path, create: true) do |zip_file|
|
|
32
|
+
write_metadata_zip(zip_file, package_store)
|
|
33
|
+
|
|
34
|
+
definition.model_entries.each do |entry|
|
|
35
|
+
fmt_name = effective_format(entry, formats)
|
|
36
|
+
fmt = resolve_format(fmt_name)
|
|
37
|
+
write_model_entry_zip(zip_file, entry, package_store, fmt)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
write_assets_zip(zip_file, package_store)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def read_metadata_zip(zip_file, package_store)
|
|
47
|
+
definition = package_store.definition
|
|
48
|
+
return unless definition.metadata_model && definition.metadata_file
|
|
49
|
+
|
|
50
|
+
entry = zip_file.find_entry(definition.metadata_file)
|
|
51
|
+
return unless entry
|
|
52
|
+
|
|
53
|
+
fmt = format_for_file(definition.metadata_file)
|
|
54
|
+
raw = entry.get_input_stream.read
|
|
55
|
+
metadata = fmt.deserialize(raw, definition.metadata_model)
|
|
56
|
+
package_store.metadata = metadata
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def write_metadata_zip(zip_file, package_store)
|
|
60
|
+
return unless package_store.metadata
|
|
61
|
+
|
|
62
|
+
definition = package_store.definition
|
|
63
|
+
fmt = format_for_file(definition.metadata_file)
|
|
64
|
+
content = fmt.serialize(package_store.metadata)
|
|
65
|
+
zip_file.get_output_stream(definition.metadata_file) { |f| f.write(content) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def read_model_entry_zip(zip_file, entry, package_store, fmt_name)
|
|
69
|
+
fmt = resolve_format(fmt_name)
|
|
70
|
+
|
|
71
|
+
if entry.file
|
|
72
|
+
read_single_file_zip(zip_file, entry, package_store, fmt)
|
|
73
|
+
else
|
|
74
|
+
read_directory_zip(zip_file, entry, package_store, fmt)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def read_single_file_zip(zip_file, entry, package_store, fmt)
|
|
79
|
+
zip_entry = zip_file.find_entry(entry.file)
|
|
80
|
+
return unless zip_entry
|
|
81
|
+
|
|
82
|
+
raw = zip_entry.get_input_stream.read
|
|
83
|
+
model = fmt.deserialize(raw, entry.model)
|
|
84
|
+
package_store.add_model(model)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def read_directory_zip(zip_file, entry, package_store, fmt)
|
|
88
|
+
prefix = entry.dir ? "#{entry.dir}/" : ""
|
|
89
|
+
|
|
90
|
+
zip_file.entries.each do |zip_entry|
|
|
91
|
+
next unless zip_entry.name.start_with?(prefix)
|
|
92
|
+
next unless matches_format?(zip_entry.name, fmt)
|
|
93
|
+
next if zip_entry.name == prefix || zip_entry.name.end_with?("/")
|
|
94
|
+
|
|
95
|
+
raw = zip_entry.get_input_stream.read
|
|
96
|
+
next if raw.strip.empty?
|
|
97
|
+
|
|
98
|
+
begin
|
|
99
|
+
loaded = fmt.deserialize_many(raw, entry.model)
|
|
100
|
+
loaded = [loaded] unless loaded.is_a?(Array)
|
|
101
|
+
loaded.each do |m|
|
|
102
|
+
set_key_from_zip_path(m, zip_entry.name, entry, prefix)
|
|
103
|
+
package_store.add_model(m)
|
|
104
|
+
end
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
warn "PackageStore: failed to load #{zip_entry.name}: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def write_model_entry_zip(zip_file, entry, package_store, fmt)
|
|
112
|
+
models = package_store.models_for(entry.model)
|
|
113
|
+
return if models.empty?
|
|
114
|
+
|
|
115
|
+
if entry.file
|
|
116
|
+
content = entry.layout == :grouped ? fmt.serialize_many(models) : fmt.serialize(models.first)
|
|
117
|
+
zip_file.get_output_stream(entry.file) { |f| f.write(content) }
|
|
118
|
+
else
|
|
119
|
+
write_directory_models_zip(zip_file, entry, models, fmt)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def write_directory_models_zip(zip_file, entry, models, fmt)
|
|
124
|
+
prefix = entry.dir ? "#{entry.dir}/" : ""
|
|
125
|
+
|
|
126
|
+
case entry.layout
|
|
127
|
+
when :grouped
|
|
128
|
+
models.group_by { |m| extract_key(m, entry) }.each do |key, group|
|
|
129
|
+
filename = "#{prefix}#{sanitize_filename(key)}#{fmt.extension}"
|
|
130
|
+
zip_file.get_output_stream(filename) { |f| f.write(fmt.serialize_many(group)) }
|
|
131
|
+
end
|
|
132
|
+
else
|
|
133
|
+
models.each do |model|
|
|
134
|
+
key = extract_key(model, entry)
|
|
135
|
+
filename = "#{prefix}#{sanitize_filename(key)}#{fmt.extension}"
|
|
136
|
+
zip_file.get_output_stream(filename) { |f| f.write(fmt.serialize(model)) }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def read_asset_entry_zip(zip_file, entry, package_store)
|
|
142
|
+
case entry.type
|
|
143
|
+
when :file
|
|
144
|
+
zip_entry = zip_file.find_entry(entry.path)
|
|
145
|
+
package_store.add_asset(entry.path, zip_entry.get_input_stream.read) if zip_entry
|
|
146
|
+
when :directory
|
|
147
|
+
prefix = entry.path.end_with?("/") ? entry.path : "#{entry.path}/"
|
|
148
|
+
zip_file.entries.each do |zip_entry|
|
|
149
|
+
next unless zip_entry.name.start_with?(prefix)
|
|
150
|
+
next if zip_entry.name == prefix || zip_entry.name.end_with?("/")
|
|
151
|
+
|
|
152
|
+
package_store.add_asset(zip_entry.name, zip_entry.get_input_stream.read)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def write_assets_zip(zip_file, package_store)
|
|
158
|
+
package_store.asset_paths.each do |asset_path|
|
|
159
|
+
content = package_store.asset(asset_path)
|
|
160
|
+
zip_file.get_output_stream(asset_path) { |f| f.write(content) } if content
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def set_key_from_zip_path(model, zip_path, entry, prefix)
|
|
165
|
+
return if model.public_send(entry.key)
|
|
166
|
+
|
|
167
|
+
filename = zip_path.sub(prefix, "")
|
|
168
|
+
set_key_from_filename(model, filename, entry)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def matches_format?(name, fmt)
|
|
172
|
+
ext = File.extname(name)
|
|
173
|
+
ext == fmt.extension || (fmt.extension == ".yaml" && ext == ".yml")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|