lutaml-store 0.2.1 → 0.2.2
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 +7 -7
- data/CLAUDE.md +22 -9
- data/README.adoc +129 -15
- 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 +19 -9
- 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/storage_key.rb +1 -1
- data/lib/lutaml/store/version.rb +1 -1
- data/lib/lutaml/store.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4917f68f56456a19670424157dd5001d243b9bd6d97618f445d306a7998e2a9f
|
|
4
|
+
data.tar.gz: 7076d765cf18cc91bfa9cf252bb0117ea8870c915bf1e7421648d9490de7e6a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 897f40bf4972373587528a80afcc44ab2b0c159f2bba63a97f39cb997eff97d3633d7e34cabeb0dac37e1b337781f0aef60b9ffaae2b3a3426d5624b35fb7950
|
|
7
|
+
data.tar.gz: 1b79b56672ace97f0b6e57715b1b56b7f4638b1d426db57b8f57c5ed21eb461a7fd2ff4181f57afaa094d85c9b7da37f9e3d4771c04afecfcef0c268e86ddcd7
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-06-05
|
|
3
|
+
# on 2026-06-05 09:38:49 UTC using RuboCop version 1.87.0.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -16,7 +16,7 @@ Gemspec/RequiredRubyVersion:
|
|
|
16
16
|
Metrics/AbcSize:
|
|
17
17
|
Max: 112
|
|
18
18
|
|
|
19
|
-
# Offense count:
|
|
19
|
+
# Offense count: 57
|
|
20
20
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
21
21
|
# AllowedMethods: refine
|
|
22
22
|
Metrics/BlockLength:
|
|
@@ -30,9 +30,9 @@ Metrics/ClassLength:
|
|
|
30
30
|
# Offense count: 9
|
|
31
31
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
32
32
|
Metrics/CyclomaticComplexity:
|
|
33
|
-
Max:
|
|
33
|
+
Max: 12
|
|
34
34
|
|
|
35
|
-
# Offense count:
|
|
35
|
+
# Offense count: 79
|
|
36
36
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
37
37
|
Metrics/MethodLength:
|
|
38
38
|
Max: 84
|
|
@@ -42,10 +42,10 @@ Metrics/MethodLength:
|
|
|
42
42
|
Metrics/ParameterLists:
|
|
43
43
|
Max: 8
|
|
44
44
|
|
|
45
|
-
# Offense count:
|
|
45
|
+
# Offense count: 4
|
|
46
46
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
47
47
|
Metrics/PerceivedComplexity:
|
|
48
|
-
Max:
|
|
48
|
+
Max: 12
|
|
49
49
|
|
|
50
50
|
# Offense count: 1
|
|
51
51
|
Naming/AccessorMethodName:
|
|
@@ -84,7 +84,7 @@ Security/MarshalLoad:
|
|
|
84
84
|
Exclude:
|
|
85
85
|
- 'lib/lutaml/store/format/marshal_format.rb'
|
|
86
86
|
|
|
87
|
-
# Offense count:
|
|
87
|
+
# Offense count: 36
|
|
88
88
|
# Configuration parameters: AllowedConstants.
|
|
89
89
|
Style/Documentation:
|
|
90
90
|
Enabled: false
|
data/CLAUDE.md
CHANGED
|
@@ -28,30 +28,43 @@ This is a Ruby gem (`lutaml-store`) providing a store-centric database-style API
|
|
|
28
28
|
|
|
29
29
|
| Class | Role |
|
|
30
30
|
|---|---|
|
|
31
|
-
| `DatabaseStore` | High-level CRUD with model registry, composite models, polymorphism |
|
|
31
|
+
| `DatabaseStore` | High-level CRUD with model registry, composite models, polymorphism, file I/O |
|
|
32
32
|
| `BasicStore` | Low-level key-value store with optional cache/monitor/events |
|
|
33
33
|
| `CacheStore` | TTL-aware cache store extending `BasicStore` |
|
|
34
|
+
| `PackageStore` | Structured multi-model packages with directory/ZIP transport |
|
|
35
|
+
| `PackageDefinition` | Declarative schema for package structure (models, assets, metadata) |
|
|
34
36
|
| `ModelRegistry` / `ModelRegistration` | Register models with their key fields and polymorphic config |
|
|
35
37
|
| `CompositeModelHandler` | Stores nested registered models independently, restores references |
|
|
36
38
|
| `AttributeUpdater` | Processes updates including dot-notation paths and block-based updates |
|
|
37
|
-
| `ModelSerializer` |
|
|
38
|
-
| `
|
|
39
|
+
| `ModelSerializer` | Hash-based serialization/deserialization for key-value storage |
|
|
40
|
+
| `FormatSerializer` | Bridges any Format handler to ModelSerializer interface for DatabaseStore |
|
|
41
|
+
| `Format` | Multi-format file I/O (YAML, YAMLS, JSON, JSONL, Marshal, XML) |
|
|
42
|
+
| `Adapter` | Storage adapter registry and factory (Memory, FileSystem, SQLite) |
|
|
43
|
+
|
|
44
|
+
### Format handlers (`lib/lutaml/store/format/`)
|
|
45
|
+
|
|
46
|
+
All inherit from `Format::Base`. Six formats: `Yaml`, `Yamls`, `Json`, `Jsonl`, `MarshalFormat`, `Xml`. Each implements `serialize`/`deserialize`, optional `serialize_many`/`deserialize_many`, `extension`, `glob_pattern`, and `binary?`. Registered in `Format::FORMATS` hash and resolved via `Format.resolve(:symbol)`.
|
|
39
47
|
|
|
40
48
|
### Storage adapters (`lib/lutaml/store/adapter/`)
|
|
41
49
|
|
|
42
|
-
All inherit from `Adapter::Base`. Three backends: `Memory`, `FileSystem`, `SQLite`.
|
|
50
|
+
All inherit from `Adapter::Base`. Three backends: `Memory`, `FileSystem`, `SQLite`. Registered in `Adapter` module and resolved via `Adapter.resolve(:type, options)`. New adapters can be added with `Adapter.register(:custom, CustomClass)` without modifying existing code (OCP).
|
|
43
51
|
|
|
44
|
-
###
|
|
52
|
+
### PackageStore and transports
|
|
45
53
|
|
|
46
|
-
`
|
|
54
|
+
`PackageStore` provides structured multi-model persistence. `PackageDefinition` declares which models, assets, and metadata the package contains. Transports (`DirectoryTransport`, `ZipTransport`) handle reading/writing to disk. Format handlers determine serialization per model entry.
|
|
47
55
|
|
|
48
|
-
###
|
|
56
|
+
### FormatSerializer pattern
|
|
57
|
+
|
|
58
|
+
`FormatSerializer` wraps a Format handler to implement the serializer interface (serialize/deserialize). This enables `DatabaseStore` to use any format (YAMLS, XML, Marshal, etc.) for key-value storage instead of the default hash serialization. Used for Glossarist-like YAMLS patterns.
|
|
59
|
+
|
|
60
|
+
### HTTP caching
|
|
49
61
|
|
|
50
|
-
`
|
|
62
|
+
`HttpCache` provides HTTP-aware caching with ETags, conditional requests (304), Cache-Control, and Vary header support. Uses `to_json`/`from_json` for model-driven serialization.
|
|
51
63
|
|
|
52
64
|
## Conventions
|
|
53
65
|
|
|
54
66
|
- Double-quoted strings (Rubocop enforced)
|
|
55
67
|
- Specs use `expect` syntax (no `should`)
|
|
56
|
-
- Documentation is in AsciiDoc (README.adoc
|
|
68
|
+
- Documentation is in AsciiDoc (README.adoc)
|
|
69
|
+
- All library code uses Ruby `autoload` (no `require_relative` or internal `require`)
|
|
57
70
|
- Error hierarchy: `Lutaml::Store::Error` → `ConfigurationError`, `BackendError`, `ModelNotRegisteredError`, `InvalidKeyError`, `PolymorphicUpdateError`, `CompositeModelError`
|
data/README.adoc
CHANGED
|
@@ -309,40 +309,154 @@ store.destroy(model: User, user_id: "u1")
|
|
|
309
309
|
== PackageStore
|
|
310
310
|
|
|
311
311
|
`PackageStore` provides structured multi-model persistence with directory and ZIP
|
|
312
|
-
transports.
|
|
313
|
-
|
|
312
|
+
transports. Use it when you need to bundle multiple model types, binary assets,
|
|
313
|
+
and metadata into a single loadable package.
|
|
314
|
+
|
|
315
|
+
The workflow is: define the package schema with `PackageDefinition`, then load,
|
|
316
|
+
query, modify, and save using `PackageStore`.
|
|
314
317
|
|
|
315
318
|
=== Define a package
|
|
316
319
|
|
|
320
|
+
`PackageDefinition` declares the package structure — which models, assets, and
|
|
321
|
+
metadata the package contains:
|
|
322
|
+
|
|
317
323
|
[source,ruby]
|
|
318
324
|
----
|
|
319
|
-
|
|
325
|
+
require "lutaml/model"
|
|
326
|
+
require "lutaml/store"
|
|
327
|
+
|
|
328
|
+
# Define your models
|
|
329
|
+
class Concept < Lutaml::Model::Serializable
|
|
330
|
+
attribute :term, :string
|
|
331
|
+
attribute :definition, :string
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
class Author < Lutaml::Model::Serializable
|
|
335
|
+
attribute :name, :string
|
|
336
|
+
attribute :email, :string
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
class GlossaryInfo < Lutaml::Model::Serializable
|
|
340
|
+
attribute :title, :string
|
|
341
|
+
attribute :version, :string
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Declare the package schema
|
|
345
|
+
glossary = Lutaml::Store::PackageDefinition.new(
|
|
346
|
+
name: "glossary",
|
|
347
|
+
metadata_model: GlossaryInfo,
|
|
348
|
+
metadata_file: "glossary.yaml",
|
|
349
|
+
metadata_key: :title
|
|
350
|
+
) do |pkg|
|
|
351
|
+
# Models stored in subdirectories, one file per instance
|
|
320
352
|
pkg.model(model: Concept, key: :term, dir: "concepts", default_format: :yaml)
|
|
321
353
|
pkg.model(model: Author, key: :name, dir: "authors", default_format: :json)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
pkg.
|
|
354
|
+
|
|
355
|
+
# Binary assets bundled in the package
|
|
356
|
+
pkg.asset("logo.png", type: :file)
|
|
357
|
+
pkg.asset("attachments", type: :directory)
|
|
325
358
|
end
|
|
326
359
|
----
|
|
327
360
|
|
|
328
|
-
|
|
361
|
+
Each `pkg.model` call registers a model type with:
|
|
362
|
+
|
|
363
|
+
* `model:` — the `Lutaml::Model::Serializable` class
|
|
364
|
+
* `key:` — attribute used as the unique identifier (becomes the filename)
|
|
365
|
+
* `dir:` — subdirectory for this model type (use `file:` instead for a single-file model)
|
|
366
|
+
* `layout:` — `:separate` (one file per instance, default) or `:grouped` (multiple instances per file)
|
|
367
|
+
* `default_format:` — `:yaml`, `:json`, `:jsonl`, `:yamls`, or `:marshal`
|
|
368
|
+
|
|
369
|
+
=== Directory structure
|
|
370
|
+
|
|
371
|
+
When saved to disk, the glossary package produces:
|
|
372
|
+
|
|
373
|
+
----
|
|
374
|
+
my_glossary/
|
|
375
|
+
├── glossary.yaml # metadata (GlossaryInfo)
|
|
376
|
+
├── concepts/ # Concept model instances
|
|
377
|
+
│ ├── API.yaml
|
|
378
|
+
│ ├── REST.yaml
|
|
379
|
+
│ └── YAML.yaml
|
|
380
|
+
├── authors/ # Author model instances
|
|
381
|
+
│ ├── john_doe.json
|
|
382
|
+
│ └── jane_smith.json
|
|
383
|
+
├── logo.png # asset file
|
|
384
|
+
└── attachments/ # asset directory
|
|
385
|
+
└── spec.pdf
|
|
386
|
+
----
|
|
387
|
+
|
|
388
|
+
=== Load and query
|
|
329
389
|
|
|
330
390
|
[source,ruby]
|
|
331
391
|
----
|
|
332
392
|
# Load from directory
|
|
333
|
-
store = Lutaml::Store::PackageStore.load(glossary, "./my_glossary"
|
|
393
|
+
store = Lutaml::Store::PackageStore.load(glossary, "./my_glossary")
|
|
334
394
|
|
|
335
|
-
# Load from ZIP
|
|
395
|
+
# Load from ZIP file
|
|
336
396
|
store = Lutaml::Store::PackageStore.load(glossary, "./glossary.zip", transport: :zip)
|
|
337
397
|
|
|
398
|
+
# Access metadata
|
|
399
|
+
store.metadata # => GlossaryInfo instance
|
|
400
|
+
|
|
338
401
|
# Query models
|
|
339
|
-
concepts = store.models_for(Concept)
|
|
340
|
-
store.
|
|
341
|
-
store.
|
|
402
|
+
concepts = store.models_for(Concept) # => array of Concept instances
|
|
403
|
+
concept = store.fetch_model(Concept, "API")
|
|
404
|
+
store.model_count(Concept) # => 3
|
|
405
|
+
store.model_exists?(Concept, "REST") # => true
|
|
406
|
+
|
|
407
|
+
# Access assets
|
|
408
|
+
store.asset("logo.png") # => binary content
|
|
409
|
+
store.asset_paths # => ["logo.png", "attachments/spec.pdf"]
|
|
410
|
+
|
|
411
|
+
# Package statistics
|
|
412
|
+
store.stats
|
|
413
|
+
# => { package: "glossary", models: { "Concept" => 3, "Author" => 2 },
|
|
414
|
+
# assets: 2, metadata: true }
|
|
415
|
+
----
|
|
416
|
+
|
|
417
|
+
=== Modify and save
|
|
418
|
+
|
|
419
|
+
[source,ruby]
|
|
420
|
+
----
|
|
421
|
+
# Add models
|
|
422
|
+
store.add_model(Concept.new(term: "JSON", definition: "..."))
|
|
423
|
+
store.add_models([concept1, concept2])
|
|
424
|
+
|
|
425
|
+
# Remove models
|
|
426
|
+
store.remove_model(Concept, "deprecated_term")
|
|
427
|
+
|
|
428
|
+
# Add/remove assets
|
|
429
|
+
store.add_asset("diagram.svg", svg_content)
|
|
430
|
+
store.remove_asset("old_diagram.svg")
|
|
431
|
+
|
|
432
|
+
# Save to directory (default format per model)
|
|
433
|
+
store.save("./output")
|
|
434
|
+
|
|
435
|
+
# Save to ZIP with per-model format overrides
|
|
436
|
+
store.save("./glossary.zip", transport: :zip, formats: { Concept => :json })
|
|
437
|
+
|
|
438
|
+
# Save with global format override
|
|
439
|
+
store.save("./output", format: :yaml)
|
|
440
|
+
|
|
441
|
+
# Bulk operations
|
|
442
|
+
store.clear_models(Concept) # remove all Concept instances
|
|
443
|
+
store.clear_all # remove everything
|
|
444
|
+
----
|
|
445
|
+
|
|
446
|
+
=== Per-model format override
|
|
447
|
+
|
|
448
|
+
The `save` method accepts format overrides:
|
|
449
|
+
|
|
450
|
+
[source,ruby]
|
|
451
|
+
----
|
|
452
|
+
# Global format for all models
|
|
453
|
+
store.save("./out", format: :json)
|
|
454
|
+
|
|
455
|
+
# Per-model format
|
|
456
|
+
store.save("./out", formats: { Concept => :yaml, Author => :jsonl })
|
|
342
457
|
|
|
343
|
-
#
|
|
344
|
-
store.
|
|
345
|
-
store.save("./output", transport: :zip)
|
|
458
|
+
# Default: each model uses its default_format from the definition
|
|
459
|
+
store.save("./out")
|
|
346
460
|
----
|
|
347
461
|
|
|
348
462
|
=== Package transports
|
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)
|
|
@@ -207,7 +207,7 @@ module Lutaml
|
|
|
207
207
|
|
|
208
208
|
content = fmt.serialize_many(models_array)
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
write_file(path, content, fmt)
|
|
211
211
|
@store.emit_event(:model_export, count: models_array.size, path: path)
|
|
212
212
|
path
|
|
213
213
|
end
|
|
@@ -331,8 +331,8 @@ module Lutaml
|
|
|
331
331
|
Dir.glob(glob).sort.each do |file_path|
|
|
332
332
|
next unless File.file?(file_path)
|
|
333
333
|
|
|
334
|
-
raw =
|
|
335
|
-
next if raw.strip.empty?
|
|
334
|
+
raw = read_file(file_path, fmt)
|
|
335
|
+
next if !fmt.binary? && raw.strip.empty?
|
|
336
336
|
|
|
337
337
|
begin
|
|
338
338
|
model = fmt.deserialize(raw, model_class)
|
|
@@ -351,8 +351,8 @@ module Lutaml
|
|
|
351
351
|
Dir.glob(glob).sort.each do |file_path|
|
|
352
352
|
next unless File.file?(file_path)
|
|
353
353
|
|
|
354
|
-
raw =
|
|
355
|
-
next if raw.strip.empty?
|
|
354
|
+
raw = read_file(file_path, fmt)
|
|
355
|
+
next if !fmt.binary? && raw.strip.empty?
|
|
356
356
|
|
|
357
357
|
begin
|
|
358
358
|
loaded = fmt.deserialize_many(raw, model_class)
|
|
@@ -375,8 +375,7 @@ module Lutaml
|
|
|
375
375
|
key = extract_model_key(model)
|
|
376
376
|
filename = key || model.class.name.to_s.gsub("::", "_")
|
|
377
377
|
file_path = File.join(dir, "#{filename}#{fmt.extension}")
|
|
378
|
-
|
|
379
|
-
File.write(file_path, content, encoding: "utf-8")
|
|
378
|
+
write_file(file_path, fmt.serialize(model), fmt)
|
|
380
379
|
model
|
|
381
380
|
end
|
|
382
381
|
end
|
|
@@ -391,8 +390,7 @@ module Lutaml
|
|
|
391
390
|
|
|
392
391
|
grouped.map do |key, group|
|
|
393
392
|
file_path = File.join(dir, "#{key}#{fmt.extension}")
|
|
394
|
-
|
|
395
|
-
File.write(file_path, content, encoding: "utf-8")
|
|
393
|
+
write_file(file_path, fmt.serialize_many(group), fmt)
|
|
396
394
|
group
|
|
397
395
|
end.flatten
|
|
398
396
|
end
|
|
@@ -420,6 +418,18 @@ module Lutaml
|
|
|
420
418
|
basename = File.basename(file_path, ".*")
|
|
421
419
|
model.public_send(:"#{registration.key_field}=", basename)
|
|
422
420
|
end
|
|
421
|
+
|
|
422
|
+
def read_file(file_path, fmt)
|
|
423
|
+
fmt.binary? ? File.binread(file_path) : File.read(file_path, encoding: "utf-8")
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def write_file(file_path, content, fmt)
|
|
427
|
+
if fmt.binary?
|
|
428
|
+
File.binwrite(file_path, content)
|
|
429
|
+
else
|
|
430
|
+
File.write(file_path, content, encoding: "utf-8")
|
|
431
|
+
end
|
|
432
|
+
end
|
|
423
433
|
end
|
|
424
434
|
end
|
|
425
435
|
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)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Store
|
|
5
|
+
# Bridges a Format handler to the ModelSerializer interface.
|
|
6
|
+
# Enables DatabaseStore to use any format (yaml, json, xml, yamls, marshal)
|
|
7
|
+
# for key-value storage instead of the default hash serialization.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# serializer = FormatSerializer.new(:yamls)
|
|
11
|
+
# store = DatabaseStore.new(
|
|
12
|
+
# adapter: :sqlite,
|
|
13
|
+
# models: [{ model: ConceptDocument, key: :id, serializer: serializer }]
|
|
14
|
+
# )
|
|
15
|
+
class FormatSerializer
|
|
16
|
+
DATA_KEY = "_data"
|
|
17
|
+
CLASS_KEY = "_class"
|
|
18
|
+
|
|
19
|
+
def initialize(format)
|
|
20
|
+
@format = Format.resolve(format)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def serialize(model)
|
|
24
|
+
{
|
|
25
|
+
DATA_KEY => @format.serialize(model),
|
|
26
|
+
CLASS_KEY => model.class.name
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def deserialize(data, model_class)
|
|
31
|
+
model_class = resolve_class(data[CLASS_KEY]) if data[CLASS_KEY]
|
|
32
|
+
@format.deserialize(data[DATA_KEY], model_class)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def resolve_class(class_name)
|
|
38
|
+
Object.const_get(class_name)
|
|
39
|
+
rescue NameError
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -42,6 +42,18 @@ module Lutaml
|
|
|
42
42
|
def sanitize_filename(key)
|
|
43
43
|
key.gsub(%r{[/:#?]}, "_")
|
|
44
44
|
end
|
|
45
|
+
|
|
46
|
+
def read_file(file_path, fmt)
|
|
47
|
+
fmt.binary? ? File.binread(file_path) : File.read(file_path, encoding: "utf-8")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def write_file(file_path, content, fmt)
|
|
51
|
+
if fmt.binary?
|
|
52
|
+
File.binwrite(file_path, content)
|
|
53
|
+
else
|
|
54
|
+
File.write(file_path, content, encoding: "utf-8")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
45
57
|
end
|
|
46
58
|
end
|
|
47
59
|
end
|
|
@@ -47,7 +47,7 @@ module Lutaml
|
|
|
47
47
|
return unless File.exist?(file_path)
|
|
48
48
|
|
|
49
49
|
fmt = format_for_file(definition.metadata_file)
|
|
50
|
-
raw =
|
|
50
|
+
raw = read_file(file_path, fmt)
|
|
51
51
|
metadata = fmt.deserialize(raw, definition.metadata_model)
|
|
52
52
|
package_store.metadata = metadata
|
|
53
53
|
end
|
|
@@ -60,7 +60,7 @@ module Lutaml
|
|
|
60
60
|
content = fmt.serialize(package_store.metadata)
|
|
61
61
|
file_path = File.join(path, definition.metadata_file)
|
|
62
62
|
FileUtils.mkdir_p(File.dirname(file_path))
|
|
63
|
-
|
|
63
|
+
write_file(file_path, content, fmt)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def read_model_entry(base_path, entry, package_store, fmt_name)
|
|
@@ -76,7 +76,7 @@ module Lutaml
|
|
|
76
76
|
return unless File.exist?(file_path)
|
|
77
77
|
|
|
78
78
|
fmt = resolve_format(fmt_name)
|
|
79
|
-
raw =
|
|
79
|
+
raw = read_file(file_path, fmt)
|
|
80
80
|
model = fmt.deserialize(raw, entry.model)
|
|
81
81
|
package_store.add_model(model)
|
|
82
82
|
end
|
|
@@ -91,8 +91,8 @@ module Lutaml
|
|
|
91
91
|
Dir.glob(glob).sort.each do |file_path|
|
|
92
92
|
next unless File.file?(file_path)
|
|
93
93
|
|
|
94
|
-
raw =
|
|
95
|
-
next if raw.strip.empty?
|
|
94
|
+
raw = read_file(file_path, fmt)
|
|
95
|
+
next if !fmt.binary? && raw.strip.empty?
|
|
96
96
|
|
|
97
97
|
begin
|
|
98
98
|
case entry.layout
|
|
@@ -127,7 +127,7 @@ module Lutaml
|
|
|
127
127
|
content = entry.layout == :grouped ? fmt.serialize_many(models) : fmt.serialize(models.first)
|
|
128
128
|
file_path = File.join(base_path, entry.file)
|
|
129
129
|
FileUtils.mkdir_p(File.dirname(file_path))
|
|
130
|
-
|
|
130
|
+
write_file(file_path, content, fmt)
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def write_directory_models(base_path, entry, models, fmt)
|
|
@@ -138,13 +138,13 @@ module Lutaml
|
|
|
138
138
|
when :grouped
|
|
139
139
|
models.group_by { |m| extract_key(m, entry) }.each do |key, group|
|
|
140
140
|
file_path = File.join(dir, "#{sanitize_filename(key)}#{fmt.extension}")
|
|
141
|
-
|
|
141
|
+
write_file(file_path, fmt.serialize_many(group), fmt)
|
|
142
142
|
end
|
|
143
143
|
else
|
|
144
144
|
models.each do |model|
|
|
145
145
|
key = extract_key(model, entry)
|
|
146
146
|
file_path = File.join(dir, "#{sanitize_filename(key)}#{fmt.extension}")
|
|
147
|
-
|
|
147
|
+
write_file(file_path, fmt.serialize(model), fmt)
|
|
148
148
|
end
|
|
149
149
|
end
|
|
150
150
|
end
|
|
@@ -93,14 +93,19 @@ module Lutaml
|
|
|
93
93
|
next if zip_entry.name == prefix || zip_entry.name.end_with?("/")
|
|
94
94
|
|
|
95
95
|
raw = zip_entry.get_input_stream.read
|
|
96
|
-
next if raw.strip.empty?
|
|
96
|
+
next if !fmt.binary? && raw.strip.empty?
|
|
97
97
|
|
|
98
98
|
begin
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
case entry.layout
|
|
100
|
+
when :grouped
|
|
101
|
+
fmt.deserialize_many(raw, entry.model).each do |m|
|
|
102
|
+
set_key_from_zip_path(m, zip_entry.name, entry, prefix)
|
|
103
|
+
package_store.add_model(m)
|
|
104
|
+
end
|
|
105
|
+
else
|
|
106
|
+
model = fmt.deserialize(raw, entry.model)
|
|
107
|
+
set_key_from_zip_path(model, zip_entry.name, entry, prefix)
|
|
108
|
+
package_store.add_model(model)
|
|
104
109
|
end
|
|
105
110
|
rescue StandardError => e
|
|
106
111
|
warn "PackageStore: failed to load #{zip_entry.name}: #{e.message}"
|
data/lib/lutaml/store/version.rb
CHANGED
data/lib/lutaml/store.rb
CHANGED
|
@@ -25,6 +25,7 @@ module Lutaml
|
|
|
25
25
|
autoload :CompositeModelHandler, "lutaml/store/composite_model_handler"
|
|
26
26
|
autoload :AttributeUpdater, "lutaml/store/attribute_updater"
|
|
27
27
|
autoload :DatabaseStore, "lutaml/store/database_store"
|
|
28
|
+
autoload :FormatSerializer, "lutaml/store/format_serializer"
|
|
28
29
|
autoload :PackageDefinition, "lutaml/store/package_definition"
|
|
29
30
|
autoload :PackageStore, "lutaml/store/package_store"
|
|
30
31
|
autoload :PackageTransport, "lutaml/store/package_transport"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-store
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ronald Tse
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -75,8 +75,10 @@ files:
|
|
|
75
75
|
- lib/lutaml/store/format/json.rb
|
|
76
76
|
- lib/lutaml/store/format/jsonl.rb
|
|
77
77
|
- lib/lutaml/store/format/marshal_format.rb
|
|
78
|
+
- lib/lutaml/store/format/xml.rb
|
|
78
79
|
- lib/lutaml/store/format/yaml.rb
|
|
79
80
|
- lib/lutaml/store/format/yamls.rb
|
|
81
|
+
- lib/lutaml/store/format_serializer.rb
|
|
80
82
|
- lib/lutaml/store/http_cache.rb
|
|
81
83
|
- lib/lutaml/store/http_cache_config.rb
|
|
82
84
|
- lib/lutaml/store/http_cache_entry.rb
|