lutaml-store 0.1.1 → 0.2.0
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 +79 -275
- data/Gemfile.lock +11 -4
- data/TODO.impl/1-lutaml-hal-migration.md +54 -18
- data/lib/lutaml/store/attribute_updater.rb +12 -3
- data/lib/lutaml/store/basic_store.rb +4 -0
- data/lib/lutaml/store/cache_store.rb +1 -1
- data/lib/lutaml/store/database_store.rb +1 -1
- data/lib/lutaml/store/format/yamls.rb +2 -2
- data/lib/lutaml/store/http_cache.rb +4 -1
- data/lib/lutaml/store/model_registry.rb +4 -1
- data/lib/lutaml/store/package_definition.rb +62 -0
- data/lib/lutaml/store/package_store.rb +149 -0
- data/lib/lutaml/store/package_transport.rb +450 -0
- data/lib/lutaml/store/version.rb +1 -1
- data/lib/lutaml/store.rb +3 -0
- data/lutaml-store.gemspec +1 -0
- data/spec/lutaml/store/cache_store_spec.rb +2 -2
- data/spec/lutaml/store/file_io_spec.rb +6 -5
- data/spec/lutaml/store/format/yamls_spec.rb +80 -0
- data/spec/lutaml/store/format_round_trip_spec.rb +2 -2
- data/spec/lutaml/store/package_definition_spec.rb +89 -0
- data/spec/lutaml/store/package_store_spec.rb +153 -0
- data/spec/lutaml/store/package_transport/directory_transport_spec.rb +139 -0
- data/spec/lutaml/store/package_transport/zip_transport_spec.rb +85 -0
- data/spec/support/simple_test_model.rb +15 -0
- data/spec/support/yamls_test_model.rb +35 -0
- metadata +25 -1
|
@@ -100,10 +100,16 @@ module Lutaml
|
|
|
100
100
|
|
|
101
101
|
parts.each_with_index do |part, index|
|
|
102
102
|
if part.match?(/\A\d+\z/)
|
|
103
|
-
|
|
103
|
+
unless current.is_a?(Array)
|
|
104
|
+
raise InvalidKeyError,
|
|
105
|
+
"Expected array at #{parts[0..index - 1].join(".")}, got #{current.class}"
|
|
106
|
+
end
|
|
104
107
|
|
|
105
108
|
index_val = part.to_i
|
|
106
|
-
|
|
109
|
+
if index_val >= current.length
|
|
110
|
+
raise InvalidKeyError,
|
|
111
|
+
"Array index #{index_val} out of bounds for #{parts[0..index - 1].join(".")}"
|
|
112
|
+
end
|
|
107
113
|
|
|
108
114
|
current = current[index_val]
|
|
109
115
|
else
|
|
@@ -130,7 +136,10 @@ module Lutaml
|
|
|
130
136
|
end
|
|
131
137
|
|
|
132
138
|
current_model = model.public_send(attr_name)
|
|
133
|
-
|
|
139
|
+
if current_model && @registry.registered?(current_model.class)
|
|
140
|
+
validate_polymorphic_key_compatibility!(current_model,
|
|
141
|
+
new_polymorphic_model)
|
|
142
|
+
end
|
|
134
143
|
|
|
135
144
|
model.public_send("#{attr_name}=", new_polymorphic_model)
|
|
136
145
|
end
|
|
@@ -106,7 +106,7 @@ module Lutaml
|
|
|
106
106
|
registration = @registry.registration_for(model)
|
|
107
107
|
|
|
108
108
|
models = []
|
|
109
|
-
@store.
|
|
109
|
+
@store.each_key do |storage_key|
|
|
110
110
|
parsed = StorageKey.parse(storage_key.to_s)
|
|
111
111
|
next unless parsed.class_name == model.name
|
|
112
112
|
|
|
@@ -236,7 +236,10 @@ module Lutaml
|
|
|
236
236
|
|
|
237
237
|
# Extract vary header values from request
|
|
238
238
|
request_vary_headers = {}
|
|
239
|
-
|
|
239
|
+
if vary_headers.any?
|
|
240
|
+
request_vary_headers = HttpHeaderProcessor.extract_vary_headers(request_headers,
|
|
241
|
+
vary_headers)
|
|
242
|
+
end
|
|
240
243
|
|
|
241
244
|
HttpCacheEntry.new(
|
|
242
245
|
cache_key: cache_key,
|
|
@@ -89,7 +89,10 @@ module Lutaml
|
|
|
89
89
|
attr_value = model.public_send(attr_name)
|
|
90
90
|
next if attr_value.nil?
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
if attr_value.is_a?(Object) && registered?(attr_value.class)
|
|
93
|
+
add_composite_entry(composite_models, attr_name,
|
|
94
|
+
attr_value)
|
|
95
|
+
end
|
|
93
96
|
|
|
94
97
|
next unless attr_value.is_a?(Array)
|
|
95
98
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Store
|
|
5
|
+
class PackageDefinition
|
|
6
|
+
attr_reader :name, :model_entries, :asset_entries,
|
|
7
|
+
:metadata_model, :metadata_file, :metadata_key
|
|
8
|
+
|
|
9
|
+
def initialize(name:, metadata_model: nil, metadata_file: nil,
|
|
10
|
+
metadata_key: :shortname)
|
|
11
|
+
@name = name
|
|
12
|
+
@metadata_model = metadata_model
|
|
13
|
+
@metadata_file = metadata_file
|
|
14
|
+
@metadata_key = metadata_key
|
|
15
|
+
@model_entries = []
|
|
16
|
+
@asset_entries = []
|
|
17
|
+
yield self if block_given?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def model(model:, key:, dir: nil, file: nil, layout: :separate,
|
|
21
|
+
default_format: :yaml, serializer: nil)
|
|
22
|
+
raise ArgumentError, "Specify dir: or file:, not both" if dir && file
|
|
23
|
+
|
|
24
|
+
@model_entries << ModelEntry.new(
|
|
25
|
+
model: model, dir: dir, file: file, layout: layout,
|
|
26
|
+
key: key, default_format: default_format, serializer: serializer
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def asset(path, type: :file)
|
|
31
|
+
@asset_entries << AssetEntry.new(path: path, type: type)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def entry_for(model_class)
|
|
35
|
+
@model_entries.find { |e| e.model == model_class }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def model_classes
|
|
39
|
+
@model_entries.map(&:model)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def database_store_models
|
|
43
|
+
@model_entries.map do |e|
|
|
44
|
+
config = { model: e.model, key: e.key }
|
|
45
|
+
config[:serializer] = e.serializer if e.serializer
|
|
46
|
+
config[:dir] = e.dir if e.dir
|
|
47
|
+
config
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
ModelEntry = Struct.new(
|
|
52
|
+
:model, :dir, :file, :layout, :key, :default_format, :serializer,
|
|
53
|
+
keyword_init: true
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
AssetEntry = Struct.new(
|
|
57
|
+
:path, :type,
|
|
58
|
+
keyword_init: true
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Store
|
|
5
|
+
class PackageStore
|
|
6
|
+
attr_accessor :metadata
|
|
7
|
+
attr_reader :definition, :assets
|
|
8
|
+
|
|
9
|
+
def initialize(definition)
|
|
10
|
+
@definition = definition
|
|
11
|
+
@db = DatabaseStore.new(
|
|
12
|
+
adapter: :memory,
|
|
13
|
+
models: definition.database_store_models
|
|
14
|
+
)
|
|
15
|
+
@assets = {}
|
|
16
|
+
@metadata = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# ── Load / Save ──
|
|
20
|
+
|
|
21
|
+
def self.load(definition, path, transport: :directory, format: nil)
|
|
22
|
+
store = new(definition)
|
|
23
|
+
transporter = resolve_transport(transport)
|
|
24
|
+
transporter.read(path, store, format: format)
|
|
25
|
+
store
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def save(path, transport: :directory, format: nil, formats: {})
|
|
29
|
+
resolved_formats = resolve_formats(format, formats)
|
|
30
|
+
transporter = self.class.resolve_transport(transport)
|
|
31
|
+
transporter.write(path, self, formats: resolved_formats)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# ── Model CRUD ──
|
|
35
|
+
|
|
36
|
+
def add_model(model_instance)
|
|
37
|
+
@db.save(model_instance)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def add_models(model_instances)
|
|
41
|
+
model_instances.each { |m| add_model(m) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def fetch_model(model_class, key)
|
|
45
|
+
entry = definition.entry_for(model_class)
|
|
46
|
+
@db.fetch(model: model_class, **{ entry.key => key })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def models_for(model_class)
|
|
50
|
+
@db.all(model: model_class)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def model_count(model_class)
|
|
54
|
+
@db.count(model: model_class)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def model_exists?(model_class, key)
|
|
58
|
+
entry = definition.entry_for(model_class)
|
|
59
|
+
@db.exists?(model: model_class, **{ entry.key => key })
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def remove_model(model_class, key)
|
|
63
|
+
entry = definition.entry_for(model_class)
|
|
64
|
+
@db.destroy(model: model_class, **{ entry.key => key })
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ── Metadata ──
|
|
68
|
+
|
|
69
|
+
# ── Assets ──
|
|
70
|
+
|
|
71
|
+
def asset(path)
|
|
72
|
+
@assets[path]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def add_asset(path, content)
|
|
76
|
+
@assets[path] = content
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def asset_paths
|
|
80
|
+
@assets.keys
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def remove_asset(path)
|
|
84
|
+
@assets.delete(path)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# ── Bulk ──
|
|
88
|
+
|
|
89
|
+
def clear_models(model_class)
|
|
90
|
+
@db.all(model: model_class).each do |m|
|
|
91
|
+
entry = definition.entry_for(model_class)
|
|
92
|
+
key = m.public_send(entry.key)
|
|
93
|
+
@db.destroy(model: model_class, **{ entry.key => key })
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def clear_assets
|
|
98
|
+
@assets.clear
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def clear_all
|
|
102
|
+
definition.model_classes.each { |mc| clear_models(mc) }
|
|
103
|
+
clear_assets
|
|
104
|
+
@metadata = nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# ── Stats ──
|
|
108
|
+
|
|
109
|
+
def stats
|
|
110
|
+
model_stats = definition.model_classes
|
|
111
|
+
.map { |mc| [mc.name, model_count(mc)] }
|
|
112
|
+
.to_h
|
|
113
|
+
{
|
|
114
|
+
package: definition.name,
|
|
115
|
+
models: model_stats,
|
|
116
|
+
assets: @assets.size,
|
|
117
|
+
metadata: @metadata ? true : false
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Public access for PackageTransport (avoids instance_variable_get).
|
|
122
|
+
attr_reader :db
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
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
|
+
def resolve_formats(global_format, per_model_formats)
|
|
138
|
+
if global_format
|
|
139
|
+
definition.model_entries
|
|
140
|
+
.map { |e| [e.model, global_format] }
|
|
141
|
+
.to_h
|
|
142
|
+
.merge(per_model_formats)
|
|
143
|
+
else
|
|
144
|
+
per_model_formats
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|