lutaml-store 0.1.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 (110) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +27 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +10 -0
  6. data/.rubocop_todo.yml +450 -0
  7. data/CLAUDE.md +57 -0
  8. data/CODE_OF_CONDUCT.md +132 -0
  9. data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +209 -0
  10. data/CORRECTED_HTTP_CACHE_PLAN.md +164 -0
  11. data/Gemfile +15 -0
  12. data/Gemfile.lock +220 -0
  13. data/README.adoc +1430 -0
  14. data/Rakefile +12 -0
  15. data/TODO.impl/0-lutaml-store-self-quality.md +112 -0
  16. data/TODO.impl/1-lutaml-hal-migration.md +60 -0
  17. data/TODO.impl/2-glossarist-migration.md +359 -0
  18. data/TODO.impl/3-lutaml-jsonschema-migration.md +273 -0
  19. data/bin/console +11 -0
  20. data/bin/setup +8 -0
  21. data/demo/Gemfile +15 -0
  22. data/demo/Gemfile.lock +61 -0
  23. data/demo/README.adoc +301 -0
  24. data/demo/data/vcards/co/contact_10_thompson.data +1 -0
  25. data/demo/data/vcards/co/contact_10_thompson.meta +1 -0
  26. data/demo/data/vcards/co/contact_1_doe.data +1 -0
  27. data/demo/data/vcards/co/contact_1_doe.meta +1 -0
  28. data/demo/data/vcards/co/contact_2_smith.data +1 -0
  29. data/demo/data/vcards/co/contact_2_smith.meta +1 -0
  30. data/demo/data/vcards/co/contact_3_johnson.data +1 -0
  31. data/demo/data/vcards/co/contact_3_johnson.meta +1 -0
  32. data/demo/data/vcards/co/contact_4_garcia.data +1 -0
  33. data/demo/data/vcards/co/contact_4_garcia.meta +1 -0
  34. data/demo/data/vcards/co/contact_5_wilson.data +1 -0
  35. data/demo/data/vcards/co/contact_5_wilson.meta +1 -0
  36. data/demo/data/vcards/co/contact_6_brown.data +1 -0
  37. data/demo/data/vcards/co/contact_6_brown.meta +1 -0
  38. data/demo/data/vcards/co/contact_7_davis.data +1 -0
  39. data/demo/data/vcards/co/contact_7_davis.meta +1 -0
  40. data/demo/data/vcards/co/contact_8_anderson.data +1 -0
  41. data/demo/data/vcards/co/contact_8_anderson.meta +1 -0
  42. data/demo/data/vcards/co/contact_9_taylor.data +1 -0
  43. data/demo/data/vcards/co/contact_9_taylor.meta +1 -0
  44. data/demo/data/vcards.db +0 -0
  45. data/demo/pottery_class_demo.rb +164 -0
  46. data/demo/vcard_models.rb +140 -0
  47. data/demo/vcard_store_demo.rb +526 -0
  48. data/lib/lutaml/store/adapter/base.rb +65 -0
  49. data/lib/lutaml/store/adapter/filesystem.rb +288 -0
  50. data/lib/lutaml/store/adapter/memory.rb +225 -0
  51. data/lib/lutaml/store/adapter/sqlite.rb +193 -0
  52. data/lib/lutaml/store/adapter.rb +12 -0
  53. data/lib/lutaml/store/attribute_updater.rb +198 -0
  54. data/lib/lutaml/store/basic_store.rb +190 -0
  55. data/lib/lutaml/store/cache.rb +108 -0
  56. data/lib/lutaml/store/cache_store.rb +282 -0
  57. data/lib/lutaml/store/composite_model_handler.rb +169 -0
  58. data/lib/lutaml/store/compression.rb +137 -0
  59. data/lib/lutaml/store/config.rb +178 -0
  60. data/lib/lutaml/store/database_store.rb +425 -0
  61. data/lib/lutaml/store/events.rb +92 -0
  62. data/lib/lutaml/store/format/base.rb +33 -0
  63. data/lib/lutaml/store/format/json.rb +25 -0
  64. data/lib/lutaml/store/format/jsonl.rb +37 -0
  65. data/lib/lutaml/store/format/marshal_format.rb +37 -0
  66. data/lib/lutaml/store/format/yaml.rb +29 -0
  67. data/lib/lutaml/store/format/yamls.rb +35 -0
  68. data/lib/lutaml/store/format.rb +33 -0
  69. data/lib/lutaml/store/http_cache.rb +279 -0
  70. data/lib/lutaml/store/http_cache_config.rb +53 -0
  71. data/lib/lutaml/store/http_cache_entry.rb +69 -0
  72. data/lib/lutaml/store/http_header_processor.rb +175 -0
  73. data/lib/lutaml/store/integrity.rb +102 -0
  74. data/lib/lutaml/store/model_registration.rb +75 -0
  75. data/lib/lutaml/store/model_registry.rb +123 -0
  76. data/lib/lutaml/store/model_serializer.rb +69 -0
  77. data/lib/lutaml/store/monitor.rb +192 -0
  78. data/lib/lutaml/store/storage_key.rb +40 -0
  79. data/lib/lutaml/store/version.rb +7 -0
  80. data/lib/lutaml/store.rb +41 -0
  81. data/lutaml-store.gemspec +35 -0
  82. data/plan.adoc +606 -0
  83. data/sig/lutaml/store.rbs +6 -0
  84. data/spec/lutaml/store/adapter_interface_spec.rb +89 -0
  85. data/spec/lutaml/store/anti_pattern_guard_spec.rb +35 -0
  86. data/spec/lutaml/store/anti_pattern_spec.rb +78 -0
  87. data/spec/lutaml/store/autoload_spec.rb +34 -0
  88. data/spec/lutaml/store/cache_store_spec.rb +271 -0
  89. data/spec/lutaml/store/compression_spec.rb +78 -0
  90. data/spec/lutaml/store/config_enhanced_spec.rb +158 -0
  91. data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +336 -0
  92. data/spec/lutaml/store/custom_serializer_spec.rb +108 -0
  93. data/spec/lutaml/store/database_store_spec.rb +279 -0
  94. data/spec/lutaml/store/file_io_spec.rb +219 -0
  95. data/spec/lutaml/store/format_round_trip_spec.rb +110 -0
  96. data/spec/lutaml/store/format_spec.rb +70 -0
  97. data/spec/lutaml/store/http_cache_entry_spec.rb +203 -0
  98. data/spec/lutaml/store/http_cache_hal_integration_spec.rb +404 -0
  99. data/spec/lutaml/store/http_cache_spec.rb +422 -0
  100. data/spec/lutaml/store/http_header_processor_spec.rb +290 -0
  101. data/spec/lutaml/store/import_spec.rb +90 -0
  102. data/spec/lutaml/store/integrity_spec.rb +157 -0
  103. data/spec/lutaml/store/key_collision_serializer_spec.rb +98 -0
  104. data/spec/lutaml/store/load_save_spec.rb +107 -0
  105. data/spec/lutaml/store/lutaml_model_integration_spec.rb +291 -0
  106. data/spec/lutaml/store/model_serializer_spec.rb +140 -0
  107. data/spec/lutaml/store/store_spec.rb +182 -0
  108. data/spec/lutaml/store_spec.rb +21 -0
  109. data/spec/spec_helper.rb +16 -0
  110. metadata +166 -0
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Store
5
+ # Processes model updates including dot notation for nested attributes
6
+ class AttributeUpdater
7
+ def initialize(registry, composite_handler)
8
+ @registry = registry
9
+ @composite_handler = composite_handler
10
+ end
11
+
12
+ # Update model with attribute array
13
+ def update_with_attributes(model, attributes)
14
+ return model if attributes.nil? || attributes.empty?
15
+
16
+ updated_model = model.dup
17
+
18
+ attributes.each do |attr_update|
19
+ key = attr_update[:key]
20
+ value = attr_update[:value]
21
+
22
+ if key.to_s.include?(".")
23
+ update_nested_attribute(updated_model, key.to_s, value)
24
+ else
25
+ update_direct_attribute(updated_model, key, value)
26
+ end
27
+ end
28
+
29
+ @composite_handler.update_composite_models(updated_model, attributes)
30
+
31
+ updated_model
32
+ end
33
+
34
+ # Update model with block
35
+ def update_with_block(model, &block)
36
+ return model unless block_given?
37
+
38
+ updated_model = model.dup
39
+ block.call(updated_model)
40
+
41
+ composite_models = @registry.find_composite_models(updated_model)
42
+ @composite_handler.process_composite_models(updated_model) unless composite_models.empty?
43
+
44
+ updated_model
45
+ end
46
+
47
+ # Update model with hash
48
+ def update_with_hash(model, updates_hash)
49
+ return model if updates_hash.nil? || updates_hash.empty?
50
+
51
+ attributes = updates_hash.map { |key, value| { key: key, value: value } }
52
+ update_with_attributes(model, attributes)
53
+ end
54
+
55
+ private
56
+
57
+ def update_nested_attribute(model, attr_path, value)
58
+ parts = attr_path.split(".")
59
+ current = model
60
+
61
+ parts[0..-2].each do |part|
62
+ current = if part.match?(/\A\d+\z/)
63
+ current[part.to_i]
64
+ else
65
+ current.public_send(part)
66
+ end
67
+
68
+ raise InvalidKeyError, "Cannot navigate to #{attr_path}: #{part} is nil" if current.nil?
69
+ end
70
+
71
+ final_attr = parts.last
72
+ if final_attr.match?(/\A\d+\z/)
73
+ current[final_attr.to_i] = value
74
+ else
75
+ setter_method = "#{final_attr}="
76
+ begin
77
+ current.public_send(setter_method, value)
78
+ rescue NoMethodError
79
+ raise InvalidKeyError, "No setter method #{setter_method} on #{current.class}"
80
+ end
81
+ end
82
+ end
83
+
84
+ def update_direct_attribute(model, attr_name, value)
85
+ setter_method = "#{attr_name}="
86
+
87
+ begin
88
+ model.public_send(setter_method, value)
89
+ rescue NoMethodError
90
+ return if try_polymorphic_upgrade(model, attr_name, value)
91
+
92
+ raise InvalidKeyError, "No setter method #{setter_method} on #{model.class}"
93
+ end
94
+ end
95
+
96
+ # Validate attribute path without respond_to? on leaf — use is_a? for type checking
97
+ def validate_attribute_path(model, attr_path)
98
+ parts = attr_path.split(".")
99
+ current = model
100
+
101
+ parts.each_with_index do |part, index|
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)
104
+
105
+ 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
107
+
108
+ current = current[index_val]
109
+ else
110
+ unless current.is_a?(Lutaml::Model::Serializable) || current.public_methods.include?(part.to_sym)
111
+ raise InvalidKeyError, "No method #{part} on #{current.class} at #{parts[0..index - 1].join(".")}"
112
+ end
113
+
114
+ current = current.public_send(part)
115
+ end
116
+ end
117
+ end
118
+
119
+ def extract_polymorphic_updates(attributes)
120
+ attributes.select do |attr_update|
121
+ value = attr_update[:value]
122
+ value.is_a?(Object) && @registry.registered?(value.class)
123
+ end
124
+ end
125
+
126
+ def handle_polymorphic_update(model, attr_name, new_polymorphic_model)
127
+ unless @registry.registered?(new_polymorphic_model.class)
128
+ raise PolymorphicUpdateError,
129
+ "Cannot update with unregistered model #{new_polymorphic_model.class}"
130
+ end
131
+
132
+ 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)
134
+
135
+ model.public_send("#{attr_name}=", new_polymorphic_model)
136
+ end
137
+
138
+ # Try to upgrade a model to a polymorphic subclass that supports the attribute.
139
+ # Uses proper constructors instead of instance_variable_set/get.
140
+ def try_polymorphic_upgrade(model, attr_name, value)
141
+ return false unless @registry.registered?(model.class)
142
+
143
+ registration = @registry.registration_for(model.class)
144
+
145
+ @registry.registered_models.each do |registered_class|
146
+ next unless registered_class > model.class
147
+ next unless registered_class.instance_methods.include?("#{attr_name}=".to_sym)
148
+
149
+ subclass_registration = @registry.registration_for(registered_class)
150
+ next unless subclass_registration.key_field == registration.key_field
151
+
152
+ begin
153
+ current_data = model.to_hash
154
+ current_data[attr_name.to_s] = value
155
+
156
+ upgraded_model = registered_class.from_hash(current_data)
157
+ copy_model_attributes!(model, upgraded_model)
158
+ return true
159
+ rescue StandardError
160
+ next
161
+ end
162
+ end
163
+
164
+ false
165
+ end
166
+
167
+ # Copy all Lutaml::Model attributes from source to target using public setters
168
+ def copy_model_attributes!(target, source)
169
+ source.class.attributes.each_key do |attr_name|
170
+ setter = "#{attr_name}="
171
+ begin
172
+ target.public_send(setter, source.public_send(attr_name))
173
+ rescue NoMethodError
174
+ next
175
+ end
176
+ end
177
+ end
178
+
179
+ def validate_polymorphic_key_compatibility!(current_model, new_model)
180
+ current_registration = @registry.registration_for(current_model.class)
181
+ new_registration = @registry.registration_for(new_model.class)
182
+
183
+ if current_registration.key_field != new_registration.key_field
184
+ raise PolymorphicUpdateError,
185
+ "Key field mismatch: #{current_registration.key_field} vs #{new_registration.key_field}"
186
+ end
187
+
188
+ current_key = current_registration.extract_key(current_model)
189
+ new_key = new_registration.extract_key(new_model)
190
+
191
+ return unless current_key != new_key
192
+
193
+ raise PolymorphicUpdateError,
194
+ "Key value mismatch: #{current_key} vs #{new_key}"
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Store
5
+ class BasicStore
6
+ attr_reader :adapter, :cache, :monitor, :events, :config
7
+
8
+ def initialize(config_or_adapter = {})
9
+ if config_or_adapter.is_a?(Adapter::Base)
10
+ @adapter = config_or_adapter
11
+ @config = Config.new
12
+ elsif config_or_adapter.is_a?(Config)
13
+ @config = config_or_adapter
14
+ @adapter = create_adapter
15
+ else
16
+ @config = Config.new(**config_or_adapter.transform_keys(&:to_sym))
17
+ @adapter = create_adapter
18
+ end
19
+
20
+ @config.validate!
21
+
22
+ @cache = @config.cache_enabled? ? create_cache : nil
23
+ @monitor = @config.monitoring_enabled? ? Monitor.new : nil
24
+ @events = Events.new(async: @config.async_events?)
25
+ end
26
+
27
+ def self.from_file(file_path)
28
+ new(Config.from_file(file_path))
29
+ end
30
+
31
+ def self.from_yaml(yaml_string)
32
+ new(Config.from_yaml(yaml_string))
33
+ end
34
+
35
+ def get(key)
36
+ with_monitoring(:get) do
37
+ if @cache
38
+ cached_value = @cache.get(key)
39
+ if cached_value
40
+ emit_event(:get, key: key, value: cached_value, source: :cache)
41
+ next cached_value
42
+ end
43
+ end
44
+
45
+ value = @adapter.get(key)
46
+ @cache&.set(key, value) if value
47
+ emit_event(:get, key: key, value: value, source: :adapter)
48
+ value
49
+ end
50
+ end
51
+
52
+ def set(key, value)
53
+ with_monitoring(:set) do
54
+ @adapter.set(key, value)
55
+ @cache&.set(key, value)
56
+ emit_event(:set, key: key, value: value)
57
+ end
58
+ end
59
+
60
+ def delete(key)
61
+ with_monitoring(:delete) do
62
+ result = @adapter.delete(key)
63
+ @cache&.delete(key) if result
64
+ emit_event(:delete, key: key, deleted: result)
65
+ result
66
+ end
67
+ end
68
+
69
+ def exists?(key)
70
+ with_monitoring(:exists) do
71
+ next true if @cache&.exists?(key)
72
+
73
+ @adapter.exists?(key)
74
+ end
75
+ end
76
+
77
+ def all
78
+ with_monitoring(:all) do
79
+ @adapter.all
80
+ end
81
+ end
82
+
83
+ def clear
84
+ with_monitoring(:clear) do
85
+ @adapter.clear
86
+ @cache&.clear
87
+ emit_event(:clear)
88
+ end
89
+ end
90
+
91
+ def size
92
+ with_monitoring(:size) do
93
+ @adapter.size
94
+ end
95
+ end
96
+
97
+ def keys
98
+ @adapter.keys
99
+ end
100
+
101
+ def bulk_get(keys)
102
+ @adapter.bulk_get(keys)
103
+ end
104
+
105
+ def bulk_set(key_value_pairs)
106
+ @adapter.bulk_set(key_value_pairs)
107
+ key_value_pairs.each { |key, value| @cache&.set(key, value) }
108
+ end
109
+
110
+ def bulk_delete(keys)
111
+ result = @adapter.bulk_delete(keys)
112
+ keys.each { |key| @cache&.delete(key) }
113
+ result
114
+ end
115
+
116
+ def on(event, callable = nil, &block)
117
+ @events.on(event, callable, &block)
118
+ end
119
+
120
+ def off(event, listener)
121
+ @events.off(event, listener)
122
+ end
123
+
124
+ def emit_event(event, data = {})
125
+ @events.emit(event, data)
126
+ end
127
+
128
+ def stats
129
+ base_stats = {
130
+ adapter: @adapter.class.name,
131
+ size: size,
132
+ cache_enabled: !@cache.nil?,
133
+ monitoring_enabled: !@monitor.nil?
134
+ }
135
+
136
+ base_stats[:adapter_stats] = @adapter.stats
137
+ base_stats[:cache_stats] = @cache.stats if @cache
138
+ base_stats[:monitor_stats] = @monitor.stats if @monitor
139
+
140
+ base_stats
141
+ end
142
+
143
+ def cache_stats
144
+ @cache&.stats
145
+ end
146
+
147
+ def close
148
+ @events.stop if @config.async_events?
149
+ @adapter.close
150
+ end
151
+
152
+ private
153
+
154
+ def with_monitoring(operation)
155
+ start_time = Time.now
156
+ result = yield
157
+ record_operation(operation, Time.now - start_time, true)
158
+ result
159
+ rescue StandardError => e
160
+ record_operation(operation, Time.now - start_time, false)
161
+ @monitor&.record_error(operation, e)
162
+ raise
163
+ end
164
+
165
+ def create_adapter
166
+ case @config.adapter_type
167
+ when :memory
168
+ Adapter::Memory.new(@config.adapter_options)
169
+ when :filesystem
170
+ Adapter::FileSystem.new(@config.adapter_options)
171
+ when :sqlite
172
+ Adapter::Sqlite.new(@config.adapter_options)
173
+ else
174
+ raise ConfigurationError, "Unknown adapter type: #{@config.adapter_type}"
175
+ end
176
+ end
177
+
178
+ def create_cache
179
+ Cache.new(
180
+ max_size: @config.cache_max_size,
181
+ ttl: @config.cache_ttl
182
+ )
183
+ end
184
+
185
+ def record_operation(operation, duration, success)
186
+ @monitor&.record_operation(operation, duration: duration, success: success)
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Store
5
+ class Cache
6
+ Entry = Struct.new(:value, :timestamp, keyword_init: true)
7
+
8
+ DEFAULT_MAX_SIZE = 1000
9
+
10
+ def initialize(max_size: DEFAULT_MAX_SIZE, ttl: nil)
11
+ @max_size = max_size
12
+ @ttl = ttl
13
+ @data = {}
14
+ @access_order = []
15
+ @mutex = Mutex.new
16
+ end
17
+
18
+ def get(key)
19
+ @mutex.synchronize do
20
+ entry = @data[key]
21
+ return nil unless entry
22
+ return nil if expired?(entry)
23
+
24
+ @access_order.delete(key)
25
+ @access_order << key
26
+ entry.value
27
+ end
28
+ end
29
+
30
+ def set(key, value)
31
+ @mutex.synchronize do
32
+ @access_order.delete(key) if @data.key?(key)
33
+
34
+ @data[key] = Entry.new(value: value, timestamp: Time.now)
35
+ @access_order << key
36
+
37
+ evict_lru while @data.size > @max_size
38
+ end
39
+ end
40
+
41
+ def delete(key)
42
+ @mutex.synchronize do
43
+ if @data.delete(key)
44
+ @access_order.delete(key)
45
+ true
46
+ else
47
+ false
48
+ end
49
+ end
50
+ end
51
+
52
+ def exists?(key)
53
+ @mutex.synchronize do
54
+ entry = @data[key]
55
+ entry && !expired?(entry)
56
+ end
57
+ end
58
+
59
+ def clear
60
+ @mutex.synchronize do
61
+ @data.clear
62
+ @access_order.clear
63
+ end
64
+ end
65
+
66
+ def size
67
+ @mutex.synchronize { @data.size }
68
+ end
69
+
70
+ def stats
71
+ @mutex.synchronize do
72
+ {
73
+ size: @data.size,
74
+ max_size: @max_size,
75
+ ttl: @ttl,
76
+ keys: @data.keys
77
+ }
78
+ end
79
+ end
80
+
81
+ def cleanup_expired
82
+ @mutex.synchronize do
83
+ expired_keys = @data.select { |_, entry| expired?(entry) }.keys
84
+ expired_keys.each do |key|
85
+ @data.delete(key)
86
+ @access_order.delete(key)
87
+ end
88
+ expired_keys.size
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def expired?(entry)
95
+ return false unless @ttl
96
+
97
+ Time.now - entry.timestamp > @ttl
98
+ end
99
+
100
+ def evict_lru
101
+ return if @access_order.empty?
102
+
103
+ lru_key = @access_order.shift
104
+ @data.delete(lru_key)
105
+ end
106
+ end
107
+ end
108
+ end