lutaml-store 0.1.1 → 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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +11 -371
  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/attribute_updater.rb +12 -3
  7. data/lib/lutaml/store/basic_store.rb +4 -0
  8. data/lib/lutaml/store/cache_store.rb +10 -7
  9. data/lib/lutaml/store/database_store.rb +1 -1
  10. data/lib/lutaml/store/format/yamls.rb +2 -2
  11. data/lib/lutaml/store/format.rb +19 -0
  12. data/lib/lutaml/store/http_cache.rb +7 -14
  13. data/lib/lutaml/store/model_registration.rb +5 -2
  14. data/lib/lutaml/store/model_registry.rb +22 -17
  15. data/lib/lutaml/store/package_definition.rb +62 -0
  16. data/lib/lutaml/store/package_store.rb +133 -0
  17. data/lib/lutaml/store/package_transport/base.rb +48 -0
  18. data/lib/lutaml/store/package_transport/directory_transport.rb +196 -0
  19. data/lib/lutaml/store/package_transport/zip_transport.rb +178 -0
  20. data/lib/lutaml/store/package_transport.rb +23 -0
  21. data/lib/lutaml/store/version.rb +1 -1
  22. data/lib/lutaml/store.rb +3 -0
  23. metadata +29 -70
  24. data/.github/workflows/main.yml +0 -27
  25. data/.gitignore +0 -12
  26. data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +0 -209
  27. data/CORRECTED_HTTP_CACHE_PLAN.md +0 -164
  28. data/Gemfile +0 -15
  29. data/Gemfile.lock +0 -220
  30. data/TODO.impl/0-lutaml-store-self-quality.md +0 -112
  31. data/TODO.impl/1-lutaml-hal-migration.md +0 -60
  32. data/TODO.impl/2-glossarist-migration.md +0 -359
  33. data/TODO.impl/3-lutaml-jsonschema-migration.md +0 -273
  34. data/bin/console +0 -11
  35. data/bin/setup +0 -8
  36. data/demo/Gemfile +0 -15
  37. data/demo/Gemfile.lock +0 -61
  38. data/demo/README.adoc +0 -301
  39. data/demo/data/vcards/co/contact_10_thompson.data +0 -1
  40. data/demo/data/vcards/co/contact_10_thompson.meta +0 -1
  41. data/demo/data/vcards/co/contact_1_doe.data +0 -1
  42. data/demo/data/vcards/co/contact_1_doe.meta +0 -1
  43. data/demo/data/vcards/co/contact_2_smith.data +0 -1
  44. data/demo/data/vcards/co/contact_2_smith.meta +0 -1
  45. data/demo/data/vcards/co/contact_3_johnson.data +0 -1
  46. data/demo/data/vcards/co/contact_3_johnson.meta +0 -1
  47. data/demo/data/vcards/co/contact_4_garcia.data +0 -1
  48. data/demo/data/vcards/co/contact_4_garcia.meta +0 -1
  49. data/demo/data/vcards/co/contact_5_wilson.data +0 -1
  50. data/demo/data/vcards/co/contact_5_wilson.meta +0 -1
  51. data/demo/data/vcards/co/contact_6_brown.data +0 -1
  52. data/demo/data/vcards/co/contact_6_brown.meta +0 -1
  53. data/demo/data/vcards/co/contact_7_davis.data +0 -1
  54. data/demo/data/vcards/co/contact_7_davis.meta +0 -1
  55. data/demo/data/vcards/co/contact_8_anderson.data +0 -1
  56. data/demo/data/vcards/co/contact_8_anderson.meta +0 -1
  57. data/demo/data/vcards/co/contact_9_taylor.data +0 -1
  58. data/demo/data/vcards/co/contact_9_taylor.meta +0 -1
  59. data/demo/data/vcards.db +0 -0
  60. data/demo/pottery_class_demo.rb +0 -164
  61. data/demo/vcard_models.rb +0 -140
  62. data/demo/vcard_store_demo.rb +0 -526
  63. data/lutaml-store.gemspec +0 -35
  64. data/plan.adoc +0 -606
  65. data/spec/lutaml/store/adapter_interface_spec.rb +0 -89
  66. data/spec/lutaml/store/anti_pattern_guard_spec.rb +0 -35
  67. data/spec/lutaml/store/anti_pattern_spec.rb +0 -78
  68. data/spec/lutaml/store/autoload_spec.rb +0 -34
  69. data/spec/lutaml/store/cache_store_spec.rb +0 -271
  70. data/spec/lutaml/store/compression_spec.rb +0 -78
  71. data/spec/lutaml/store/config_enhanced_spec.rb +0 -158
  72. data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +0 -336
  73. data/spec/lutaml/store/custom_serializer_spec.rb +0 -108
  74. data/spec/lutaml/store/database_store_spec.rb +0 -279
  75. data/spec/lutaml/store/file_io_spec.rb +0 -219
  76. data/spec/lutaml/store/format_round_trip_spec.rb +0 -110
  77. data/spec/lutaml/store/format_spec.rb +0 -70
  78. data/spec/lutaml/store/http_cache_entry_spec.rb +0 -203
  79. data/spec/lutaml/store/http_cache_hal_integration_spec.rb +0 -404
  80. data/spec/lutaml/store/http_cache_spec.rb +0 -422
  81. data/spec/lutaml/store/http_header_processor_spec.rb +0 -290
  82. data/spec/lutaml/store/import_spec.rb +0 -90
  83. data/spec/lutaml/store/integrity_spec.rb +0 -157
  84. data/spec/lutaml/store/key_collision_serializer_spec.rb +0 -98
  85. data/spec/lutaml/store/load_save_spec.rb +0 -107
  86. data/spec/lutaml/store/lutaml_model_integration_spec.rb +0 -291
  87. data/spec/lutaml/store/model_serializer_spec.rb +0 -140
  88. data/spec/lutaml/store/store_spec.rb +0 -182
  89. data/spec/lutaml/store_spec.rb +0 -21
  90. data/spec/spec_helper.rb +0 -16
