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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +13 -177
  3. data/CLAUDE.md +22 -9
  4. data/README.adoc +326 -1103
  5. data/lib/lutaml/store/adapter/base.rb +4 -0
  6. data/lib/lutaml/store/adapter/memory.rb +8 -0
  7. data/lib/lutaml/store/adapter.rb +17 -0
  8. data/lib/lutaml/store/basic_store.rb +1 -10
  9. data/lib/lutaml/store/cache_store.rb +11 -16
  10. data/lib/lutaml/store/database_store.rb +19 -9
  11. data/lib/lutaml/store/format/base.rb +4 -0
  12. data/lib/lutaml/store/format/marshal_format.rb +4 -0
  13. data/lib/lutaml/store/format/xml.rb +37 -0
  14. data/lib/lutaml/store/format/yamls.rb +1 -1
  15. data/lib/lutaml/store/format.rb +22 -1
  16. data/lib/lutaml/store/format_serializer.rb +44 -0
  17. data/lib/lutaml/store/http_cache.rb +3 -13
  18. data/lib/lutaml/store/model_registration.rb +5 -2
  19. data/lib/lutaml/store/model_registry.rb +22 -20
  20. data/lib/lutaml/store/package_store.rb +2 -18
  21. data/lib/lutaml/store/package_transport/base.rb +60 -0
  22. data/lib/lutaml/store/package_transport/directory_transport.rb +196 -0
  23. data/lib/lutaml/store/package_transport/zip_transport.rb +183 -0
  24. data/lib/lutaml/store/package_transport.rb +11 -438
  25. data/lib/lutaml/store/storage_key.rb +1 -1
  26. data/lib/lutaml/store/version.rb +1 -1
  27. data/lib/lutaml/store.rb +1 -0
  28. metadata +14 -77
  29. data/.github/workflows/main.yml +0 -27
  30. data/.gitignore +0 -12
  31. data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +0 -209
  32. data/CORRECTED_HTTP_CACHE_PLAN.md +0 -164
  33. data/Gemfile +0 -15
  34. data/Gemfile.lock +0 -227
  35. data/TODO.impl/0-lutaml-store-self-quality.md +0 -112
  36. data/TODO.impl/1-lutaml-hal-migration.md +0 -96
  37. data/TODO.impl/2-glossarist-migration.md +0 -359
  38. data/TODO.impl/3-lutaml-jsonschema-migration.md +0 -273
  39. data/bin/console +0 -11
  40. data/bin/setup +0 -8
  41. data/demo/Gemfile +0 -15
  42. data/demo/Gemfile.lock +0 -61
  43. data/demo/README.adoc +0 -301
  44. data/demo/data/vcards/co/contact_10_thompson.data +0 -1
  45. data/demo/data/vcards/co/contact_10_thompson.meta +0 -1
  46. data/demo/data/vcards/co/contact_1_doe.data +0 -1
  47. data/demo/data/vcards/co/contact_1_doe.meta +0 -1
  48. data/demo/data/vcards/co/contact_2_smith.data +0 -1
  49. data/demo/data/vcards/co/contact_2_smith.meta +0 -1
  50. data/demo/data/vcards/co/contact_3_johnson.data +0 -1
  51. data/demo/data/vcards/co/contact_3_johnson.meta +0 -1
  52. data/demo/data/vcards/co/contact_4_garcia.data +0 -1
  53. data/demo/data/vcards/co/contact_4_garcia.meta +0 -1
  54. data/demo/data/vcards/co/contact_5_wilson.data +0 -1
  55. data/demo/data/vcards/co/contact_5_wilson.meta +0 -1
  56. data/demo/data/vcards/co/contact_6_brown.data +0 -1
  57. data/demo/data/vcards/co/contact_6_brown.meta +0 -1
  58. data/demo/data/vcards/co/contact_7_davis.data +0 -1
  59. data/demo/data/vcards/co/contact_7_davis.meta +0 -1
  60. data/demo/data/vcards/co/contact_8_anderson.data +0 -1
  61. data/demo/data/vcards/co/contact_8_anderson.meta +0 -1
  62. data/demo/data/vcards/co/contact_9_taylor.data +0 -1
  63. data/demo/data/vcards/co/contact_9_taylor.meta +0 -1
  64. data/demo/data/vcards.db +0 -0
  65. data/demo/pottery_class_demo.rb +0 -164
  66. data/demo/vcard_models.rb +0 -140
  67. data/demo/vcard_store_demo.rb +0 -526
  68. data/lutaml-store.gemspec +0 -36
  69. data/plan.adoc +0 -606
  70. data/spec/lutaml/store/adapter_interface_spec.rb +0 -89
  71. data/spec/lutaml/store/anti_pattern_guard_spec.rb +0 -35
  72. data/spec/lutaml/store/anti_pattern_spec.rb +0 -78
  73. data/spec/lutaml/store/autoload_spec.rb +0 -34
  74. data/spec/lutaml/store/cache_store_spec.rb +0 -271
  75. data/spec/lutaml/store/compression_spec.rb +0 -78
  76. data/spec/lutaml/store/config_enhanced_spec.rb +0 -158
  77. data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +0 -336
  78. data/spec/lutaml/store/custom_serializer_spec.rb +0 -108
  79. data/spec/lutaml/store/database_store_spec.rb +0 -279
  80. data/spec/lutaml/store/file_io_spec.rb +0 -220
  81. data/spec/lutaml/store/format/yamls_spec.rb +0 -80
  82. data/spec/lutaml/store/format_round_trip_spec.rb +0 -110
  83. data/spec/lutaml/store/format_spec.rb +0 -70
  84. data/spec/lutaml/store/http_cache_entry_spec.rb +0 -203
  85. data/spec/lutaml/store/http_cache_hal_integration_spec.rb +0 -404
  86. data/spec/lutaml/store/http_cache_spec.rb +0 -422
  87. data/spec/lutaml/store/http_header_processor_spec.rb +0 -290
  88. data/spec/lutaml/store/import_spec.rb +0 -90
  89. data/spec/lutaml/store/integrity_spec.rb +0 -157
  90. data/spec/lutaml/store/key_collision_serializer_spec.rb +0 -98
  91. data/spec/lutaml/store/load_save_spec.rb +0 -107
  92. data/spec/lutaml/store/lutaml_model_integration_spec.rb +0 -291
  93. data/spec/lutaml/store/model_serializer_spec.rb +0 -140
  94. data/spec/lutaml/store/package_definition_spec.rb +0 -89
  95. data/spec/lutaml/store/package_store_spec.rb +0 -153
  96. data/spec/lutaml/store/package_transport/directory_transport_spec.rb +0 -139
  97. data/spec/lutaml/store/package_transport/zip_transport_spec.rb +0 -85
  98. data/spec/lutaml/store/store_spec.rb +0 -182
  99. data/spec/lutaml/store_spec.rb +0 -21
  100. data/spec/spec_helper.rb +0 -16
  101. data/spec/support/simple_test_model.rb +0 -15
  102. data/spec/support/yamls_test_model.rb +0 -35
