Purpose
Lutaml::Store provides a store-centric database-style API for Lutaml::Model objects with model registry, polymorphic support, composite relationships, and multiple storage backends.
It offers a unified interface for storing and retrieving complex model hierarchies, batch file I/O with multiple serialization formats, HTTP-aware caching, and package-based persistence with ZIP and directory transports.
Features
-
DatabaseStore — high-level CRUD with model registry, polymorphism, composites
-
PackageStore — structured multi-model packages with ZIP and directory transport
-
BasicStore — low-level key-value store with optional cache, events, monitoring
-
CacheStore — TTL-aware cache with LRU eviction extending BasicStore
-
HttpCache — HTTP-aware caching with ETags, conditional requests, Cache-Control
-
Format-aware file I/O — YAML, YAMLS, JSON, JSONL, Marshal with layout strategies
-
Multiple backends — Memory, FileSystem, SQLite (all thread-safe)
-
Model registry — configurable key fields, polymorphic inheritance, composite relationships
-
Dot-notation updates — nested attribute paths with block-based updates
-
Ruby autoload — lazy constant loading, only loads what you use
Installation
Add this line to your application’s Gemfile:
gem 'lutaml-store'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install lutaml-store
For SQLite backend support, also add:
gem 'sqlite3'
Quick start
require 'lutaml/model'
require 'lutaml/store'
# Define your models
class Studio < Lutaml::Model::Serializable
attribute :studio_key, :string
attribute :name, :string
attribute :location, :string
end
class PotteryClass < Lutaml::Model::Serializable
attribute :studio, Studio
attribute :class_id, :string
attribute :description, :string
end
# Create a store with model registry
store = Lutaml::Store.new(
adapter: :memory,
models: [
{ model: PotteryClass, key: :class_id },
{ model: Studio, key: :studio_key }
]
)
# Save with composite relationships (both stored independently)
pottery = PotteryClass.new(
class_id: "pottery_101",
studio: Studio.new(studio_key: "main_studio", name: "Main Studio"),
description: "Beginner pottery class"
)
store.save(pottery)
# Fetch by model and key
retrieved = store.fetch(model: PotteryClass, class_id: "pottery_101")
puts retrieved.studio.name # => "Main Studio"
# Nested update with dot notation
store.update(
model: PotteryClass,
class_id: "pottery_101",
attributes: [
{ key: :description, value: "Advanced pottery class" },
{ key: "studio.location", value: "Building A" }
]
)
Architecture
Lutaml::Store has two layers:
- DatabaseStore (via
Lutaml::Store.new) -
High-level CRUD with model registry. Handles polymorphic dispatch, composite model decomposition, dot-notation updates, file I/O (
save_all,load_all,import_all,export). - BasicStore
-
Low-level key-value store wrapping an adapter. Provides
get,set,delete,exists?,all,keys, bulk operations, with optional caching, monitoring, and event emission.
Key classes
| Class | Role |
|---|---|
|
High-level CRUD with model registry, composites, polymorphism |
|
Multi-model packages with directory/ZIP transport |
|
Declarative schema for package structure (models, assets, metadata) |
|
Low-level key-value store with optional cache/events/monitoring |
|
TTL-aware cache store extending BasicStore |
|
HTTP-aware caching with ETags, conditional requests, Cache-Control |
|
Register models with key fields and polymorphic config |
|
Stores nested registered models independently, restores references |
|
Processes dot-notation paths and block-based updates |
|
Serialization/deserialization with custom serializer support |
|
Multi-format file I/O (YAML, JSON, JSONL, Marshal) with layout strategies |
|
Parses and validates store configuration |
Storage adapters
All adapters inherit from Adapter::Base and provide get, set, delete,
exists?, keys, all, clear, size, each_key, and bulk operations.
| Adapter | Type symbol | Use case |
|---|---|---|
|
|
Fast in-memory storage for testing, caching, temporary data |
|
|
Persistent file-based storage with integrity checks |
|
|
ACID-compliant database storage for production use |
Model registry
Registration
Register models with their unique key fields:
Polymorphic models
For inheritance hierarchies, register the base class with a polymorphic class key:
class Studio < Lutaml::Model::Serializable
attribute :studio_key, :string
attribute :name, :string
attribute :_class, :string, default: -> { "Studio" }, polymorphic_class: true
end
class CeramicStudio < Studio
attribute :clay_type, :string
attribute :_class, :string, default: -> { "CeramicStudio" }
end
store = Lutaml::Store.new(
adapter: :memory,
models: [
{ model: Studio, key: :studio_key, polymorphic_class_key: :_class }
]
)
store.save(CeramicStudio.new(studio_key: "cs1", name: "Clay Haus", clay_type: "Porcelain"))
retrieved = store.fetch(model: Studio, studio_key: "cs1")
puts retrieved.class.name # => "CeramicStudio"
Composite models
When registered models are nested within other registered models, they are stored independently while maintaining references:
store = Lutaml::Store.new(
adapter: :memory,
models: [
{ model: PotteryClass, key: :class_id },
{ model: Studio, key: :studio_key }
]
)
pottery = PotteryClass.new(
class_id: "p101",
studio: Studio.new(studio_key: "s1", name: "Main Studio")
)
store.save(pottery)
# Both accessible independently
store.fetch(model: Studio, studio_key: "s1").name # => "Main Studio"
store.fetch(model: PotteryClass, class_id: "p101").studio.name # => "Main Studio"
CRUD operations
Save
store.save(User.new(user_id: "u1", name: "Ada"))
store.save([user1, user2, user3]) # bulk save
Fetch
user = store.fetch(model: User, user_id: "u1")
Update
# Hash-based attributes
store.update(
model: User, user_id: "u1",
attributes: [
{ key: :name, value: "Grace" },
{ key: "studio.location", value: "Building A" } # dot notation for nesting
]
)
# Block-based
store.update(model: User, user_id: "u1") do |user|
user.name = "Grace"
end
Destroy
store.destroy(model: User, user_id: "u1")
PackageStore
PackageStore provides structured multi-model persistence with directory and ZIP
transports. It is built on PackageDefinition, which declares the package schema
declaratively.
Define a package
glossary = Lutaml::Store::PackageDefinition.new(name: "glossary") do |pkg|
pkg.model(model: Concept, key: :term, dir: "concepts", default_format: :yaml)
pkg.model(model: Author, key: :name, dir: "authors", default_format: :json)
pkg.asset("glossary.yaml", type: :file)
pkg. = GlossaryInfo
pkg. = "glossary.yaml"
end
Load and save packages
# Load from directory
store = Lutaml::Store::PackageStore.load(glossary, "./my_glossary", transport: :directory)
# Load from ZIP
store = Lutaml::Store::PackageStore.load(glossary, "./glossary.zip", transport: :zip)
# Query models
concepts = store.models_for(Concept)
store.model_count(Concept) # => 42
store.fetch_model(Concept, "API")
# Modify and save
store.add_model(Concept.new(term: "REST", definition: "..."))
store.save("./output", transport: :zip)
Package transports
| Transport | Symbol | Description |
|---|---|---|
|
|
Filesystem directory with subdirectories per model type |
|
|
ZIP archive containing the same directory structure |
Transports are resolved via registry (PackageTransport.resolve(:zip)),
extensible without modifying existing code.
File I/O
DatabaseStore provides batch file I/O through Format handlers.
Format handlers
| Format | Symbol | Extension | Description |
|---|---|---|---|
YAML |
|
|
Single-document YAML files |
YAMLS |
|
|
Multi-document YAML streams |
JSON |
|
|
Single JSON objects |
JSONL |
|
|
Line-delimited JSON |
Marshal |
|
|
Ruby Marshal binary format |
save_all / load_all / import_all / export
# Write models to directory
store.save_all(concepts, path: "./data", format: :yaml, layout: :separate)
# Read models (returns array, does NOT store in backend)
models = store.load_all(Concept, path: "./data", format: :yaml, layout: :separate)
# Read AND store in backend (makes them queryable)
store.import_all(Concept, path: "./data", format: :yaml, layout: :separate)
store.fetch(model: Concept, term: "API") # now available
# Export to a single file
store.export(all_concepts, path: "output.yaml", format: :yaml)
Layout strategies: :separate (one file per model), :grouped (models grouped
by key), :flat (one file per model, no subdirectory).
HTTP caching
HttpCache provides HTTP-aware caching with ETags, conditional requests (304),
Cache-Control directives, and Vary header support. It uses a storage adapter
internally and serializes cache entries as Lutaml::Model objects via JSON.
cache = Lutaml::Store::HttpCache.new(
adapter_type: "memory",
default_ttl: 3600,
respect_http_headers: true,
enable_conditional_requests: true
)
# Fetch with automatic caching
response = cache.fetch("GET", "https://api.example.com/resource", {}) do |headers|
http_client.get("https://api.example.com/resource", headers)
end
# Second call returns cached response (no HTTP request made)
cached = cache.fetch("GET", "https://api.example.com/resource", {}) { raise "shouldn't be called" }
Supports no-store, no-cache, must-revalidate, max-age, ETag-based
conditional requests, query parameter normalization, and filesystem/sqlite
adapters for persistent cache storage.
CacheStore
CacheStore extends BasicStore with TTL-aware caching and LRU eviction:
cache = Lutaml::Store::CacheStore.new(
adapter: :memory,
max_size: 1000,
default_ttl: 3600
)
cache.set("key1", "value1", ttl: 600)
cache.get("key1") # => "value1"
cache.fetch("key2", "default_value") # => "default_value" (stored)
cache.fetch("key3") { expensive_compute } # computes and caches
Storage backends
Memory
store = Lutaml::Store.new(adapter: :memory, models: [...])
Fast in-memory storage. Data lost on process exit. Thread-safe via mutex.
FileSystem
store = Lutaml::Store.new(
adapter: { type: :filesystem, path: "./data", extension: ".json" },
models: [...]
)
Persistent file-based storage with SHA-256 integrity checks. Files organized by key in subdirectories.
SQLite
store = Lutaml::Store.new(
adapter: { type: :sqlite, path: "./store.db" },
models: [...]
)
ACID-compliant database storage. Requires sqlite3 gem. Thread-safe with
connection pooling.
Event system
store.on(:model_save) { |data| logger.info("Saved #{data[:model].class}") }
store.on(:model_fetch) { |data| logger.debug("Fetched #{data[:key]}") }
store.on(:model_destroy) { |data| audit_log << data }
Events: :model_save, :model_fetch, :model_update, :model_destroy,
:model_save_all, :model_import, :model_export, :model_load_error,
:composite_model_stored, :polymorphic_model_resolved.
Error handling
Error hierarchy:
-
Lutaml::Store::Error-
ConfigurationError— invalid store or adapter config -
BackendError— adapter-level failures -
ModelNotRegisteredError— operations on unregistered models -
InvalidKeyError— missing or invalid key fields -
PolymorphicUpdateError— polymorphic type conflicts -
CompositeModelError— composite model handling failures
-
Thread safety
All adapters use mutex-based synchronization. Safe for concurrent use across threads.
Development
bin/setup # Install dependencies
bundle exec rake # Run specs + rubocop
bundle exec rspec # Run all specs
bundle exec rspec spec/lutaml/store/database_store_spec.rb # Single spec file
bundle exec rubocop # Lint
Ruby >= 3.1 required.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/lutaml/lutaml-store.
License
BSD-2-Clause. See the LICENSE file for details.
Copyright Ribose.