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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c26211ca36caae48ef419c2feb670e6d35ddfb958b97b27eb04173c16eaa804
4
- data.tar.gz: '06953ee0ec088797069476ccd0a2595aaae24b71fc295b3882ac628464b53da0'
3
+ metadata.gz: b6093f17ea0bca9240b608835a29b8f060a0c8c41be950e93fc0713e8c1b9444
4
+ data.tar.gz: f0f9ee8ace22fa22ec62408425ba7fa479648c57158c92967bc8e235fd037285
5
5
  SHA512:
6
- metadata.gz: ac429b0436457ac91e52d18290c407598529d041294911c652a33fda075b4f24373965aea422568e85bcfa68e978a64fb4dc45b642fb9034555869a597ccb161
7
- data.tar.gz: fefac8e5929d619716a9cf890601928fb52738b3ecb5d6a861a90d7d28db7902624108a8d2054a230ea4810291001690455777e0cbcebaa912fd9c8f67f11591
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-05 04:23:39 UTC using RuboCop version 1.87.0.
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: 29
14
+ # Offense count: 38
15
15
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
16
16
  Metrics/AbcSize:
17
17
  Max: 112
18
18
 
19
- # Offense count: 50
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: 16
25
+ # Offense count: 17
26
26
  # Configuration parameters: CountComments, CountAsOne.
27
27
  Metrics/ClassLength:
28
- Max: 379
28
+ Max: 516
29
29
 
30
- # Offense count: 9
30
+ # Offense count: 15
31
31
  # Configuration parameters: AllowedMethods, AllowedPatterns.
32
32
  Metrics/CyclomaticComplexity:
33
- Max: 11
33
+ Max: 13
34
34
 
35
- # Offense count: 80
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: 5
40
+ # Offense count: 6
41
41
  # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
42
42
  Metrics/ParameterLists:
43
43
  Max: 8
44
44
 
45
- # Offense count: 5
45
+ # Offense count: 8
46
46
  # Configuration parameters: AllowedMethods, AllowedPatterns.
47
47
  Metrics/PerceivedComplexity:
48
- Max: 11
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: 3
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: 35
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` | Single point of serialization/deserialization for Lutaml::Model objects |
38
- | `Config` | Parses and validates store configuration (adapter, cache, monitoring, compression) |
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`. The `DatabaseStore` creates the adapter internally via `BasicStore`; the adapter type is passed as `adapter: :memory`, `adapter: { type: :filesystem, path: "..." }`, etc.
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
- ### HTTP caching
52
+ ### PackageStore and transports
45
53
 
46
- `HttpCache` provides HTTP-aware caching with ETags, conditional requests (304), Cache-Control, and Vary header support. Used by lutaml-hal to avoid re-fetching HAL resources.
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
- ### Serialization & integrity
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
- `Compression` adds gzip support. `Integrity` provides SHA256 checksums for data verification (used by the FileSystem adapter).
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, plan.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. It is built on `PackageDefinition`, which declares the package schema
313
- declaratively.
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
- glossary = Lutaml::Store::PackageDefinition.new(name: "glossary") do |pkg|
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
- pkg.asset("glossary.yaml", type: :file)
323
- pkg.metadata_model = GlossaryInfo
324
- pkg.metadata_file = "glossary.yaml"
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
- === Load and save packages
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", transport: :directory)
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.model_count(Concept) # => 42
341
- store.fetch_model(Concept, "API")
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
- # Modify and save
344
- store.add_model(Concept.new(term: "REST", definition: "..."))
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