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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +11 -175
  3. data/README.adoc +233 -1124
  4. data/lib/lutaml/store/adapter/base.rb +4 -0
  5. data/lib/lutaml/store/adapter/memory.rb +8 -0
  6. data/lib/lutaml/store/cache_store.rb +9 -6
  7. data/lib/lutaml/store/format.rb +19 -0
  8. data/lib/lutaml/store/http_cache.rb +3 -13
  9. data/lib/lutaml/store/model_registration.rb +5 -2
  10. data/lib/lutaml/store/model_registry.rb +22 -20
  11. data/lib/lutaml/store/package_store.rb +2 -18
  12. data/lib/lutaml/store/package_transport/base.rb +48 -0
  13. data/lib/lutaml/store/package_transport/directory_transport.rb +196 -0
  14. data/lib/lutaml/store/package_transport/zip_transport.rb +178 -0
  15. data/lib/lutaml/store/package_transport.rb +11 -438
  16. data/lib/lutaml/store/version.rb +1 -1
  17. metadata +12 -77
  18. data/.github/workflows/main.yml +0 -27
  19. data/.gitignore +0 -12
  20. data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +0 -209
  21. data/CORRECTED_HTTP_CACHE_PLAN.md +0 -164
  22. data/Gemfile +0 -15
  23. data/Gemfile.lock +0 -227
  24. data/TODO.impl/0-lutaml-store-self-quality.md +0 -112
  25. data/TODO.impl/1-lutaml-hal-migration.md +0 -96
  26. data/TODO.impl/2-glossarist-migration.md +0 -359
  27. data/TODO.impl/3-lutaml-jsonschema-migration.md +0 -273
  28. data/bin/console +0 -11
  29. data/bin/setup +0 -8
  30. data/demo/Gemfile +0 -15
  31. data/demo/Gemfile.lock +0 -61
  32. data/demo/README.adoc +0 -301
  33. data/demo/data/vcards/co/contact_10_thompson.data +0 -1
  34. data/demo/data/vcards/co/contact_10_thompson.meta +0 -1
  35. data/demo/data/vcards/co/contact_1_doe.data +0 -1
  36. data/demo/data/vcards/co/contact_1_doe.meta +0 -1
  37. data/demo/data/vcards/co/contact_2_smith.data +0 -1
  38. data/demo/data/vcards/co/contact_2_smith.meta +0 -1
  39. data/demo/data/vcards/co/contact_3_johnson.data +0 -1
  40. data/demo/data/vcards/co/contact_3_johnson.meta +0 -1
  41. data/demo/data/vcards/co/contact_4_garcia.data +0 -1
  42. data/demo/data/vcards/co/contact_4_garcia.meta +0 -1
  43. data/demo/data/vcards/co/contact_5_wilson.data +0 -1
  44. data/demo/data/vcards/co/contact_5_wilson.meta +0 -1
  45. data/demo/data/vcards/co/contact_6_brown.data +0 -1
  46. data/demo/data/vcards/co/contact_6_brown.meta +0 -1
  47. data/demo/data/vcards/co/contact_7_davis.data +0 -1
  48. data/demo/data/vcards/co/contact_7_davis.meta +0 -1
  49. data/demo/data/vcards/co/contact_8_anderson.data +0 -1
  50. data/demo/data/vcards/co/contact_8_anderson.meta +0 -1
  51. data/demo/data/vcards/co/contact_9_taylor.data +0 -1
  52. data/demo/data/vcards/co/contact_9_taylor.meta +0 -1
  53. data/demo/data/vcards.db +0 -0
  54. data/demo/pottery_class_demo.rb +0 -164
  55. data/demo/vcard_models.rb +0 -140
  56. data/demo/vcard_store_demo.rb +0 -526
  57. data/lutaml-store.gemspec +0 -36
  58. data/plan.adoc +0 -606
  59. data/spec/lutaml/store/adapter_interface_spec.rb +0 -89
  60. data/spec/lutaml/store/anti_pattern_guard_spec.rb +0 -35
  61. data/spec/lutaml/store/anti_pattern_spec.rb +0 -78
  62. data/spec/lutaml/store/autoload_spec.rb +0 -34
  63. data/spec/lutaml/store/cache_store_spec.rb +0 -271
  64. data/spec/lutaml/store/compression_spec.rb +0 -78
  65. data/spec/lutaml/store/config_enhanced_spec.rb +0 -158
  66. data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +0 -336
  67. data/spec/lutaml/store/custom_serializer_spec.rb +0 -108
  68. data/spec/lutaml/store/database_store_spec.rb +0 -279
  69. data/spec/lutaml/store/file_io_spec.rb +0 -220
  70. data/spec/lutaml/store/format/yamls_spec.rb +0 -80
  71. data/spec/lutaml/store/format_round_trip_spec.rb +0 -110
  72. data/spec/lutaml/store/format_spec.rb +0 -70
  73. data/spec/lutaml/store/http_cache_entry_spec.rb +0 -203
  74. data/spec/lutaml/store/http_cache_hal_integration_spec.rb +0 -404
  75. data/spec/lutaml/store/http_cache_spec.rb +0 -422
  76. data/spec/lutaml/store/http_header_processor_spec.rb +0 -290
  77. data/spec/lutaml/store/import_spec.rb +0 -90
  78. data/spec/lutaml/store/integrity_spec.rb +0 -157
  79. data/spec/lutaml/store/key_collision_serializer_spec.rb +0 -98
  80. data/spec/lutaml/store/load_save_spec.rb +0 -107
  81. data/spec/lutaml/store/lutaml_model_integration_spec.rb +0 -291
  82. data/spec/lutaml/store/model_serializer_spec.rb +0 -140
  83. data/spec/lutaml/store/package_definition_spec.rb +0 -89
  84. data/spec/lutaml/store/package_store_spec.rb +0 -153
  85. data/spec/lutaml/store/package_transport/directory_transport_spec.rb +0 -139
  86. data/spec/lutaml/store/package_transport/zip_transport_spec.rb +0 -85
  87. data/spec/lutaml/store/store_spec.rb +0 -182
  88. data/spec/lutaml/store_spec.rb +0 -21
  89. data/spec/spec_helper.rb +0 -16
  90. data/spec/support/simple_test_model.rb +0 -15
  91. data/spec/support/yamls_test_model.rb +0 -35