@@ -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
@@ -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
- raise InvalidKeyError, "Expected array at #{parts[0..index - 1].join(".")}, got #{current.class}" unless current.is_a?(Array)
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
- raise InvalidKeyError, "Array index #{index_val} out of bounds for #{parts[0..index - 1].join(".")}" if index_val >= current.length
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
- validate_polymorphic_key_compatibility!(current_model, new_polymorphic_model) if current_model && @registry.registered?(current_model.class)
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
@@ -98,6 +98,10 @@ module Lutaml
98
98
  @adapter.keys
99
99
  end
100
100
 
101
+ def each_key(&block)
102
+ @adapter.keys.each(&block)
103
+ end
104
+
101
105
  def bulk_get(keys)
102
106
  @adapter.bulk_get(keys)
103
107
  end
@@ -170,7 +170,7 @@ module Lutaml
170
170
  def cleanup_expired
171
171
  expired_keys = []
172
172
 
173
- @adapter.keys.each do |key|
173
+ @adapter.each_key do |key|
174
174
  entry_data = @adapter.get(key)
175
175
  next unless entry_data
176
176
 
@@ -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
@@ -106,7 +106,7 @@ module Lutaml
106
106
  registration = @registry.registration_for(model)
107
107
 
108
108
  models = []
109
- @store.keys.each do |storage_key|
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
 
@@ -13,11 +13,11 @@ module Lutaml
13
13
  end
14
14
 
15
15
  def serialize(model)
16
- model.to_yaml
16
+ model.to_yamls
17
17
  end
18
18
 
19
19
  def serialize_many(models)
20
- models.map(&:to_yaml).join
20
+ models.map(&:to_yamls).join
21
21
  end
22
22
 
23
23
  def deserialize(data, model_class)
@@ -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
 
@@ -236,7 +226,10 @@ module Lutaml
236
226
 
237
227
  # Extract vary header values from request
238
228
  request_vary_headers = {}
239
- request_vary_headers = HttpHeaderProcessor.extract_vary_headers(request_headers, vary_headers) if vary_headers.any?
229
+ if vary_headers.any?
230
+ request_vary_headers = HttpHeaderProcessor.extract_vary_headers(request_headers,
231
+ vary_headers)
232
+ end
240
233
 
