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,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Enforces code quality rules from CLAUDE.md global instructions.
6
+ # These specs MUST pass — any failure means an anti-pattern was introduced.
7
+ RSpec.describe "Anti-pattern guard" do
8
+ lib_dir = File.expand_path("../../../lib", __dir__)
9
+ rb_files = Dir.glob(File.join(lib_dir, "**", "*.rb"))
10
+
11
+ rb_files.each do |path|
12
+ rel = path.sub("#{lib_dir}/", "")
13
+
14
+ it "#{rel} does not use .send() to call private methods" do
15
+ code = File.read(path).lines.reject { |l| l.strip.start_with?("#") }.join
16
+ code = code.gsub(/#.*$/, "")
17
+ uses = code.scan(/\.send\s*\(/)
18
+ expect(uses).to be_empty, "#{rel} uses .send() — refactor to use a public API"
19
+ end
20
+
21
+ it "#{rel} does not use instance_variable_get or instance_variable_set" do
22
+ code = File.read(path).lines.reject { |l| l.strip.start_with?("#") }.join
23
+ code = code.gsub(/#.*$/, "")
24
+ uses = code.scan(/instance_variable_(get|set)/)
25
+ expect(uses).to be_empty, "#{rel} uses instance_variable_get/set — add a public accessor"
26
+ end
27
+
28
+ it "#{rel} does not use respond_to? for type checking" do
29
+ code = File.read(path).lines.reject { |l| l.strip.start_with?("#") }.join
30
+ code = code.gsub(/#.*$/, "")
31
+ uses = code.scan(/respond_to\?\s*/)
32
+ expect(uses).to be_empty, "#{rel} uses respond_to? — use is_a? or redesign the type hierarchy"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Anti-pattern regression prevention" do
6
+ let(:lib_dir) { File.expand_path("../../lib/lutaml/store", __dir__) }
7
+
8
+ def ruby_files
9
+ Dir.glob(File.join(lib_dir, "**", "*.rb"))
10
+ end
11
+
12
+ def file_contents
13
+ ruby_files.to_h { |f| [f, File.read(f)] }
14
+ end
15
+
16
+ describe "no respond_to? in lib code" do
17
+ it "contains zero respond_to? calls" do
18
+ offenders = file_contents.filter_map do |path, content|
19
+ matches = content.scan(/^.*respond_to\?.*$/)
20
+ next if matches.empty?
21
+ next if matches.all? { |line| line.strip.start_with?("#") }
22
+
23
+ rel = path.sub(%r{.*lib/}, "")
24
+ count = matches.count { |line| !line.strip.start_with?("#") }
25
+ "#{rel}: #{count} occurrence(s)"
26
+ end
27
+
28
+ expect(offenders).to be_empty,
29
+ "Found respond_to? in lib code:\n#{offenders.join("\n")}"
30
+ end
31
+ end
32
+
33
+ describe "no instance_variable_get/set in lib code" do
34
+ it "contains zero instance_variable_get calls" do
35
+ offenders = file_contents.filter_map do |path, content|
36
+ matches = content.scan(/^.*instance_variable_get.*$/)
37
+ next if matches.empty?
38
+ next if matches.all? { |line| line.strip.start_with?("#") }
39
+
40
+ rel = path.sub(%r{.*lib/}, "")
41
+ "#{rel}: #{matches.size} occurrence(s)"
42
+ end
43
+
44
+ expect(offenders).to be_empty,
45
+ "Found instance_variable_get in lib code:\n#{offenders.join("\n")}"
46
+ end
47
+
48
+ it "contains zero instance_variable_set calls" do
49
+ offenders = file_contents.filter_map do |path, content|
50
+ matches = content.scan(/^.*instance_variable_set.*$/)
51
+ next if matches.empty?
52
+ next if matches.all? { |line| line.strip.start_with?("#") }
53
+
54
+ rel = path.sub(%r{.*lib/}, "")
55
+ "#{rel}: #{matches.size} occurrence(s)"
56
+ end
57
+
58
+ expect(offenders).to be_empty,
59
+ "Found instance_variable_set in lib code:\n#{offenders.join("\n")}"
60
+ end
61
+ end
62
+
63
+ describe "no send on private methods in lib code" do
64
+ it "contains zero .send( calls" do
65
+ offenders = file_contents.filter_map do |path, content|
66
+ matches = content.scan(/^.*\.send\(.*$/)
67
+ next if matches.empty?
68
+ next if matches.all? { |line| line.strip.start_with?("#") }
69
+
70
+ rel = path.sub(%r{.*lib/}, "")
71
+ "#{rel}: #{matches.size} occurrence(s)"
72
+ end
73
+
74
+ expect(offenders).to be_empty,
75
+ "Found .send( in lib code:\n#{offenders.join("\n")}"
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Autoload" do
6
+ it "loads BasicStore without sqlite3" do
7
+ store = Lutaml::Store::BasicStore.new(adapter_type: :memory)
8
+ store.set("key", "value")
9
+ expect(store.get("key")).to eq("value")
10
+
11
+ sqlite_loaded = $LOADED_FEATURES.any? { |path| path.include?("sqlite3") }
12
+ expect(sqlite_loaded).to be false
13
+ end
14
+
15
+ it "loads all constants lazily" do
16
+ # After requiring lutaml/store, only error classes and autoload entries exist
17
+ expect(Lutaml::Store::Error).to be_a(Class)
18
+ expect(Lutaml::Store::ConfigurationError).to be_a(Class)
19
+ expect(Lutaml::Store::BackendError).to be_a(Class)
20
+ end
21
+
22
+ it "loads DatabaseStore on first use" do
23
+ expect(defined?(Lutaml::Store::DatabaseStore)).to eq("constant")
24
+ end
25
+
26
+ it "loads Adapter::Base and subclasses on first reference" do
27
+ expect(defined?(Lutaml::Store::Adapter::Base)).to eq("constant")
28
+ expect(defined?(Lutaml::Store::Adapter::Memory)).to eq("constant")
29
+ end
30
+
31
+ it "loads BasicStore lazily" do
32
+ expect(defined?(Lutaml::Store::BasicStore)).to eq("constant")
33
+ end
34
+ end
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Lutaml::Store::CacheStore do
6
+ let(:config) do
7
+ {
8
+ adapter: {
9
+ type: :memory
10
+ },
11
+ default_ttl: 60,
12
+ max_size: 100,
13
+ cleanup_interval: 10
14
+ }
15
+ end
16
+ let(:cache) { described_class.new(config) }
17
+
18
+ describe "CacheEntry" do
19
+ let(:entry) { described_class::CacheEntry.new("value", ttl: 60, metadata: { source: "test" }) }
20
+
21
+ it "tracks creation time" do
22
+ expect(entry.created_at).to be_within(1).of(Time.now)
23
+ end
24
+
25
+ it "calculates expiration" do
26
+ expect(entry.expired?).to be false
27
+
28
+ # Simulate time passing
29
+ allow(Time).to receive(:now).and_return(entry.created_at + 61)
30
+ expect(entry.expired?).to be true
31
+ end
32
+
33
+ it "calculates expires_at" do
34
+ expect(entry.expires_at).to be_within(1).of(entry.created_at + 60)
35
+ end
36
+
37
+ it "serializes to hash" do
38
+ hash = entry.to_h
39
+ expect(hash).to include(:value, :created_at, :ttl, :expires_at, :metadata)
40
+ expect(hash[:value]).to eq("value")
41
+ expect(hash[:ttl]).to eq(60)
42
+ expect(hash[:metadata]).to eq({ source: "test" })
43
+ end
44
+
45
+ it "deserializes from hash" do
46
+ hash = entry.to_h
47
+ restored = described_class::CacheEntry.from_h(hash)
48
+
49
+ expect(restored.value).to eq(entry.value)
50
+ expect(restored.ttl).to eq(entry.ttl)
51
+ expect(restored.metadata).to eq(entry.metadata)
52
+ expect(restored.created_at).to be_within(1).of(entry.created_at)
53
+ end
54
+ end
55
+
56
+ describe "#set and #get" do
57
+ it "stores and retrieves values" do
58
+ cache.set("key1", "value1")
59
+ expect(cache.get("key1")).to eq("value1")
60
+ end
61
+
62
+ it "returns nil for non-existent keys" do
63
+ expect(cache.get("nonexistent")).to be_nil
64
+ end
65
+
66
+ it "respects TTL" do
67
+ cache.set("key1", "value1", ttl: 1)
68
+ expect(cache.get("key1")).to eq("value1")
69
+
70
+ # Simulate time passing
71
+ allow(Time).to receive(:now).and_return(Time.now + 2)
72
+ expect(cache.get("key1")).to be_nil
73
+ end
74
+
75
+ it "uses default TTL when not specified" do
76
+ cache.set("key1", "value1")
77
+ expect(cache.ttl("key1")).to be_within(5).of(60)
78
+ end
79
+
80
+ it "stores metadata" do
81
+ cache.set("key1", "value1", metadata: { source: "api" })
82
+
83
+ entry_data = cache.adapter.get("key1")
84
+ parsed = JSON.parse(entry_data, symbolize_names: true)
85
+ expect(parsed[:metadata]).to eq({ source: "api" })
86
+ end
87
+ end
88
+
89
+ describe "#exists?" do
90
+ it "returns true for existing non-expired keys" do
91
+ cache.set("key1", "value1")
92
+ expect(cache.exists?("key1")).to be true
93
+ end
94
+
95
+ it "returns false for non-existent keys" do
96
+ expect(cache.exists?("nonexistent")).to be false
97
+ end
98
+
99
+ it "returns false for expired keys" do
100
+ cache.set("key1", "value1", ttl: 1)
101
+
102
+ # Simulate time passing
103
+ allow(Time).to receive(:now).and_return(Time.now + 2)
104
+ expect(cache.exists?("key1")).to be false
105
+ end
106
+ end
107
+
108
+ describe "#ttl" do
109
+ it "returns remaining TTL" do
110
+ cache.set("key1", "value1", ttl: 60)
111
+ expect(cache.ttl("key1")).to be_within(5).of(60)
112
+ end
113
+
114
+ it "returns nil for keys without TTL" do
115
+ cache.set("key1", "value1", ttl: nil)
116
+ expect(cache.ttl("key1")).to be_nil
117
+ end
118
+
119
+ it "returns nil for expired keys" do
120
+ cache.set("key1", "value1", ttl: 1)
121
+
122
+ # Simulate time passing
123
+ allow(Time).to receive(:now).and_return(Time.now + 2)
124
+ expect(cache.ttl("key1")).to be_nil
125
+ end
126
+ end
127
+
128
+ describe "#touch" do
129
+ it "updates TTL for existing key" do
130
+ cache.set("key1", "value1", ttl: 60)
131
+ cache.ttl("key1")
132
+
133
+ # Simulate some time passing
134
+ allow(Time).to receive(:now).and_return(Time.now + 30)
135
+
136
+ expect(cache.touch("key1", ttl: 120)).to be true
137
+ expect(cache.ttl("key1")).to be_within(5).of(120)
138
+ end
139
+
140
+ it "returns false for non-existent keys" do
141
+ expect(cache.touch("nonexistent")).to be false
142
+ end
143
+
144
+ it "returns false for expired keys" do
145
+ cache.set("key1", "value1", ttl: 1)
146
+
147
+ # Simulate time passing
148
+ allow(Time).to receive(:now).and_return(Time.now + 2)
149
+ expect(cache.touch("key1")).to be false
150
+ end
151
+ end
152
+
153
+ describe "#fetch" do
154
+ it "returns existing value" do
155
+ cache.set("key1", "value1")
156
+
157
+ result = cache.fetch("key1") { "new_value" }
158
+ expect(result).to eq("value1")
159
+ end
160
+
161
+ it "executes block for missing key" do
162
+ result = cache.fetch("key1") { "new_value" }
163
+ expect(result).to eq("new_value")
164
+ expect(cache.get("key1")).to eq("new_value")
165
+ end
166
+
167
+ it "returns nil for missing key without block" do
168
+ result = cache.fetch("key1")
169
+ expect(result).to be_nil
170
+ end
171
+
172
+ it "respects TTL in fetch" do
173
+ result = cache.fetch("key1", ttl: 30) { "new_value" }
174
+ expect(result).to eq("new_value")
175
+ expect(cache.ttl("key1")).to be_within(5).of(30)
176
+ end
177
+ end
178
+
179
+ describe "#cleanup_expired" do
180
+ it "removes expired entries" do
181
+ cache.set("key1", "value1", ttl: 1)
182
+ cache.set("key2", "value2", ttl: 60)
183
+
184
+ # Simulate time passing
185
+ allow(Time).to receive(:now).and_return(Time.now + 2)
186
+
187
+ expired_count = cache.cleanup_expired
188
+ expect(expired_count).to eq(1)
189
+ expect(cache.exists?("key1")).to be false
190
+ expect(cache.exists?("key2")).to be true
191
+ end
192
+ end
193
+
194
+ describe "#cache_info" do
195
+ it "provides cache statistics" do
196
+ cache.set("key1", "value1", ttl: 1)
197
+ cache.set("key2", "value2", ttl: 60)
198
+
199
+ # Simulate time passing to expire one entry
200
+ allow(Time).to receive(:now).and_return(Time.now + 2)
201
+
202
+ info = cache.cache_info
203
+ expect(info).to include(:total_entries, :valid_entries, :expired_entries, :max_size, :default_ttl)
204
+ expect(info[:total_entries]).to eq(2)
205
+ expect(info[:valid_entries]).to eq(1)
206
+ expect(info[:expired_entries]).to eq(1)
207
+ end
208
+ end
209
+
210
+ describe "LRU eviction" do
211
+ let(:small_cache) { described_class.new(config.merge(max_size: 2)) }
212
+
213
+ it "evicts least recently used entries when max size is reached" do
214
+ small_cache.set("key1", "value1")
215
+ small_cache.set("key2", "value2")
216
+
217
+ # Access key1 to make it more recently used
218
+ small_cache.get("key1")
219
+
220
+ # Adding key3 should evict key2 (least recently used)
221
+ small_cache.set("key3", "value3")
222
+
223
+ expect(small_cache.exists?("key1")).to be true
224
+ expect(small_cache.exists?("key2")).to be false
225
+ expect(small_cache.exists?("key3")).to be true
226
+ end
227
+ end
228
+
229
+ describe "#clear" do
230
+ it "removes all entries" do
231
+ cache.set("key1", "value1")
232
+ cache.set("key2", "value2")
233
+
234
+ cache.clear
235
+
236
+ expect(cache.size).to eq(0)
237
+ expect(cache.exists?("key1")).to be false
238
+ expect(cache.exists?("key2")).to be false
239
+ end
240
+ end
241
+
242
+ describe "#delete" do
243
+ it "removes specific entry" do
244
+ cache.set("key1", "value1")
245
+ cache.set("key2", "value2")
246
+
247
+ deleted_value = cache.delete("key1")
248
+
249
+ expect(deleted_value).to eq("value1")
250
+ expect(cache.exists?("key1")).to be false
251
+ expect(cache.exists?("key2")).to be true
252
+ end
253
+ end
254
+
255
+ describe "automatic cleanup" do
256
+ let(:auto_cache) { described_class.new(config.merge(cleanup_interval: 1)) }
257
+
258
+ it "automatically cleans up expired entries" do
259
+ auto_cache.set("key1", "value1", ttl: 1)
260
+
261
+ # Simulate time passing beyond cleanup interval and TTL
262
+ allow(Time).to receive(:now).and_return(Time.now + 2)
263
+
264
+ # Next operation should trigger cleanup
265
+ auto_cache.set("key2", "value2")
266
+
267
+ expect(auto_cache.exists?("key1")).to be false
268
+ expect(auto_cache.exists?("key2")).to be true
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Lutaml::Store::Compression do
6
+ describe ".compress and .decompress" do
7
+ let(:test_data) { "Hello, World! This is a test string for compression." }
8
+
9
+ context "with gzip compression" do
10
+ it "compresses and decompresses data correctly" do
11
+ compressed = described_class.compress(test_data, "gzip")
12
+ expect(compressed).not_to eq(test_data)
13
+ # NOTE: For small strings, compression might actually increase size due to headers
14
+ # The important thing is that decompression works correctly
15
+
16
+ decompressed = described_class.decompress(compressed, "gzip")
17
+ expect(decompressed).to eq(test_data)
18
+ end
19
+
20
+ it "supports different compression levels" do
21
+ compressed_1 = described_class.compress(test_data, "gzip", 1)
22
+ compressed_9 = described_class.compress(test_data, "gzip", 9)
23
+
24
+ expect(compressed_1.length).to be >= compressed_9.length
25
+ expect(described_class.decompress(compressed_1, "gzip")).to eq(test_data)
26
+ expect(described_class.decompress(compressed_9, "gzip")).to eq(test_data)
27
+ end
28
+ end
29
+
30
+ context "with deflate compression" do
31
+ it "compresses and decompresses data correctly" do
32
+ compressed = described_class.compress(test_data, "deflate")
33
+ expect(compressed).not_to eq(test_data)
34
+
35
+ decompressed = described_class.decompress(compressed, "deflate")
36
+ expect(decompressed).to eq(test_data)
37
+ end
38
+ end
39
+
40
+ context "with unsupported algorithm" do
41
+ it "raises an error for unsupported compression algorithm" do
42
+ expect do
43
+ described_class.compress(test_data, "unsupported")
44
+ end.to raise_error(ArgumentError, /Unsupported compression algorithm/)
45
+ end
46
+
47
+ it "raises an error for unsupported decompression algorithm" do
48
+ expect do
49
+ described_class.decompress(test_data, "unsupported")
50
+ end.to raise_error(ArgumentError, /Unsupported compression algorithm/)
51
+ end
52
+ end
53
+ end
54
+
55
+ describe ".detect_algorithm" do
56
+ let(:test_data) { "Hello, World!" }
57
+
58
+ it "detects gzip compression" do
59
+ compressed = described_class.compress(test_data, "gzip")
60
+ expect(described_class.detect_algorithm(compressed)).to eq("gzip")
61
+ end
62
+
63
+ it "detects deflate compression" do
64
+ compressed = described_class.compress(test_data, "deflate")
65
+ expect(described_class.detect_algorithm(compressed)).to eq("deflate")
66
+ end
67
+
68
+ it "returns nil for uncompressed data" do
69
+ expect(described_class.detect_algorithm(test_data)).to be_nil
70
+ end
71
+ end
72
+
73
+ describe "SUPPORTED_ALGORITHMS" do
74
+ it "includes expected algorithms" do
75
+ expect(described_class::SUPPORTED_ALGORITHMS).to include("gzip", "deflate", "bzip2", "lz4", "zstd")
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Lutaml::Store::Config do
6
+ describe "enhanced configuration features" do
7
+ let(:enhanced_config) do
8
+ {
9
+ adapter_type: :memory,
10
+ adapter_options: {},
11
+ cache: {
12
+ enabled: true,
13
+ max_size: 500,
14
+ ttl: 3600
15
+ },
16
+ monitoring: {
17
+ enabled: true
18
+ },
19
+ events: {
20
+ async: true
21
+ },
22
+ serialization: {
23
+ formats: %w[marshal json yaml],
24
+ validate_on_write: true
25
+ },
26
+ compression: {
27
+ enabled: true,
28
+ algorithm: "gzip",
29
+ level: 9
30
+ }
31
+ }
32
+ end
33
+
34
+ subject { described_class.new(**enhanced_config) }
35
+
36
+ describe "serialization configuration" do
37
+ it "provides access to serialization formats" do
38
+ expect(subject.serialization_formats).to eq(%w[marshal json yaml])
39
+ end
40
+
41
+ it "provides access to validate_on_write setting" do
42
+ expect(subject.validate_on_write?).to be true
43
+ end
44
+
45
+ context "with default configuration" do
46
+ subject { described_class.new }
47
+
48
+ it "has default serialization formats" do
49
+ expect(subject.serialization_formats).to include("marshal", "hash", "json", "yaml", "xml", "toml")
50
+ end
51
+
52
+ it "has validate_on_write disabled by default" do
53
+ expect(subject.validate_on_write?).to be false
54
+ end
55
+ end
56
+ end
57
+
58
+ describe "compression configuration" do
59
+ it "provides access to compression enabled setting" do
60
+ expect(subject.compression_enabled?).to be true
61
+ end
62
+
63
+ it "provides access to compression algorithm" do
64
+ expect(subject.compression_algorithm).to eq("gzip")
65
+ end
66
+
67
+ it "provides access to compression level" do
68
+ expect(subject.compression_level).to eq(9)
69
+ end
70
+
71
+ context "with default configuration" do
72
+ subject { described_class.new }
73
+
74
+ it "has compression disabled by default" do
75
+ expect(subject.compression_enabled?).to be false
76
+ end
77
+
78
+ it "has default compression algorithm" do
79
+ expect(subject.compression_algorithm).to eq("gzip")
80
+ end
81
+
82
+ it "has default compression level" do
83
+ expect(subject.compression_level).to eq(6)
84
+ end
85
+ end
86
+ end
87
+
88
+ describe "configuration merging" do
89
+ it "merges partial configuration with defaults" do
90
+ config = described_class.new(compression: { enabled: true })
91
+ expect(config.compression_enabled?).to be true
92
+ expect(config.compression_algorithm).to eq("gzip") # default
93
+ expect(config.compression_level).to eq(6) # default
94
+ end
95
+ end
96
+
97
+ describe "YAML configuration loading" do
98
+ let(:yaml_config) do
99
+ <<~YAML
100
+ lutaml_store:
101
+ adapter:
102
+ type: filesystem
103
+ options:
104
+ path: /tmp/test_store
105
+ compression:
106
+ enabled: true
107
+ algorithm: deflate
108
+ level: 3
109
+ serialization:
110
+ validate_on_write: true
111
+ formats:
112
+ - json
113
+ - yaml
114
+ YAML
115
+ end
116
+
117
+ it "loads enhanced configuration from YAML" do
118
+ config = described_class.from_yaml(yaml_config)
119
+
120
+ expect(config.adapter_type).to eq(:filesystem)
121
+ expect(config.adapter_options[:path]).to eq("/tmp/test_store")
122
+ expect(config.compression_enabled?).to be true
123
+ expect(config.compression_algorithm).to eq("deflate")
124
+ expect(config.compression_level).to eq(3)
125
+ expect(config.validate_on_write?).to be true
126
+ expect(config.serialization_formats).to eq(%w[json yaml])
127
+ end
128
+ end
129
+
130
+ describe "#to_h" do
131
+ it "includes all configuration sections" do
132
+ hash = subject.to_h
133
+
134
+ expect(hash).to have_key(:adapter)
135
+ expect(hash).to have_key(:cache)
136
+ expect(hash).to have_key(:monitoring)
137
+ expect(hash).to have_key(:events)
138
+ # NOTE: to_h method needs to be updated to include new sections
139
+ end
140
+ end
141
+ end
142
+
143
+ describe "configuration validation" do
144
+ context "with invalid compression settings" do
145
+ it "validates compression algorithm" do
146
+ # This would require adding validation for compression settings
147
+ # in the Config class validate! method
148
+ end
149
+ end
150
+
151
+ context "with invalid serialization settings" do
152
+ it "validates serialization formats" do
153
+ # This would require adding validation for serialization settings
154
+ # in the Config class validate! method
155
+ end
156
+ end
157
+ end
158
+ end