lutaml-store 0.2.1 → 0.2.4
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 +4 -4
- data/.rubocop_todo.yml +22 -13
- data/CLAUDE.md +22 -9
- data/README.adoc +129 -15
- data/lib/lutaml/store/adapter/base.rb +28 -0
- data/lib/lutaml/store/adapter/memory.rb +213 -121
- data/lib/lutaml/store/adapter/sqlite.rb +181 -7
- data/lib/lutaml/store/adapter.rb +17 -0
- data/lib/lutaml/store/basic_store.rb +1 -10
- data/lib/lutaml/store/cache_store.rb +2 -10
- data/lib/lutaml/store/database_store.rb +321 -58
- data/lib/lutaml/store/format/base.rb +4 -0
- data/lib/lutaml/store/format/marshal_format.rb +4 -0
- data/lib/lutaml/store/format/xml.rb +37 -0
- data/lib/lutaml/store/format/yamls.rb +1 -1
- data/lib/lutaml/store/format.rb +3 -1
- data/lib/lutaml/store/format_serializer.rb +44 -0
- data/lib/lutaml/store/package_transport/base.rb +12 -0
- data/lib/lutaml/store/package_transport/directory_transport.rb +8 -8
- data/lib/lutaml/store/package_transport/zip_transport.rb +11 -6
- data/lib/lutaml/store/predicate.rb +248 -0
- data/lib/lutaml/store/query.rb +259 -0
- data/lib/lutaml/store/storage_key.rb +1 -1
- data/lib/lutaml/store/version.rb +1 -1
- data/lib/lutaml/store.rb +3 -0
- metadata +14 -4
data/lib/lutaml/store/adapter.rb
CHANGED
|
@@ -7,6 +7,23 @@ module Lutaml
|
|
|
7
7
|
autoload :Memory, "lutaml/store/adapter/memory"
|
|
8
8
|
autoload :FileSystem, "lutaml/store/adapter/filesystem"
|
|
9
9
|
autoload :Sqlite, "lutaml/store/adapter/sqlite"
|
|
10
|
+
|
|
11
|
+
@registry = {
|
|
12
|
+
memory: "Memory",
|
|
13
|
+
filesystem: "FileSystem",
|
|
14
|
+
sqlite: "Sqlite"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def self.resolve(type, options = {})
|
|
18
|
+
entry = @registry[type.to_sym]
|
|
19
|
+
raise ConfigurationError, "Unknown adapter type: #{type}" unless entry
|
|
20
|
+
|
|
21
|
+
const_get(entry).new(options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.register(type, adapter_class)
|
|
25
|
+
@registry[type.to_sym] = adapter_class.name
|
|
26
|
+
end
|
|
10
27
|
end
|
|
11
28
|
end
|
|
12
29
|
end
|
|
@@ -167,16 +167,7 @@ module Lutaml
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
def create_adapter
|
|
170
|
-
|
|
171
|
-
when :memory
|
|
172
|
-
Adapter::Memory.new(@config.adapter_options)
|
|
173
|
-
when :filesystem
|
|
174
|
-
Adapter::FileSystem.new(@config.adapter_options)
|
|
175
|
-
when :sqlite
|
|
176
|
-
Adapter::Sqlite.new(@config.adapter_options)
|
|
177
|
-
else
|
|
178
|
-
raise ConfigurationError, "Unknown adapter type: #{@config.adapter_type}"
|
|
179
|
-
end
|
|
170
|
+
Adapter.resolve(@config.adapter_type, @config.adapter_options)
|
|
180
171
|
end
|
|
181
172
|
|
|
182
173
|
def create_cache
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "time"
|
|
4
5
|
|
|
5
6
|
module Lutaml
|
|
6
7
|
module Store
|
|
@@ -246,16 +247,7 @@ module Lutaml
|
|
|
246
247
|
adapter_type = config[:adapter]&.dig(:type) || config[:adapter_type] || :memory
|
|
247
248
|
adapter_options = config[:adapter]&.dig(:options) || config[:adapter_options] || {}
|
|
248
249
|
|
|
249
|
-
|
|
250
|
-
when :memory
|
|
251
|
-
Adapter::Memory.new(adapter_options)
|
|
252
|
-
when :filesystem
|
|
253
|
-
Adapter::FileSystem.new(adapter_options)
|
|
254
|
-
when :sqlite
|
|
255
|
-
Adapter::Sqlite.new(adapter_options)
|
|
256
|
-
else
|
|
257
|
-
raise ConfigurationError, "Unknown adapter type: #{adapter_type}"
|
|
258
|
-
end
|
|
250
|
+
Adapter.resolve(adapter_type, adapter_options)
|
|
259
251
|
end
|
|
260
252
|
|
|
261
253
|
def serialize_entry(entry)
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Store
|
|
5
|
-
# Store-centric API with model registry and database-style operations
|
|
5
|
+
# Store-centric API with model registry and database-style operations.
|
|
6
|
+
# Provides CRUD, querying, file I/O, transactions, and events.
|
|
6
7
|
class DatabaseStore
|
|
7
|
-
attr_reader :store, :registry, :composite_handler, :attribute_updater
|
|
8
|
+
attr_reader :store, :registry, :composite_handler, :attribute_updater, :serializer
|
|
8
9
|
|
|
9
10
|
def initialize(adapter:, models: [], **options)
|
|
10
11
|
@store = BasicStore.new(adapter_type: adapter, **options)
|
|
@@ -12,11 +13,13 @@ module Lutaml
|
|
|
12
13
|
@serializer = ModelSerializer.new
|
|
13
14
|
@composite_handler = CompositeModelHandler.new(@registry, @store, self, serializer: @serializer)
|
|
14
15
|
@attribute_updater = AttributeUpdater.new(@registry, @composite_handler)
|
|
16
|
+
@scopes = {}
|
|
15
17
|
|
|
16
18
|
validate_configuration!
|
|
17
19
|
end
|
|
18
20
|
|
|
19
|
-
#
|
|
21
|
+
# ── CRUD ──
|
|
22
|
+
|
|
20
23
|
def save(models)
|
|
21
24
|
models_array = Array(models)
|
|
22
25
|
saved_models = models_array.map { |model| save_single_model(model) }
|
|
@@ -25,7 +28,6 @@ module Lutaml
|
|
|
25
28
|
models.is_a?(Array) ? saved_models : saved_models.first
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
# Fetch model by class and key field
|
|
29
31
|
def fetch(model:, **key_params)
|
|
30
32
|
registration = @registry.registration_for(model)
|
|
31
33
|
key_field = registration.key_field
|
|
@@ -35,20 +37,12 @@ module Lutaml
|
|
|
35
37
|
stored_data = find_stored_data(registration, model, key_value)
|
|
36
38
|
return nil unless stored_data
|
|
37
39
|
|
|
38
|
-
model_instance =
|
|
39
|
-
|
|
40
|
-
composite_references = stored_data["_composite_models"]
|
|
41
|
-
if composite_references
|
|
42
|
-
model_instance = @composite_handler.restore_composite_models(
|
|
43
|
-
model_instance, composite_references
|
|
44
|
-
)
|
|
45
|
-
end
|
|
40
|
+
model_instance = deserialize_record(stored_data, model, registration)
|
|
46
41
|
|
|
47
42
|
@store.emit_event(:model_fetch, model: model_instance, key: key_value, source: :backend)
|
|
48
43
|
model_instance
|
|
49
44
|
end
|
|
50
45
|
|
|
51
|
-
# Update model with attributes array or block
|
|
52
46
|
def update(model:, attributes: nil, **key_params, &block)
|
|
53
47
|
current_model = fetch(model: model, **key_params)
|
|
54
48
|
raise ModelNotRegisteredError, "Model not found" unless current_model
|
|
@@ -75,7 +69,6 @@ module Lutaml
|
|
|
75
69
|
updated_model
|
|
76
70
|
end
|
|
77
71
|
|
|
78
|
-
# Destroy model by class and key field
|
|
79
72
|
def destroy(model:, **key_params)
|
|
80
73
|
registration = @registry.registration_for(model)
|
|
81
74
|
key_field = registration.key_field
|
|
@@ -94,54 +87,96 @@ module Lutaml
|
|
|
94
87
|
deleted
|
|
95
88
|
end
|
|
96
89
|
|
|
97
|
-
# Query
|
|
90
|
+
# ── Query API ──
|
|
91
|
+
|
|
92
|
+
def query(model_class)
|
|
93
|
+
Query.new(self, model_class)
|
|
94
|
+
end
|
|
95
|
+
|
|
98
96
|
def where(model:, **conditions)
|
|
99
|
-
|
|
100
|
-
conditions.all? { |field, value| model_instance.public_send(field) == value }
|
|
101
|
-
end
|
|
97
|
+
query(model).where(**conditions)
|
|
102
98
|
end
|
|
103
99
|
|
|
104
|
-
# Get all models of a specific type
|
|
105
100
|
def all(model:)
|
|
106
|
-
|
|
101
|
+
query(model)
|
|
102
|
+
end
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
next unless parsed.class_name == model.name
|
|
104
|
+
def count(model:)
|
|
105
|
+
query(model).count
|
|
106
|
+
end
|
|
112
107
|
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
def exists?(model:, **key_params)
|
|
109
|
+
!fetch(model: model, **key_params).nil?
|
|
110
|
+
end
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
def find_by(model:, **conditions)
|
|
113
|
+
query(model).find_by(**conditions)
|
|
114
|
+
end
|
|
118
115
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
model_instance, composite_references
|
|
123
|
-
)
|
|
124
|
-
end
|
|
116
|
+
def find_by!(model:, **conditions)
|
|
117
|
+
query(model).find_by!(**conditions)
|
|
118
|
+
end
|
|
125
119
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
# ── Transactions ──
|
|
121
|
+
|
|
122
|
+
def transaction(&block)
|
|
123
|
+
adapter.transaction(&block)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# ── Scopes ──
|
|
127
|
+
|
|
128
|
+
def scope(name, body)
|
|
129
|
+
@scopes[name.to_sym] = body
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def scope_for(name)
|
|
133
|
+
@scopes[name.to_sym] || raise(ArgumentError, "Unknown scope: #{name}")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# ── Query execution (called by Query) ──
|
|
137
|
+
|
|
138
|
+
def execute_query(query)
|
|
139
|
+
registration = @registry.registration_for(query.model_class)
|
|
140
|
+
|
|
141
|
+
if custom_serializer?(registration)
|
|
142
|
+
execute_with_post_filter(query, registration)
|
|
143
|
+
else
|
|
144
|
+
execute_with_pre_filter(query, registration)
|
|
130
145
|
end
|
|
146
|
+
end
|
|
131
147
|
|
|
132
|
-
|
|
148
|
+
def count_query(query)
|
|
149
|
+
registration = @registry.registration_for(query.model_class)
|
|
150
|
+
|
|
151
|
+
if custom_serializer?(registration)
|
|
152
|
+
execute_with_post_filter(query, registration).size
|
|
153
|
+
else
|
|
154
|
+
count_with_pre_filter(query, registration)
|
|
155
|
+
end
|
|
133
156
|
end
|
|
134
157
|
|
|
135
|
-
def
|
|
136
|
-
|
|
158
|
+
def fetch_batch(query, after: nil, limit: 1000)
|
|
159
|
+
scan_query = Query.new(self, query.model_class)
|
|
160
|
+
raw_batch = adapter.batch_query(scan_query, after: after, limit: limit)
|
|
161
|
+
if raw_batch
|
|
162
|
+
models = deserialize_results(raw_batch, query.model_class)
|
|
163
|
+
query.predicates.empty? ? models : models.select { |m| query.predicates.all? { |p| p.match?(m) } }
|
|
164
|
+
else
|
|
165
|
+
fallback_batch(query, after: after, limit: limit)
|
|
166
|
+
end
|
|
137
167
|
end
|
|
138
168
|
|
|
139
|
-
def
|
|
140
|
-
|
|
169
|
+
def last_storage_key_from(models, model_class)
|
|
170
|
+
return nil if models.empty?
|
|
171
|
+
|
|
172
|
+
registration = @registry.registration_for(model_class)
|
|
173
|
+
last_model = models.last
|
|
174
|
+
key_value = registration.extract_key(last_model)
|
|
175
|
+
registration.generate_storage_key_from_value(key_value).to_s
|
|
141
176
|
end
|
|
142
177
|
|
|
143
|
-
#
|
|
144
|
-
|
|
178
|
+
# ── File I/O ──
|
|
179
|
+
|
|
145
180
|
def load_all(model_class, path: nil, format: :yaml, layout: :separate)
|
|
146
181
|
fmt = Format.resolve(format)
|
|
147
182
|
dir = resolve_model_dir(model_class, path)
|
|
@@ -161,8 +196,6 @@ module Lutaml
|
|
|
161
196
|
end
|
|
162
197
|
end
|
|
163
198
|
|
|
164
|
-
# Load from directory and store into the key-value backend.
|
|
165
|
-
# Returns the loaded models and makes them available via fetch/where/all.
|
|
166
199
|
def import_all(model_class, path: nil, format: :yaml, layout: :separate)
|
|
167
200
|
models = load_all(model_class, path: path, format: format, layout: layout)
|
|
168
201
|
models.each { |model| save(model) }
|
|
@@ -170,7 +203,6 @@ module Lutaml
|
|
|
170
203
|
models
|
|
171
204
|
end
|
|
172
205
|
|
|
173
|
-
# Save all models to a directory using format-specific serialization.
|
|
174
206
|
def save_all(models, path: nil, format: :yaml, layout: :separate)
|
|
175
207
|
fmt = Format.resolve(format)
|
|
176
208
|
models_array = Array(models)
|
|
@@ -198,7 +230,6 @@ module Lutaml
|
|
|
198
230
|
saved
|
|
199
231
|
end
|
|
200
232
|
|
|
201
|
-
# Export models to a single file or directory.
|
|
202
233
|
def export(models, path:, format: :yaml)
|
|
203
234
|
fmt = Format.resolve(format)
|
|
204
235
|
models_array = Array(models)
|
|
@@ -207,11 +238,13 @@ module Lutaml
|
|
|
207
238
|
|
|
208
239
|
content = fmt.serialize_many(models_array)
|
|
209
240
|
|
|
210
|
-
|
|
241
|
+
write_file(path, content, fmt)
|
|
211
242
|
@store.emit_event(:model_export, count: models_array.size, path: path)
|
|
212
243
|
path
|
|
213
244
|
end
|
|
214
245
|
|
|
246
|
+
# ── Events ──
|
|
247
|
+
|
|
215
248
|
def on(event, &block)
|
|
216
249
|
@store.on(event, &block)
|
|
217
250
|
end
|
|
@@ -235,6 +268,226 @@ module Lutaml
|
|
|
235
268
|
|
|
236
269
|
private
|
|
237
270
|
|
|
271
|
+
def adapter
|
|
272
|
+
@store.adapter
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def custom_serializer?(registration)
|
|
276
|
+
registration&.serializer
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Pre-deserialization filtering: predicates evaluated on raw hash data.
|
|
280
|
+
# Used when the serializer stores model fields as direct hash keys.
|
|
281
|
+
def execute_with_pre_filter(query, registration)
|
|
282
|
+
adapter_results = adapter.execute_query(query)
|
|
283
|
+
|
|
284
|
+
if adapter_results
|
|
285
|
+
deserialize_results(adapter_results, query.model_class)
|
|
286
|
+
else
|
|
287
|
+
fallback_execute(query, registration)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def count_with_pre_filter(query, registration)
|
|
292
|
+
adapter_count = adapter.count_query(query)
|
|
293
|
+
return adapter_count unless adapter_count.nil?
|
|
294
|
+
|
|
295
|
+
adapter_results = adapter.execute_query(query)
|
|
296
|
+
if adapter_results
|
|
297
|
+
adapter_results.size
|
|
298
|
+
else
|
|
299
|
+
fallback_count(query, registration)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Post-deserialization filtering: scan by class name only, deserialize
|
|
304
|
+
# everything, then filter predicates on model instances.
|
|
305
|
+
# Used when custom serializers store data in non-standard hash format.
|
|
306
|
+
def execute_with_post_filter(query, _registration)
|
|
307
|
+
scan_query = Query.new(self, query.model_class)
|
|
308
|
+
adapter_results = adapter.execute_query(scan_query)
|
|
309
|
+
|
|
310
|
+
raw_results = adapter_results || scan_all_by_class(query.model_class)
|
|
311
|
+
|
|
312
|
+
models = deserialize_results(raw_results, query.model_class)
|
|
313
|
+
results = apply_model_predicates(models, query.predicates)
|
|
314
|
+
results = apply_model_sort(results, query.orders)
|
|
315
|
+
apply_model_pagination(results, query.limit_value, query.offset_value)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def scan_all_by_class(model_class)
|
|
319
|
+
results = []
|
|
320
|
+
@store.each_key do |storage_key|
|
|
321
|
+
parsed = StorageKey.parse(storage_key.to_s)
|
|
322
|
+
next unless parsed.class_name == model_class.name
|
|
323
|
+
|
|
324
|
+
stored_data = @store.get(storage_key)
|
|
325
|
+
results << [storage_key, stored_data] if stored_data
|
|
326
|
+
rescue StandardError
|
|
327
|
+
next
|
|
328
|
+
end
|
|
329
|
+
results
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def apply_model_predicates(models, predicates)
|
|
333
|
+
return models if predicates.empty?
|
|
334
|
+
|
|
335
|
+
models.select { |m| predicates.all? { |p| p.match?(m) } }
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def apply_model_sort(models, orders)
|
|
339
|
+
return models if orders.empty?
|
|
340
|
+
|
|
341
|
+
models.sort do |a, b|
|
|
342
|
+
orders.reduce(0) do |cmp, o|
|
|
343
|
+
break cmp unless cmp.zero?
|
|
344
|
+
|
|
345
|
+
va = a.public_send(o.field)
|
|
346
|
+
vb = b.public_send(o.field)
|
|
347
|
+
|
|
348
|
+
cmp_val = if va.nil? && vb.nil?
|
|
349
|
+
0
|
|
350
|
+
elsif va.nil?
|
|
351
|
+
1
|
|
352
|
+
elsif vb.nil?
|
|
353
|
+
-1
|
|
354
|
+
else
|
|
355
|
+
va <=> vb || 0
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
o.direction == :desc ? -cmp_val : cmp_val
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def apply_model_pagination(models, limit, offset)
|
|
364
|
+
start = offset || 0
|
|
365
|
+
models = models[start..] || []
|
|
366
|
+
models = models.first(limit) if limit
|
|
367
|
+
models
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def deserialize_record(hash_data, model_class, registration)
|
|
371
|
+
model_instance = @serializer.deserialize(hash_data, model_class, registration)
|
|
372
|
+
|
|
373
|
+
composite_references = hash_data["_composite_models"]
|
|
374
|
+
if composite_references
|
|
375
|
+
model_instance = @composite_handler.restore_composite_models(
|
|
376
|
+
model_instance, composite_references
|
|
377
|
+
)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
model_instance
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def deserialize_results(raw_results, model_class)
|
|
384
|
+
registration = @registry.registration_for(model_class)
|
|
385
|
+
|
|
386
|
+
raw_results.filter_map do |(storage_key, hash_data)|
|
|
387
|
+
deserialize_record(hash_data, model_class, registration)
|
|
388
|
+
rescue StandardError => e
|
|
389
|
+
@store.emit_event(:deserialization_error, key: storage_key, error: e)
|
|
390
|
+
nil
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# ── Fallback query execution (no adapter query support) ──
|
|
395
|
+
|
|
396
|
+
def fallback_execute(query, _registration)
|
|
397
|
+
results = []
|
|
398
|
+
|
|
399
|
+
@store.each_key do |storage_key|
|
|
400
|
+
parsed = StorageKey.parse(storage_key.to_s)
|
|
401
|
+
next unless parsed.class_name == query.model_class.name
|
|
402
|
+
|
|
403
|
+
stored_data = @store.get(storage_key)
|
|
404
|
+
next unless stored_data
|
|
405
|
+
next unless query.predicates.all? { |p| p.match?(stored_data) }
|
|
406
|
+
|
|
407
|
+
results << [storage_key, stored_data]
|
|
408
|
+
rescue StandardError
|
|
409
|
+
next
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
results = apply_sort(results, query.orders)
|
|
413
|
+
results = apply_pagination(results, query.limit_value, query.offset_value)
|
|
414
|
+
deserialize_results(results, query.model_class)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def fallback_count(query, _registration)
|
|
418
|
+
count = 0
|
|
419
|
+
|
|
420
|
+
@store.each_key do |storage_key|
|
|
421
|
+
parsed = StorageKey.parse(storage_key.to_s)
|
|
422
|
+
next unless parsed.class_name == query.model_class.name
|
|
423
|
+
|
|
424
|
+
stored_data = @store.get(storage_key)
|
|
425
|
+
next unless stored_data
|
|
426
|
+
next unless query.predicates.all? { |p| p.match?(stored_data) }
|
|
427
|
+
|
|
428
|
+
count += 1
|
|
429
|
+
rescue StandardError
|
|
430
|
+
next
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
count
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def fallback_batch(query, after: nil, limit: 1000)
|
|
437
|
+
results = []
|
|
438
|
+
|
|
439
|
+
@store.each_key do |storage_key|
|
|
440
|
+
parsed = StorageKey.parse(storage_key.to_s)
|
|
441
|
+
next unless parsed.class_name == query.model_class.name
|
|
442
|
+
next if after && storage_key.to_s <= after
|
|
443
|
+
|
|
444
|
+
stored_data = @store.get(storage_key)
|
|
445
|
+
next unless stored_data
|
|
446
|
+
next unless query.predicates.all? { |p| p.match?(stored_data) }
|
|
447
|
+
|
|
448
|
+
results << [storage_key, stored_data]
|
|
449
|
+
break if results.size >= limit
|
|
450
|
+
rescue StandardError
|
|
451
|
+
next
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
deserialize_results(results, query.model_class)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def apply_sort(results, orders)
|
|
458
|
+
return results if orders.empty?
|
|
459
|
+
|
|
460
|
+
results.sort do |a, b|
|
|
461
|
+
orders.reduce(0) do |cmp, o|
|
|
462
|
+
break cmp unless cmp.zero?
|
|
463
|
+
|
|
464
|
+
va = a.last.is_a?(Hash) ? a.last[o.field.to_s] : nil
|
|
465
|
+
vb = b.last.is_a?(Hash) ? b.last[o.field.to_s] : nil
|
|
466
|
+
|
|
467
|
+
cmp_val = if va.nil? && vb.nil?
|
|
468
|
+
0
|
|
469
|
+
elsif va.nil?
|
|
470
|
+
1
|
|
471
|
+
elsif vb.nil?
|
|
472
|
+
-1
|
|
473
|
+
else
|
|
474
|
+
va <=> vb || 0
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
o.direction == :desc ? -cmp_val : cmp_val
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def apply_pagination(results, limit, offset)
|
|
483
|
+
start = offset || 0
|
|
484
|
+
results = results[start..] || []
|
|
485
|
+
results = results.first(limit) if limit
|
|
486
|
+
results
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# ── CRUD internals ──
|
|
490
|
+
|
|
238
491
|
def find_stored_data(registration, model, key_value)
|
|
239
492
|
if registration.polymorphic?
|
|
240
493
|
find_polymorphic_data(model, key_value)
|
|
@@ -331,8 +584,8 @@ module Lutaml
|
|
|
331
584
|
Dir.glob(glob).sort.each do |file_path|
|
|
332
585
|
next unless File.file?(file_path)
|
|
333
586
|
|
|
334
|
-
raw =
|
|
335
|
-
next if raw.strip.empty?
|
|
587
|
+
raw = read_file(file_path, fmt)
|
|
588
|
+
next if !fmt.binary? && raw.strip.empty?
|
|
336
589
|
|
|
337
590
|
begin
|
|
338
591
|
model = fmt.deserialize(raw, model_class)
|
|
@@ -351,8 +604,8 @@ module Lutaml
|
|
|
351
604
|
Dir.glob(glob).sort.each do |file_path|
|
|
352
605
|
next unless File.file?(file_path)
|
|
353
606
|
|
|
354
|
-
raw =
|
|
355
|
-
next if raw.strip.empty?
|
|
607
|
+
raw = read_file(file_path, fmt)
|
|
608
|
+
next if !fmt.binary? && raw.strip.empty?
|
|
356
609
|
|
|
357
610
|
begin
|
|
358
611
|
loaded = fmt.deserialize_many(raw, model_class)
|
|
@@ -375,8 +628,7 @@ module Lutaml
|
|
|
375
628
|
key = extract_model_key(model)
|
|
376
629
|
filename = key || model.class.name.to_s.gsub("::", "_")
|
|
377
630
|
file_path = File.join(dir, "#{filename}#{fmt.extension}")
|
|
378
|
-
|
|
379
|
-
File.write(file_path, content, encoding: "utf-8")
|
|
631
|
+
write_file(file_path, fmt.serialize(model), fmt)
|
|
380
632
|
model
|
|
381
633
|
end
|
|
382
634
|
end
|
|
@@ -391,8 +643,7 @@ module Lutaml
|
|
|
391
643
|
|
|
392
644
|
grouped.map do |key, group|
|
|
393
645
|
file_path = File.join(dir, "#{key}#{fmt.extension}")
|
|
394
|
-
|
|
395
|
-
File.write(file_path, content, encoding: "utf-8")
|
|
646
|
+
write_file(file_path, fmt.serialize_many(group), fmt)
|
|
396
647
|
group
|
|
397
648
|
end.flatten
|
|
398
649
|
end
|
|
@@ -420,6 +671,18 @@ module Lutaml
|
|
|
420
671
|
basename = File.basename(file_path, ".*")
|
|
421
672
|
model.public_send(:"#{registration.key_field}=", basename)
|
|
422
673
|
end
|
|
674
|
+
|
|
675
|
+
def read_file(file_path, fmt)
|
|
676
|
+
fmt.binary? ? File.binread(file_path) : File.read(file_path, encoding: "utf-8")
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def write_file(file_path, content, fmt)
|
|
680
|
+
if fmt.binary?
|
|
681
|
+
File.binwrite(file_path, content)
|
|
682
|
+
else
|
|
683
|
+
File.write(file_path, content, encoding: "utf-8")
|
|
684
|
+
end
|
|
685
|
+
end
|
|
423
686
|
end
|
|
424
687
|
end
|
|
425
688
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Store
|
|
5
|
+
module Format
|
|
6
|
+
class Xml < Base
|
|
7
|
+
def extension
|
|
8
|
+
".xml"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def glob_pattern
|
|
12
|
+
"*.xml"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def serialize(model)
|
|
16
|
+
model.to_xml
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def deserialize(data, model_class)
|
|
20
|
+
model_class.from_xml(data)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def serialize_many(models)
|
|
24
|
+
inner = models.map(&:to_xml).join("\n")
|
|
25
|
+
"<items>\n#{inner}\n</items>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def deserialize_many(data, model_class)
|
|
29
|
+
doc = Moxml.parse(data)
|
|
30
|
+
doc.root.children.select(&:element?).map do |child|
|
|
31
|
+
model_class.from_xml(child.to_xml)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/lutaml/store/format.rb
CHANGED
|
@@ -13,13 +13,15 @@ module Lutaml
|
|
|
13
13
|
autoload :Json, "lutaml/store/format/json"
|
|
14
14
|
autoload :Jsonl, "lutaml/store/format/jsonl"
|
|
15
15
|
autoload :MarshalFormat, "lutaml/store/format/marshal_format"
|
|
16
|
+
autoload :Xml, "lutaml/store/format/xml"
|
|
16
17
|
|
|
17
18
|
FORMATS = {
|
|
18
19
|
yaml: "Yaml",
|
|
19
20
|
yamls: "Yamls",
|
|
20
21
|
json: "Json",
|
|
21
22
|
jsonl: "Jsonl",
|
|
22
|
-
marshal: "MarshalFormat"
|
|
23
|
+
marshal: "MarshalFormat",
|
|
24
|
+
xml: "Xml"
|
|
23
25
|
}.freeze
|
|
24
26
|
|
|
25
27
|
def self.resolve(format)
|