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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6093f17ea0bca9240b608835a29b8f060a0c8c41be950e93fc0713e8c1b9444
|
|
4
|
+
data.tar.gz: f0f9ee8ace22fa22ec62408425ba7fa479648c57158c92967bc8e235fd037285
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97a8aa5dc0d52617b1f4d97bd8378664eead01e299410d8dd506b775be23d69616008cde2e053b6a649819726048afdd2b227a455979630b2f07d04e76deed6d
|
|
7
|
+
data.tar.gz: b80834cd232f87d378c0a3a5f7ea17ef2c7ea2d68103d890756eabb2f2c85e20e1b1f193ec64cd0198fdb4235c0ff68ca6de87e788b7aabaa26b49a081e2023e
|
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-
|
|
3
|
+
# on 2026-06-10 09:02:53 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
|
|
@@ -11,54 +11,63 @@ Gemspec/RequiredRubyVersion:
|
|
|
11
11
|
Exclude:
|
|
12
12
|
- 'lutaml-store.gemspec'
|
|
13
13
|
|
|
14
|
-
# Offense count:
|
|
14
|
+
# Offense count: 38
|
|
15
15
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
|
16
16
|
Metrics/AbcSize:
|
|
17
17
|
Max: 112
|
|
18
18
|
|
|
19
|
-
# Offense count:
|
|
19
|
+
# Offense count: 63
|
|
20
20
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
21
21
|
# AllowedMethods: refine
|
|
22
22
|
Metrics/BlockLength:
|
|
23
23
|
Max: 387
|
|
24
24
|
|
|
25
|
-
# Offense count:
|
|
25
|
+
# Offense count: 17
|
|
26
26
|
# Configuration parameters: CountComments, CountAsOne.
|
|
27
27
|
Metrics/ClassLength:
|
|
28
|
-
Max:
|
|
28
|
+
Max: 516
|
|
29
29
|
|
|
30
|
-
# Offense count:
|
|
30
|
+
# Offense count: 15
|
|
31
31
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
32
32
|
Metrics/CyclomaticComplexity:
|
|
33
|
-
Max:
|
|
33
|
+
Max: 13
|
|
34
34
|
|
|
35
|
-
# Offense count:
|
|
35
|
+
# Offense count: 89
|
|
36
36
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
37
37
|
Metrics/MethodLength:
|
|
38
38
|
Max: 84
|
|
39
39
|
|
|
40
|
-
# Offense count:
|
|
40
|
+
# Offense count: 6
|
|
41
41
|
# Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
|
|
42
42
|
Metrics/ParameterLists:
|
|
43
43
|
Max: 8
|
|
44
44
|
|
|
45
|
-
# Offense count:
|
|
45
|
+
# Offense count: 8
|
|
46
46
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
47
47
|
Metrics/PerceivedComplexity:
|
|
48
|
-
Max:
|
|
48
|
+
Max: 14
|
|
49
49
|
|
|
50
50
|
# Offense count: 1
|
|
51
51
|
Naming/AccessorMethodName:
|
|
52
52
|
Exclude:
|
|
53
53
|
- 'lib/lutaml/store/adapter/sqlite.rb'
|
|
54
54
|
|
|
55
|
-
# Offense count:
|
|
55
|
+
# Offense count: 1
|
|
56
|
+
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
57
|
+
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
|
|
58
|
+
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
|
|
59
|
+
Naming/MemoizedInstanceVariableName:
|
|
60
|
+
Exclude:
|
|
61
|
+
- 'lib/lutaml/store/query.rb'
|
|
62
|
+
|
|
63
|
+
# Offense count: 4
|
|
56
64
|
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
|
57
65
|
# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
|
|
58
66
|
Naming/MethodParameterName:
|
|
59
67
|
Exclude:
|
|
60
68
|
- 'demo/vcard_models.rb'
|
|
61
69
|
- 'demo/vcard_store_demo.rb'
|
|
70
|
+
- 'lib/lutaml/store/adapter/sqlite.rb'
|
|
62
71
|
|
|
63
72
|
# Offense count: 1
|
|
64
73
|
# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs.
|
|
@@ -84,7 +93,7 @@ Security/MarshalLoad:
|
|
|
84
93
|
Exclude:
|
|
85
94
|
- 'lib/lutaml/store/format/marshal_format.rb'
|
|
86
95
|
|
|
87
|
-
# Offense count:
|
|
96
|
+
# Offense count: 51
|
|
88
97
|
# Configuration parameters: AllowedConstants.
|
|
89
98
|
Style/Documentation:
|
|
90
99
|
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
|
|
@@ -8,6 +8,8 @@ module Lutaml
|
|
|
8
8
|
@config = config
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# ── Key-value operations ──
|
|
12
|
+
|
|
11
13
|
def get(key)
|
|
12
14
|
raise NotImplementedError
|
|
13
15
|
end
|
|
@@ -60,6 +62,32 @@ module Lutaml
|
|
|
60
62
|
keys.each_with_object({}) { |k, h| h[k] = delete(k) }
|
|
61
63
|
end
|
|
62
64
|
|
|
65
|
+
# ── Query operations ──
|
|
66
|
+
|
|
67
|
+
# Execute a query specification.
|
|
68
|
+
# Returns array of [storage_key, hash_data] pairs, sorted and paginated.
|
|
69
|
+
# Returns nil to indicate "not supported — fall back to in-memory scan".
|
|
70
|
+
def execute_query(_query)
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Count matching records without returning data.
|
|
75
|
+
# Returns nil to indicate "not supported — count execute_query results".
|
|
76
|
+
def count_query(_query)
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Fetch a batch of [storage_key, hash_data] pairs for keyset pagination.
|
|
81
|
+
# Returns nil to indicate "not supported — fall back to in-memory".
|
|
82
|
+
def batch_query(_query, _after: nil, _limit: 1000)
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Execute a block atomically. No-op for adapters without transaction support.
|
|
87
|
+
def transaction
|
|
88
|
+
yield
|
|
89
|
+
end
|
|
90
|
+
|
|
63
91
|
def stats
|
|
64
92
|
{ adapter: self.class.name }
|
|
65
93
|
end
|