@@ -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 = read_file(file_path, fmt)
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
+ write_file(file_path, content, fmt)
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 = read_file(file_path, fmt)
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 = read_file(file_path, fmt)
95
+ next if !fmt.binary? && 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
+ write_file(file_path, content, fmt)
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
+ write_file(file_path, fmt.serialize_many(group), fmt)
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
+ write_file(file_path, fmt.serialize(model), fmt)
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,183 @@
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 !fmt.binary? && raw.strip.empty?
97
+
98
+ begin
99
+ case entry.layout
100
+ when :grouped
101
+ fmt.deserialize_many(raw, entry.model).each do |m|
102
+ set_key_from_zip_path(m, zip_entry.name, entry, prefix)
103
+ package_store.add_model(m)
104
+ end
105
+ else
106
+ model = fmt.deserialize(raw, entry.model)
107
+ set_key_from_zip_path(model, zip_entry.name, entry, prefix)
108
+ package_store.add_model(model)
109
+ end
110
+ rescue StandardError => e
111
+ warn "PackageStore: failed to load #{zip_entry.name}: #{e.message}"
112
+ end
113
+ end
114
+ end
115
+
116
+ def write_model_entry_zip(zip_file, entry, package_store, fmt)
117
+ models = package_store.models_for(entry.model)
118
+ return if models.empty?
119
+
120
+ if entry.file
121
+ content = entry.layout == :grouped ? fmt.serialize_many(models) : fmt.serialize(models.first)
122
+ zip_file.get_output_stream(entry.file) { |f| f.write(content) }
123
+ else
124
+ write_directory_models_zip(zip_file, entry, models, fmt)
125
+ end
126
+ end
127
+
128
+ def write_directory_models_zip(zip_file, entry, models, fmt)
129
+ prefix = entry.dir ? "#{entry.dir}/" : ""
130
+
131
+ case entry.layout
132
+ when :grouped
133
+ models.group_by { |m| extract_key(m, entry) }.each do |key, group|
134
+ filename = "#{prefix}#{sanitize_filename(key)}#{fmt.extension}"
135
+ zip_file.get_output_stream(filename) { |f| f.write(fmt.serialize_many(group)) }
136
+ end
137
+ else
138
+ models.each do |model|
139
+ key = extract_key(model, entry)
140
+ filename = "#{prefix}#{sanitize_filename(key)}#{fmt.extension}"
141
+ zip_file.get_output_stream(filename) { |f| f.write(fmt.serialize(model)) }
142
+ end
143
+ end
144
+ end
145
+
146
+ def read_asset_entry_zip(zip_file, entry, package_store)
147
+ case entry.type
148
+ when :file
149
+ zip_entry = zip_file.find_entry(entry.path)
150
+ package_store.add_asset(entry.path, zip_entry.get_input_stream.read) if zip_entry
151
+ when :directory
152
+ prefix = entry.path.end_with?("/") ? entry.path : "#{entry.path}/"
153
+ zip_file.entries.each do |zip_entry|
154
+ next unless zip_entry.name.start_with?(prefix)
155
+ next if zip_entry.name == prefix || zip_entry.name.end_with?("/")
156
+
157
+ package_store.add_asset(zip_entry.name, zip_entry.get_input_stream.read)
158
+ end
159
+ end
160
+ end
161
+
162
+ def write_assets_zip(zip_file, package_store)
163
+ package_store.asset_paths.each do |asset_path|
164
+ content = package_store.asset(asset_path)
165
+ zip_file.get_output_stream(asset_path) { |f| f.write(content) } if content
166
+ end
167
+ end
168
+
169
+ def set_key_from_zip_path(model, zip_path, entry, prefix)
170
+ return if model.public_send(entry.key)
171
+
172
+ filename = zip_path.sub(prefix, "")
173
+ set_key_from_filename(model, filename, entry)
174
+ end
175
+
176
+ def matches_format?(name, fmt)
177
+ ext = File.extname(name)
178
+ ext == fmt.extension || (fmt.extension == ".yaml" && ext == ".yml")
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end