241
234
  HttpCacheEntry.new(
242
235
  cache_key: cache_key,
@@ -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,31 +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
- add_composite_entry(composite_models, attr_name, attr_value) if attr_value.is_a?(Object) && registered?(attr_value.class)
89
+ registration.composites.each_with_object({}) do |attr_name, result|
90
+ collect_composite(model, attr_name, result)
91
+ end
92
+ end
93
93
 
94
- next unless attr_value.is_a?(Array)
94
+ private
95
95
 
96
- attr_value.each_with_index do |item, index|
97
- 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?
98
99
 
99
- add_composite_entry(composite_models, "#{attr_name}.#{index}", item)
100
- 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)
101
104
  end
102
-
103
- composite_models
104
- rescue NoMethodError
105
- {}
106
105
  end
107
106
 
108
- 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
109
114
 
110
115
  def add_composite_entry(composite_models, attr_path, model_instance)
111
116
  registration = find_registration(model_instance.class)
@@ -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,133 @@
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
+ PackageTransport.resolve(transport).read(path, store, format: format)
24
+ store
25
+ end
26
+
27
+ def save(path, transport: :directory, format: nil, formats: {})
28
+ resolved_formats = resolve_formats(format, formats)
29
+ PackageTransport.resolve(transport).write(path, self, formats: resolved_formats)
30
+ end
31
+
32
+ # ── Model CRUD ──
33
+
34
+ def add_model(model_instance)
35
+ @db.save(model_instance)
36
+ end
37
+
38
+ def add_models(model_instances)
39
+ model_instances.each { |m| add_model(m) }
40
+ end
41
+
42
+ def fetch_model(model_class, key)
43
+ entry = definition.entry_for(model_class)
44
+ @db.fetch(model: model_class, **{ entry.key => key })
45
+ end
46
+
47
+ def models_for(model_class)
48
+ @db.all(model: model_class)
49
+ end
50
+
51
+ def model_count(model_class)
52
+ @db.count(model: model_class)
53
+ end
54
+
55
+ def model_exists?(model_class, key)
56
+ entry = definition.entry_for(model_class)
57
+ @db.exists?(model: model_class, **{ entry.key => key })
58
+ end
59
+
60
+ def remove_model(model_class, key)
61
+ entry = definition.entry_for(model_class)
62
+ @db.destroy(model: model_class, **{ entry.key => key })
63
+ end
64
+
65
+ # ── Metadata ──
66
+
67
+ # ── Assets ──
68
+
69
+ def asset(path)
70
+ @assets[path]
71
+ end
72
+
73
+ def add_asset(path, content)
74
+ @assets[path] = content
75
+ end
76
+
77
+ def asset_paths
78
+ @assets.keys
79
+ end
80
+
81
+ def remove_asset(path)
82
+ @assets.delete(path)
83
+ end
84
+
85
+ # ── Bulk ──
86
+
87
+ def clear_models(model_class)
88
+ @db.all(model: model_class).each do |m|
89
+ entry = definition.entry_for(model_class)
90
+ key = m.public_send(entry.key)
91
+ @db.destroy(model: model_class, **{ entry.key => key })
92
+ end
93
+ end
94
+
95
+ def clear_assets
96
+ @assets.clear
97
+ end
98
+
99
+ def clear_all
100
+ definition.model_classes.each { |mc| clear_models(mc) }
101
+ clear_assets
102
+ @metadata = nil
103
+ end
104
+
105
+ # ── Stats ──
106
+
107
+ def stats
108
+ model_stats = definition.model_classes
109
+ .map { |mc| [mc.name, model_count(mc)] }
110
+ .to_h
111
+ {
112
+ package: definition.name,
113
+ models: model_stats,
114
+ assets: @assets.size,
115
+ metadata: @metadata ? true : false
116
+ }
117
+ end
118
+
119
+ private
120
+
121
+ def resolve_formats(global_format, per_model_formats)
122
+ if global_format
123
+ definition.model_entries
124
+ .map { |e| [e.model, global_format] }
125
+ .to_h
126
+ .merge(per_model_formats)
127
+ else
128
+ per_model_formats
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -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