@@ -28,6 +28,10 @@ module Lutaml
28
28
  raise NotImplementedError
29
29
  end
30
30
 
31
+ def each_key(&block)
32
+ raise NotImplementedError
33
+ end
34
+
31
35
  def all
32
36
  raise NotImplementedError
33
37
  end
@@ -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: {}, &block)
225
+ def fetch(key, default = nil, ttl: :default, metadata: {})
226
226
  value = get(key)
227
227
  return value unless value.nil?
228
228
 
229
- return nil unless block_given?
230
-
231
- value = block.call
232
- set(key, value, ttl: ttl, metadata: metadata)
233
- value
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
@@ -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
- entry_data = data.is_a?(String) ? JSON.parse(data) : data
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
- # Handle both serialized and hash data
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
- # Serialize entry for storage
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, dir: 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 models that are registered and nested within other models
84
+ # Find declared composite models nested within the given model
85
85
  def find_composite_models(model)
86
- composite_models = {}
86
+ registration = find_registration(model.class)
87
+ return {} unless registration&.composites&.any?
87
88
 
88
- model.class.attributes.each_key do |attr_name|
89
- attr_value = model.public_send(attr_name)
90
- next if attr_value.nil?
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
- next unless attr_value.is_a?(Array)
94
+ private
98
95
 
99
- attr_value.each_with_index do |item, index|
100
- next unless item.is_a?(Object) && registered?(item.class)
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
- add_composite_entry(composite_models, "#{attr_name}.#{index}", item)
103
- end
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
- private
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
- transporter = resolve_transport(transport)
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
- transporter = self.class.resolve_transport(transport)
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