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,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Lutaml
6
+ module Store
7
+ # TTL-aware cache store with LRU eviction. Wraps a storage adapter directly.
8
+ class CacheStore
9
+ class CacheEntry
10
+ attr_reader :value, :created_at, :ttl, :metadata
11
+
12
+ def initialize(value, ttl: nil, metadata: {}, created_at: nil)
13
+ @value = value
14
+ @created_at = created_at || Time.now
15
+ @ttl = ttl
16
+ @metadata = metadata
17
+ end
18
+
19
+ def expired?
20
+ return false unless @ttl
21
+
22
+ Time.now - @created_at > @ttl
23
+ end
24
+
25
+ def expires_at
26
+ return nil unless @ttl
27
+
28
+ @created_at + @ttl
29
+ end
30
+
31
+ def to_h
32
+ {
33
+ value: @value,
34
+ created_at: @created_at.iso8601,
35
+ ttl: @ttl,
36
+ expires_at: expires_at&.iso8601,
37
+ metadata: @metadata
38
+ }
39
+ end
40
+
41
+ def self.from_h(hash)
42
+ new(
43
+ hash[:value],
44
+ ttl: hash[:ttl],
45
+ metadata: hash[:metadata] || {},
46
+ created_at: Time.parse(hash[:created_at])
47
+ )
48
+ end
49
+ end
50
+
51
+ attr_reader :adapter
52
+
53
+ def initialize(config = {})
54
+ @adapter = create_adapter(config)
55
+ @default_ttl = config[:default_ttl]
56
+ @max_size = config[:max_size]
57
+ @cleanup_interval = config[:cleanup_interval] || 300
58
+ @last_cleanup = Time.now
59
+ @access_times = {}
60
+ end
61
+
62
+ def get(key)
63
+ cleanup_expired if should_cleanup?
64
+
65
+ entry_data = @adapter.get(key)
66
+ return nil unless entry_data
67
+
68
+ begin
69
+ entry = deserialize_entry(entry_data)
70
+
71
+ if entry.expired?
72
+ delete(key)
73
+ return nil
74
+ end
75
+
76
+ @access_times[key] = Time.now
77
+ entry.value
78
+ rescue StandardError
79
+ delete(key)
80
+ nil
81
+ end
82
+ end
83
+
84
+ def set(key, value, ttl: :default, metadata: {})
85
+ cleanup_expired if should_cleanup?
86
+ evict_if_needed
87
+
88
+ effective_ttl = ttl == :default ? @default_ttl : ttl
89
+ entry = CacheEntry.new(value, ttl: effective_ttl, metadata: metadata)
90
+
91
+ serialized_entry = serialize_entry(entry)
92
+ @adapter.set(key, serialized_entry)
93
+
94
+ @access_times[key] = Time.now
95
+ value
96
+ end
97
+
98
+ def delete(key)
99
+ value = nil
100
+ entry_data = @adapter.get(key)
101
+ if entry_data
102
+ begin
103
+ entry = deserialize_entry(entry_data)
104
+ value = entry.value unless entry.expired?
105
+ rescue StandardError
106
+ # If we can't deserialize, treat as nil
107
+ end
108
+ end
109
+
110
+ @access_times.delete(key)
111
+
112
+ deleted = @adapter.delete(key)
113
+
114
+ deleted ? value : nil
115
+ end
116
+
117
+ def clear
118
+ @access_times.clear
119
+ @adapter.clear
120
+ end
121
+
122
+ def exists?(key)
123
+ return false unless @adapter.exists?(key)
124
+
125
+ entry_data = @adapter.get(key)
126
+ return false unless entry_data
127
+
128
+ begin
129
+ entry = deserialize_entry(entry_data)
130
+ !entry.expired?
131
+ rescue StandardError
132
+ false
133
+ end
134
+ end
135
+
136
+ def keys
137
+ cleanup_expired if should_cleanup?
138
+ @adapter.keys.select { |key| exists?(key) }
139
+ end
140
+
141
+ def size
142
+ cleanup_expired if should_cleanup?
143
+ keys.size
144
+ end
145
+
146
+ def ttl(key)
147
+ entry_data = @adapter.get(key)
148
+ return nil unless entry_data
149
+
150
+ begin
151
+ entry = deserialize_entry(entry_data)
152
+ return nil if entry.expired?
153
+ return nil unless entry.ttl
154
+
155
+ remaining = entry.ttl - (Time.now - entry.created_at)
156
+ remaining.positive? ? remaining : nil
157
+ rescue StandardError
158
+ nil
159
+ end
160
+ end
161
+
162
+ def expire(key)
163
+ delete(key)
164
+ end
165
+
166
+ def expire_all
167
+ clear
168
+ end
169
+
170
+ def cleanup_expired
171
+ expired_keys = []
172
+
173
+ @adapter.keys.each do |key|
174
+ entry_data = @adapter.get(key)
175
+ next unless entry_data
176
+
177
+ entry = deserialize_entry(entry_data)
178
+ expired_keys << key if entry.expired?
179
+ rescue StandardError
180
+ expired_keys << key
181
+ end
182
+
183
+ expired_keys.each { |key| delete(key) }
184
+ @last_cleanup = Time.now
185
+
186
+ expired_keys.size
187
+ end
188
+
189
+ def cache_info
190
+ total_keys = @adapter.keys.size
191
+ valid_keys = keys.size
192
+ expired_keys = total_keys - valid_keys
193
+
194
+ {
195
+ total_entries: total_keys,
196
+ valid_entries: valid_keys,
197
+ expired_entries: expired_keys,
198
+ max_size: @max_size,
199
+ default_ttl: @default_ttl,
200
+ last_cleanup: @last_cleanup
201
+ }
202
+ end
203
+
204
+ def touch(key, ttl: nil)
205
+ entry_data = @adapter.get(key)
206
+ return false unless entry_data
207
+
208
+ begin
209
+ entry = deserialize_entry(entry_data)
210
+ return false if entry.expired?
211
+
212
+ new_ttl = ttl || entry.ttl
213
+ new_entry = CacheEntry.new(entry.value, ttl: new_ttl, metadata: entry.metadata)
214
+
215
+ serialized_entry = serialize_entry(new_entry)
216
+ @adapter.set(key, serialized_entry)
217
+
218
+ @access_times[key] = Time.now
219
+ true
220
+ rescue StandardError
221
+ false
222
+ end
223
+ end
224
+
225
+ def fetch(key, ttl: :default, metadata: {}, &block)
226
+ value = get(key)
227
+ return value unless value.nil?
228
+
229
+ return nil unless block_given?
230
+
231
+ value = block.call
232
+ set(key, value, ttl: ttl, metadata: metadata)
233
+ value
234
+ end
235
+
236
+ def close
237
+ @adapter.close
238
+ end
239
+
240
+ private
241
+
242
+ def create_adapter(config)
243
+ adapter_type = config[:adapter]&.dig(:type) || config[:adapter_type] || :memory
244
+ adapter_options = config[:adapter]&.dig(:options) || config[:adapter_options] || {}
245
+
246
+ case adapter_type.to_sym
247
+ when :memory
248
+ Adapter::Memory.new(adapter_options)
249
+ when :filesystem
250
+ Adapter::FileSystem.new(adapter_options)
251
+ when :sqlite
252
+ Adapter::Sqlite.new(adapter_options)
253
+ else
254
+ raise ConfigurationError, "Unknown adapter type: #{adapter_type}"
255
+ end
256
+ end
257
+
258
+ def serialize_entry(entry)
259
+ JSON.generate(entry.to_h)
260
+ end
261
+
262
+ def deserialize_entry(data)
263
+ hash = JSON.parse(data, symbolize_names: true)
264
+ CacheEntry.from_h(hash)
265
+ end
266
+
267
+ def should_cleanup?
268
+ Time.now - @last_cleanup > @cleanup_interval
269
+ end
270
+
271
+ def evict_if_needed
272
+ return unless @max_size
273
+ return if size < @max_size
274
+
275
+ keys_by_access = @access_times.sort_by { |_, time| time }.map(&:first)
276
+ keys_to_evict = keys_by_access.first(size - @max_size + 1)
277
+
278
+ keys_to_evict.each { |key| delete(key) }
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Store
5
+ class CompositeModelHandler
6
+ Reference = Struct.new(:storage_key, :model_class, :key_value, keyword_init: true)
7
+
8
+ def initialize(registry, store, model_store = nil, serializer:)
9
+ @registry = registry
10
+ @store = store
11
+ @model_store = model_store
12
+ @serializer = serializer
13
+ end
14
+
15
+ def process_composite_models(model)
16
+ composite_models = @registry.find_composite_models(model)
17
+ stored_composites = {}
18
+
19
+ composite_models.each do |attr_path, composite_info|
20
+ nested_model = composite_info[:model]
21
+ registration = composite_info[:registration]
22
+
23
+ storage_key = registration.generate_storage_key(nested_model)
24
+ serialized_data = @serializer.serialize(nested_model)
25
+ @store.set(storage_key, serialized_data)
26
+
27
+ stored_composites[attr_path] = Reference.new(
28
+ storage_key: storage_key.to_s,
29
+ model_class: nested_model.class.name,
30
+ key_value: composite_info[:key_value]
31
+ )
32
+ end
33
+
34
+ stored_composites
35
+ end
36
+
37
+ def restore_composite_models(model, composite_references = nil)
38
+ return model unless composite_references
39
+
40
+ restored_model = model.dup
41
+
42
+ composite_references.each do |attr_path, reference_info|
43
+ ref = if reference_info.is_a?(Hash)
44
+ Reference.new(**reference_info.transform_keys(&:to_sym).slice(
45
+ :storage_key, :model_class, :key_value
46
+ ))
47
+ else
48
+ reference_info
49
+ end
50
+ nested_model = restore_nested_model(ref)
51
+ next unless nested_model
52
+
53
+ set_nested_attribute(restored_model, attr_path, nested_model)
54
+ end
55
+
56
+ restored_model
57
+ end
58
+
59
+ def update_composite_models(model, updates)
60
+ composite_updates = extract_composite_updates(updates)
61
+ return if composite_updates.empty?
62
+
63
+ composite_updates.each do |attr_path, update_value|
64
+ if attr_path.include?(".")
65
+ update_nested_composite(model, attr_path, update_value)
66
+ else
67
+ update_direct_composite(model, attr_path, update_value)
68
+ end
69
+ end
70
+ end
71
+
72
+ def delete_composite_models(model)
73
+ composite_models = @registry.find_composite_models(model)
74
+
75
+ composite_models.each_value do |composite_info|
76
+ registration = composite_info[:registration]
77
+ storage_key = registration.generate_storage_key(composite_info[:model])
78
+ @store.delete(storage_key)
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def restore_nested_model(reference)
85
+ storage_key = reference.storage_key
86
+ model_class_name = reference.model_class
87
+ key_value = reference.key_value
88
+
89
+ if @model_store && key_value
90
+ model_class = Object.const_get(model_class_name)
91
+ registration = @registry.find_registration(model_class)
92
+ if registration
93
+ key_field = registration.key_field
94
+ return @model_store.fetch(model: model_class, key_field => key_value)
95
+ end
96
+ end
97
+
98
+ serialized_data = @store.get(storage_key)
99
+ return nil unless serialized_data
100
+
101
+ model_class = Object.const_get(model_class_name)
102
+ @serializer.deserialize(serialized_data, model_class)
103
+ end
104
+
105
+ def set_nested_attribute(model, attr_path, value)
106
+ attr_path_str = attr_path.to_s
107
+ if attr_path_str.include?(".")
108
+ parts = attr_path_str.split(".")
109
+ current = navigate_to_parent(model, parts[0..-2])
110
+ set_final_attribute(current, parts.last, value)
111
+ else
112
+ model.public_send("#{attr_path}=", value)
113
+ end
114
+ end
115
+
116
+ def navigate_to_parent(model, path_parts)
117
+ path_parts.reduce(model) do |current, part|
118
+ if part.match?(/\A\d+\z/)
119
+ current[part.to_i]
120
+ else
121
+ current.public_send(part)
122
+ end
123
+ end
124
+ end
125
+
126
+ def set_final_attribute(current, attr, value)
127
+ if attr.match?(/\A\d+\z/)
128
+ current[attr.to_i] = value
129
+ else
130
+ current.public_send("#{attr}=", value)
131
+ end
132
+ end
133
+
134
+ def extract_composite_updates(updates)
135
+ updates.each_with_object({}) do |update, result|
136
+ key = update[:key].to_s
137
+ value = update[:value]
138
+ result[key] = value if key.include?(".") || registered_model?(value)
139
+ end
140
+ end
141
+
142
+ def update_nested_composite(model, attr_path, update_value)
143
+ root_attr = attr_path.split(".").first
144
+ composite_model = model.public_send(root_attr)
145
+ return unless composite_model && @registry.registered?(composite_model.class)
146
+
147
+ set_nested_attribute(composite_model, attr_path.split(".", 2).last, update_value)
148
+
149
+ registration = @registry.registration_for(composite_model.class)
150
+ storage_key = registration.generate_storage_key(composite_model)
151
+ @store.set(storage_key, @serializer.serialize(composite_model))
152
+ end
153
+
154
+ def update_direct_composite(model, attr_path, new_composite_model)
155
+ return unless registered_model?(new_composite_model)
156
+
157
+ registration = @registry.registration_for(new_composite_model.class)
158
+ storage_key = registration.generate_storage_key(new_composite_model)
159
+ @store.set(storage_key, @serializer.serialize(new_composite_model))
160
+
161
+ model.public_send("#{attr_path}=", new_composite_model)
162
+ end
163
+
164
+ def registered_model?(value)
165
+ value.is_a?(Object) && @registry.registered?(value.class)
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "stringio"
5
+
6
+ module Lutaml
7
+ module Store
8
+ class Compression
9
+ SUPPORTED_ALGORITHMS = %w[gzip deflate bzip2 lz4 zstd].freeze
10
+
11
+ def self.compress(data, algorithm = "gzip", level = 6)
12
+ case algorithm.to_s.downcase
13
+ when "gzip"
14
+ compress_gzip(data, level)
15
+ when "deflate"
16
+ compress_deflate(data, level)
17
+ when "bzip2"
18
+ compress_bzip2(data, level)
19
+ when "lz4"
20
+ compress_lz4(data)
21
+ when "zstd"
22
+ compress_zstd(data, level)
23
+ else
24
+ raise ArgumentError,
25
+ "Unsupported compression algorithm: #{algorithm}. Supported: #{SUPPORTED_ALGORITHMS.join(", ")}"
26
+ end
27
+ end
28
+
29
+ def self.decompress(data, algorithm = "gzip")
30
+ case algorithm.to_s.downcase
31
+ when "gzip"
32
+ decompress_gzip(data)
33
+ when "deflate"
34
+ decompress_deflate(data)
35
+ when "bzip2"
36
+ decompress_bzip2(data)
37
+ when "lz4"
38
+ decompress_lz4(data)
39
+ when "zstd"
40
+ decompress_zstd(data)
41
+ else
42
+ raise ArgumentError,
43
+ "Unsupported compression algorithm: #{algorithm}. Supported: #{SUPPORTED_ALGORITHMS.join(", ")}"
44
+ end
45
+ end
46
+
47
+ def self.detect_algorithm(data)
48
+ return nil unless data.is_a?(String)
49
+
50
+ # Force binary encoding for magic number detection
51
+ binary_data = data.dup.force_encoding("ASCII-8BIT")
52
+
53
+ # Define magic numbers as binary strings
54
+ gzip_magic = "\x1f\x8b".b
55
+ deflate_magic = "\x78".b
56
+ bzip2_magic = "BZ".b
57
+ lz4_magic = "\x04\"M\x18".b
58
+ zstd_magic = "\x28\xb5\x2f\xfd".b
59
+
60
+ # Check magic numbers
61
+ return "gzip" if binary_data.start_with?(gzip_magic)
62
+ return "deflate" if binary_data.start_with?(deflate_magic)
63
+ return "bzip2" if binary_data.start_with?(bzip2_magic)
64
+ return "lz4" if binary_data.start_with?(lz4_magic)
65
+ return "zstd" if binary_data.start_with?(zstd_magic)
66
+
67
+ nil # No compression detected
68
+ end
69
+
70
+ def self.compress_gzip(data, level)
71
+ io = StringIO.new
72
+ gz = Zlib::GzipWriter.new(io, level)
73
+ gz.write(data)
74
+ gz.close
75
+ io.string
76
+ end
77
+
78
+ def self.decompress_gzip(data)
79
+ io = StringIO.new(data)
80
+ gz = Zlib::GzipReader.new(io)
81
+ result = gz.read
82
+ gz.close
83
+ result
84
+ end
85
+
86
+ def self.compress_deflate(data, level)
87
+ Zlib::Deflate.deflate(data, level)
88
+ end
89
+
90
+ def self.decompress_deflate(data)
91
+ Zlib::Inflate.inflate(data)
92
+ end
93
+
94
+ def self.compress_bzip2(data, level)
95
+ require "bzip2-ffi"
96
+ Bzip2::FFI.compress(data, level)
97
+ rescue LoadError
98
+ raise ArgumentError, "bzip2-ffi gem is required for bzip2 compression"
99
+ end
100
+
101
+ def self.decompress_bzip2(data)
102
+ require "bzip2-ffi"
103
+ Bzip2::FFI.decompress(data)
104
+ rescue LoadError
105
+ raise ArgumentError, "bzip2-ffi gem is required for bzip2 compression"
106
+ end
107
+
108
+ def self.compress_lz4(data)
109
+ require "lz4-ruby"
110
+ LZ4.compress(data)
111
+ rescue LoadError
112
+ raise ArgumentError, "lz4-ruby gem is required for LZ4 compression"
113
+ end
114
+
115
+ def self.decompress_lz4(data)
116
+ require "lz4-ruby"
117
+ LZ4.decompress(data)
118
+ rescue LoadError
119
+ raise ArgumentError, "lz4-ruby gem is required for LZ4 compression"
120
+ end
121
+
122
+ def self.compress_zstd(data, level)
123
+ require "zstd-ruby"
124
+ Zstd.compress(data, level: level)
125
+ rescue LoadError
126
+ raise ArgumentError, "zstd-ruby gem is required for Zstd compression"
127
+ end
128
+
129
+ def self.decompress_zstd(data)
130
+ require "zstd-ruby"
131
+ Zstd.decompress(data)
132
+ rescue LoadError
133
+ raise ArgumentError, "zstd-ruby gem is required for Zstd compression"
134
+ end
135
+ end
136
+ end
137
+ end