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.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +27 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +10 -0
- data/.rubocop_todo.yml +450 -0
- data/CLAUDE.md +57 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +209 -0
- data/CORRECTED_HTTP_CACHE_PLAN.md +164 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +220 -0
- data/README.adoc +1430 -0
- data/Rakefile +12 -0
- data/TODO.impl/0-lutaml-store-self-quality.md +112 -0
- data/TODO.impl/1-lutaml-hal-migration.md +60 -0
- data/TODO.impl/2-glossarist-migration.md +359 -0
- data/TODO.impl/3-lutaml-jsonschema-migration.md +273 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/demo/Gemfile +15 -0
- data/demo/Gemfile.lock +61 -0
- data/demo/README.adoc +301 -0
- data/demo/data/vcards/co/contact_10_thompson.data +1 -0
- data/demo/data/vcards/co/contact_10_thompson.meta +1 -0
- data/demo/data/vcards/co/contact_1_doe.data +1 -0
- data/demo/data/vcards/co/contact_1_doe.meta +1 -0
- data/demo/data/vcards/co/contact_2_smith.data +1 -0
- data/demo/data/vcards/co/contact_2_smith.meta +1 -0
- data/demo/data/vcards/co/contact_3_johnson.data +1 -0
- data/demo/data/vcards/co/contact_3_johnson.meta +1 -0
- data/demo/data/vcards/co/contact_4_garcia.data +1 -0
- data/demo/data/vcards/co/contact_4_garcia.meta +1 -0
- data/demo/data/vcards/co/contact_5_wilson.data +1 -0
- data/demo/data/vcards/co/contact_5_wilson.meta +1 -0
- data/demo/data/vcards/co/contact_6_brown.data +1 -0
- data/demo/data/vcards/co/contact_6_brown.meta +1 -0
- data/demo/data/vcards/co/contact_7_davis.data +1 -0
- data/demo/data/vcards/co/contact_7_davis.meta +1 -0
- data/demo/data/vcards/co/contact_8_anderson.data +1 -0
- data/demo/data/vcards/co/contact_8_anderson.meta +1 -0
- data/demo/data/vcards/co/contact_9_taylor.data +1 -0
- data/demo/data/vcards/co/contact_9_taylor.meta +1 -0
- data/demo/data/vcards.db +0 -0
- data/demo/pottery_class_demo.rb +164 -0
- data/demo/vcard_models.rb +140 -0
- data/demo/vcard_store_demo.rb +526 -0
- data/lib/lutaml/store/adapter/base.rb +65 -0
- data/lib/lutaml/store/adapter/filesystem.rb +288 -0
- data/lib/lutaml/store/adapter/memory.rb +225 -0
- data/lib/lutaml/store/adapter/sqlite.rb +193 -0
- data/lib/lutaml/store/adapter.rb +12 -0
- data/lib/lutaml/store/attribute_updater.rb +198 -0
- data/lib/lutaml/store/basic_store.rb +190 -0
- data/lib/lutaml/store/cache.rb +108 -0
- data/lib/lutaml/store/cache_store.rb +282 -0
- data/lib/lutaml/store/composite_model_handler.rb +169 -0
- data/lib/lutaml/store/compression.rb +137 -0
- data/lib/lutaml/store/config.rb +178 -0
- data/lib/lutaml/store/database_store.rb +425 -0
- data/lib/lutaml/store/events.rb +92 -0
- data/lib/lutaml/store/format/base.rb +33 -0
- data/lib/lutaml/store/format/json.rb +25 -0
- data/lib/lutaml/store/format/jsonl.rb +37 -0
- data/lib/lutaml/store/format/marshal_format.rb +37 -0
- data/lib/lutaml/store/format/yaml.rb +29 -0
- data/lib/lutaml/store/format/yamls.rb +35 -0
- data/lib/lutaml/store/format.rb +33 -0
- data/lib/lutaml/store/http_cache.rb +279 -0
- data/lib/lutaml/store/http_cache_config.rb +53 -0
- data/lib/lutaml/store/http_cache_entry.rb +69 -0
- data/lib/lutaml/store/http_header_processor.rb +175 -0
- data/lib/lutaml/store/integrity.rb +102 -0
- data/lib/lutaml/store/model_registration.rb +75 -0
- data/lib/lutaml/store/model_registry.rb +123 -0
- data/lib/lutaml/store/model_serializer.rb +69 -0
- data/lib/lutaml/store/monitor.rb +192 -0
- data/lib/lutaml/store/storage_key.rb +40 -0
- data/lib/lutaml/store/version.rb +7 -0
- data/lib/lutaml/store.rb +41 -0
- data/lutaml-store.gemspec +35 -0
- data/plan.adoc +606 -0
- data/sig/lutaml/store.rbs +6 -0
- data/spec/lutaml/store/adapter_interface_spec.rb +89 -0
- data/spec/lutaml/store/anti_pattern_guard_spec.rb +35 -0
- data/spec/lutaml/store/anti_pattern_spec.rb +78 -0
- data/spec/lutaml/store/autoload_spec.rb +34 -0
- data/spec/lutaml/store/cache_store_spec.rb +271 -0
- data/spec/lutaml/store/compression_spec.rb +78 -0
- data/spec/lutaml/store/config_enhanced_spec.rb +158 -0
- data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +336 -0
- data/spec/lutaml/store/custom_serializer_spec.rb +108 -0
- data/spec/lutaml/store/database_store_spec.rb +279 -0
- data/spec/lutaml/store/file_io_spec.rb +219 -0
- data/spec/lutaml/store/format_round_trip_spec.rb +110 -0
- data/spec/lutaml/store/format_spec.rb +70 -0
- data/spec/lutaml/store/http_cache_entry_spec.rb +203 -0
- data/spec/lutaml/store/http_cache_hal_integration_spec.rb +404 -0
- data/spec/lutaml/store/http_cache_spec.rb +422 -0
- data/spec/lutaml/store/http_header_processor_spec.rb +290 -0
- data/spec/lutaml/store/import_spec.rb +90 -0
- data/spec/lutaml/store/integrity_spec.rb +157 -0
- data/spec/lutaml/store/key_collision_serializer_spec.rb +98 -0
- data/spec/lutaml/store/load_save_spec.rb +107 -0
- data/spec/lutaml/store/lutaml_model_integration_spec.rb +291 -0
- data/spec/lutaml/store/model_serializer_spec.rb +140 -0
- data/spec/lutaml/store/store_spec.rb +182 -0
- data/spec/lutaml/store_spec.rb +21 -0
- data/spec/spec_helper.rb +16 -0
- 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
|