Purpose
Lutaml::Store provides a sophisticated store-centric database-style API for LutaML Models with model registry, polymorphic support, and composite model relationships.
It offers a unified interface for storing and retrieving complex model hierarchies across different storage backends, making it ideal for applications that need sophisticated object persistence with database-like operations.
Features
-
Store-centric API design with all persistence operations through the store
-
Model registry system with configurable key fields
-
Polymorphic model support with inheritance handling
-
Composite model relationships (nested registered models stored independently)
-
Database-style CRUD operations (fetch, save, update, destroy)
-
Format-aware file I/O (YAML, JSON, JSONL, Marshal) with layout strategies
-
Directory import with queryable backend (import_all)
-
Custom serializer support for key collision workarounds
-
Dot notation for nested updates ("studio.location")
-
Both block-based and hash-based update patterns
-
Multiple storage backends: Memory, FileSystem, and SQLite
-
Thread-safe operations across all backends
-
Event system with synchronous and asynchronous handling
-
Performance monitoring and error tracking
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
Here’s a minimal example to get you started:
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 }
]
)
# Create and save models
pottery_class = PotteryClass.new(
class_id: "pottery_101",
studio: Studio.new(studio_key: "main_studio", name: "Main Studio"),
description: "Beginner pottery class"
)
store.save(pottery_class)
# Fetch models
retrieved = store.fetch(model: PotteryClass, class_id: "pottery_101")
puts retrieved.studio.name # => "Main Studio"
# Update models
store.update(
model: PotteryClass,
class_id: "pottery_101",
attributes: [
{ key: :description, value: "Advanced pottery class" },
{ key: "studio.location", value: "Building A" }
]
)
Architecture overview
Lutaml::Store implements a store-centric architecture where all persistence operations flow through a central store that manages model registrations, relationships, and storage backends.
Core components
The library is organized into these main components:
Lutaml::Store-
Main entry point providing the store-centric API with model registry support. Handles model registration, polymorphic relationships, and composite model management.
Lutaml::Store::ModelRegistry-
Manages registered models with their key fields and polymorphic configurations. Validates model registrations and provides model lookup capabilities.
Lutaml::Store::CompositeModelHandler-
Handles relationships between registered models, storing composite models independently while maintaining references and ensuring referential integrity.
Lutaml::Store::AttributeUpdater-
Processes model updates including dot notation for nested attributes, block-based updates, and polymorphic model changes.
Lutaml::Store::Format-
Format-aware file I/O with handlers for YAML, YAMLS, JSON, JSONL, and Marshal. Provides
load_all,save_all,import_all, andexportfor batch persistence with configurable layout strategies (separate, grouped, flat). Lutaml::Store::ModelSerializer-
Delegates to custom serializers per model registration, or falls back to
to_hash/from_hashfor standardLutaml::Model::Serializableinstances. Lutaml::Store::Store-
Low-level storage interface providing unified key-value operations across all backends with caching, events, and monitoring integration.
Storage backends
Memory Backend-
Fast in-memory storage ideal for caching, testing, and temporary data with volatile persistence characteristics.
FileSystem Backend-
Persistent file-based storage with directory organization suitable for moderate data volumes and development environments.
SQLite Backend-
Database storage with ACID compliance, transaction support, and durability for production applications requiring data integrity.
Model registry system
The model registry is the foundation of Lutaml::Store’s sophisticated persistence capabilities. It allows you to register models with their unique key fields and configure polymorphic relationships.
Basic model registration
Register models by specifying the model class and its unique key field:
Model registration with validation:
class User < Lutaml::Model::Serializable
attribute :user_id, :string
attribute :name, :string
attribute :email, :string
end
# The key field must exist as an attribute
store = Lutaml::Store.new(
adapter: :memory,
models: [
{ model: User, key: :user_id } # ✓ Valid - user_id exists
# { model: User, key: :invalid } # ✗ Error - invalid doesn't exist
]
)
Polymorphic model registration
For models with inheritance hierarchies, use polymorphic registration:
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
}
# CeramicStudio inherits from Studio, so no separate registration needed
]
)
Polymorphic model usage:
# Save different types of studios
regular_studio = Studio.new(studio_key: "studio1", name: "Regular Studio")
ceramic_studio = CeramicStudio.new(
studio_key: "studio2",
name: "Ceramic Studio",
clay_type: "Porcelain"
)
store.save([regular_studio, ceramic_studio])
# Fetch returns correct subclass
retrieved = store.fetch(model: Studio, studio_key: "studio2")
puts retrieved.class.name # => "CeramicStudio"
puts retrieved.clay_type # => "Porcelain"
Composite model relationships
When registered models are nested within other registered models, they are stored independently while maintaining references:
class PotteryClass < Lutaml::Model::Serializable
attribute :studio, Studio # Studio is also registered
attribute :class_id, :string
attribute :description, :string
end
store = Lutaml::Store.new(
adapter: :memory,
models: [
{ model: PotteryClass, key: :class_id },
{ model: Studio, key: :studio_key }
]
)
# Both PotteryClass and its nested Studio are stored independently
pottery_class = PotteryClass.new(
class_id: "pottery_101",
studio: Studio.new(studio_key: "main_studio", name: "Main Studio"),
description: "Pottery class"
)
store.save(pottery_class)
# Studio can be fetched independently
studio = store.fetch(model: Studio, studio_key: "main_studio")
puts studio.name # => "Main Studio"
# PotteryClass maintains reference to Studio
pottery = store.fetch(model: PotteryClass, class_id: "pottery_101")
puts pottery.studio.name # => "Main Studio"
CRUD operations
Lutaml::Store provides database-style CRUD operations for registered models.
Save operations
Save single models or arrays of models:
# Save single model
user = User.new(user_id: "user1", name: "John Doe")
store.save(user)
# Save array of models
users = [
User.new(user_id: "user2", name: "Jane Smith"),
User.new(user_id: "user3", name: "Bob Johnson")
]
store.save(users)
Saving models with composite relationships:
pottery_classes = [
PotteryClass.new(
class_id: "pottery_101",
studio: Studio.new(studio_key: "studio1", name: "Main Studio"),
description: "Beginner class"
),
PotteryClass.new(
class_id: "pottery_201",
studio: Studio.new(studio_key: "studio2", name: "Advanced Studio"),
description: "Advanced class"
)
]
# Saves both PotteryClass instances and their nested Studio instances
store.save(pottery_classes)
# Studios are now available independently
studio1 = store.fetch(model: Studio, studio_key: "studio1")
studio2 = store.fetch(model: Studio, studio_key: "studio2")
Fetch operations
Retrieve models by their registered key fields:
# Fetch by key field name and value
user = store.fetch(model: User, user_id: "user1")
pottery_class = store.fetch(model: PotteryClass, class_id: "pottery_101")
studio = store.fetch(model: Studio, studio_key: "main_studio")
Fetching polymorphic models:
# Save different studio types
regular_studio = Studio.new(studio_key: "studio1", name: "Regular")
ceramic_studio = CeramicStudio.new(
studio_key: "studio2",
name: "Ceramic",
clay_type: "Stoneware"
)
store.save([regular_studio, ceramic_studio])
# Fetch returns correct polymorphic type
studio1 = store.fetch(model: Studio, studio_key: "studio1")
puts studio1.class.name # => "Studio"
studio2 = store.fetch(model: Studio, studio_key: "studio2")
puts studio2.class.name # => "CeramicStudio"
puts studio2.clay_type # => "Stoneware"
Update operations
Update models using attribute arrays, dot notation, or blocks:
Attribute array updates
store.update(
model: User,
user_id: "user1",
attributes: [
{ key: :name, value: "John Smith" },
{ key: :email, value: "john.smith@example.com" }
]
)
Dot notation for nested updates
store.update(
model: PotteryClass,
class_id: "pottery_101",
attributes: [
{ key: :description, value: "Updated description" },
{ key: "studio.location", value: "Building A, Room 101" },
{ key: "studio.name", value: "Updated Studio Name" }
]
)
Complex nested updates:
# Update multiple nested attributes
store.update(
model: PotteryClass,
class_id: "pottery_101",
attributes: [
{ key: :description, value: "Advanced pottery techniques" },
{ key: "studio.name", value: "Master Pottery Studio" },
{ key: "studio.location", value: "Downtown Arts District" }
]
)
# Verify updates
pottery_class = store.fetch(model: PotteryClass, class_id: "pottery_101")
puts pottery_class.description # => "Advanced pottery techniques"
puts pottery_class.studio.name # => "Master Pottery Studio"
puts pottery_class.studio.location # => "Downtown Arts District"
Block-based updates
store.update(model: User, user_id: "user1") do |user|
user.name = "Updated Name"
user.email = "updated@example.com"
end
Polymorphic model updates
Change model types by updating with different polymorphic instances:
# Change Studio to CeramicStudio
store.update(
model: PotteryClass,
class_id: "pottery_101",
attributes: [
{
key: :studio,
value: CeramicStudio.new(
studio_key: "main_studio", # Same key, different type
name: "Ceramic Arts Studio",
clay_type: "Porcelain"
)
}
]
)
# Fetch returns updated polymorphic type
pottery_class = store.fetch(model: PotteryClass, class_id: "pottery_101")
puts pottery_class.studio.class.name # => "CeramicStudio"
puts pottery_class.studio.clay_type # => "Porcelain"
Destroy operations
Delete models by their key fields:
# Delete single model
store.destroy(model: User, user_id: "user1")
# Delete model with composite relationships
store.destroy(model: PotteryClass, class_id: "pottery_101")
# Note: Nested Studio remains unless explicitly deleted
Managing composite model deletion:
# Delete pottery class but keep studio
store.destroy(model: PotteryClass, class_id: "pottery_101")
# Studio still exists independently
studio = store.fetch(model: Studio, studio_key: "main_studio")
puts studio.name # => Still accessible
# Delete studio separately if needed
store.destroy(model: Studio, studio_key: "main_studio")
File I/O and format handling
Lutaml::Store provides format-aware file I/O for batch persistence. Models can be written to and read from directories using multiple serialization formats and layout strategies.
Format handlers
Five built-in format handlers serialize and deserialize Lutaml::Model::Serializable instances:
| Format | Symbol | Extension | Description |
|---|---|---|---|
YAML |
|
|
Single-document YAML files |
YAMLS |
|
|
Multi-document YAML streams (many models per file) |
JSON |
|
|
Single JSON objects |
JSONL |
|
|
Line-delimited JSON (one object per line) |
Marshal |
|
|
Ruby Marshal binary format |
All format handlers implement serialize, deserialize, serialize_many, and
deserialize_many.
Layout strategies
Three layout strategies control how files are organized on disk:
| Layout | Symbol | Structure |
|---|---|---|
Separate |
|
One file per model, named by key field |
Grouped |
|
Multiple models per file, grouped by key |
Flat |
|
One file per model (no subdirectory grouping) |
save_all: batch write to directory
Write a collection of models to a directory:
# Save with separate layout (one YAML file per model)
store.save_all(concepts, path: "./data", format: :yaml, layout: :separate)
# Creates: ./data/concept/key1.yaml, ./data/concept/key2.yaml, ...
# Save as grouped (all models in one multi-document YAML)
store.save_all(concepts, path: "./data", format: :yamls, layout: :grouped)
# Save as JSONL (one JSON object per line)
store.save_all(items, path: "./data", format: :jsonl, layout: :separate)
The subdirectory name comes from the dir option in model registration:
load_all: batch read from directory
Read models from a directory without storing them in the backend:
models = store.load_all(Concept, path: "./data", format: :yaml, layout: :separate)
# Returns an array of Concept instances, does NOT store in backend
import_all: load and index for querying
Load models from a directory AND store them in the key-value backend, making
them available for fetch, where, count, and exists? queries:
# Import all concepts from directory
loaded = store.import_all(Concept, path: "./data", format: :yaml, layout: :separate)
# Now queryable via the store
concept = store.fetch(model: Concept, uuid: "abc-123")
matching = store.where(model: Concept, status: "valid")
count = store.count(model: Concept)
export: write to a single file
Serialize models to a single output file:
all_concepts = store.all(model: Concept)
store.export(all_concepts, path: "output/concepts.yaml", format: :yaml)
Custom serializers
When a model’s key_value DSL maps multiple attributes to the same serialized
key (e.g., uuid and identifier both mapping to "id"), a custom serializer
preserves both values through the store round-trip:
class ConceptStore
class Serializer
def serialize(model)
{
"_yaml" => model.to_yaml,
"_uuid" => model.uuid,
"_identifier" => model.identifier
}
end
def deserialize(data, model_class)
model = model_class.from_yaml(data["_yaml"])
model.assign_uuid(data["_uuid"]) if data["_uuid"]
model.identifier = data["_identifier"] if data["_identifier"]
model
end
end
end
store = Lutaml::Store.new(
adapter: :memory,
models: [
{
model: Concept,
key: :uuid,
dir: "concepts",
serializer: Serializer.new
}
]
)
Custom serializer with key collision workaround:
# Without custom serializer: to_hash loses one of uuid/identifier
# because both map to "id" key. The custom serializer stores the
# full YAML string plus explicit metadata fields, preserving both.
Advanced features
Polymorphic inheritance handling
Lutaml::Store automatically handles polymorphic inheritance chains, storing and retrieving the correct subclass instances:
class Vehicle < Lutaml::Model::Serializable
attribute :vehicle_id, :string
attribute :make, :string
attribute :_type, :string, default: -> { "Vehicle" }, polymorphic_class: true
end
class Car < Vehicle
attribute :doors, :integer
attribute :_type, :string, default: -> { "Car" }
end
class Truck < Vehicle
attribute :payload, :integer
attribute :_type, :string, default: -> { "Truck" }
end
store = Lutaml::Store.new(
adapter: :memory,
models: [
{ model: Vehicle, key: :vehicle_id, polymorphic_class_key: :_type }
]
)
Polymorphic inheritance in action:
# Save different vehicle types
vehicles = [
Car.new(vehicle_id: "car1", make: "Toyota", doors: 4),
Truck.new(vehicle_id: "truck1", make: "Ford", payload: 2000),
Vehicle.new(vehicle_id: "vehicle1", make: "Generic")
]
store.save(vehicles)
# Fetch returns correct subclass
car = store.fetch(model: Vehicle, vehicle_id: "car1")
puts car.class.name # => "Car"
puts car.doors # => 4
truck = store.fetch(model: Vehicle, vehicle_id: "truck1")
puts truck.class.name # => "Truck"
puts truck.payload # => 2000
Composite model reference management
When registered models contain other registered models, Lutaml::Store manages the relationships automatically:
class Order < Lutaml::Model::Serializable
attribute :order_id, :string
attribute :customer, User # User is registered
attribute :items, [Product] # Product is registered
attribute :total, :decimal
end
store = Lutaml::Store.new(
adapter: :memory,
models: [
{ model: Order, key: :order_id },
{ model: User, key: :user_id },
{ model: Product, key: :product_id }
]
)
Composite model relationships:
# Create order with nested registered models
order = Order.new(
order_id: "order1",
customer: User.new(user_id: "user1", name: "John Doe"),
items: [
Product.new(product_id: "prod1", name: "Widget", price: 10.00),
Product.new(product_id: "prod2", name: "Gadget", price: 15.00)
],
total: 25.00
)
store.save(order)
# All models are stored independently
customer = store.fetch(model: User, user_id: "user1")
product1 = store.fetch(model: Product, product_id: "prod1")
product2 = store.fetch(model: Product, product_id: "prod2")
# Order maintains references to all nested models
retrieved_order = store.fetch(model: Order, order_id: "order1")
puts retrieved_order.customer.name # => "John Doe"
puts retrieved_order.items.first.name # => "Widget"
Nested attribute updates with dot notation
Update deeply nested attributes using dot notation:
# Update nested attributes
store.update(
model: Order,
order_id: "order1",
attributes: [
{ key: "customer.name", value: "Jane Doe" },
{ key: "customer.email", value: "jane@example.com" },
{ key: "items.0.price", value: 12.00 }, # Update first item price
{ key: :total, value: 27.00 }
]
)
Complex nested updates:
# Multi-level nested updates
store.update(
model: PotteryClass,
class_id: "pottery_101",
attributes: [
{ key: "studio.name", value: "New Studio Name" },
{ key: "studio.location", value: "New Location" }
]
)
# Updates are reflected in both the parent and the independently stored model
pottery_class = store.fetch(model: PotteryClass, class_id: "pottery_101")
studio = store.fetch(model: Studio, studio_key: pottery_class.studio.studio_key)
puts pottery_class.studio.name # => "New Studio Name"
puts studio.name # => "New Studio Name" (same instance)
Storage backends
Lutaml::Store supports multiple storage backends, each optimized for different use cases and requirements.
Memory backend
Fast in-memory storage ideal for testing, caching, and temporary data:
Characteristics:
-
Fastest performance for all operations
-
Volatile storage (data lost when process ends)
-
No persistence across application restarts
-
Ideal for testing and caching scenarios
FileSystem backend
Persistent file-based storage with directory organization:
Characteristics:
-
Persistent storage across application restarts
-
Human-readable file format (JSON by default)
-
Good for development and moderate data volumes
-
Directory-based organization for easy browsing
FileSystem backend configuration:
store = Lutaml::Store.new(
adapter: {
type: :filesystem,
path: "/var/app/data",
extension: "dat",
create_directories: true
},
models: [
{ model: User, key: :user_id },
{ model: Post, key: :post_id }
]
)
# Files are organized by model type:
# /var/app/data/User/user1.dat
# /var/app/data/User/user2.dat
# /var/app/data/Post/post1.dat
SQLite backend
Database storage with ACID compliance and transaction support:
Characteristics:
-
ACID compliance with transaction support
-
Excellent durability and data integrity
-
Suitable for production applications
-
SQL query capabilities (future enhancement)
Configuration and customization
Programmatic configuration
Configure stores programmatically with full control over all options:
store = Lutaml::Store.new(
adapter: {
type: :filesystem,
path: "./data",
extension: "json"
},
models: [
{ model: User, key: :user_id },
{
model: Studio,
key: :studio_key,
polymorphic_class_key: :_class
}
],
cache: {
enabled: true,
max_size: 1000,
ttl: 3600
},
monitoring: {
enabled: true
},
events: {
async: false
}
)
YAML configuration
Use YAML files for environment-specific configurations:
# config/store.yml
development:
adapter:
type: filesystem
path: ./tmp/store
extension: json
models:
- model: User
key: user_id
- model: Studio
key: studio_key
polymorphic_class_key: _class
cache:
enabled: true
max_size: 100
ttl: 1800
production:
adapter:
type: sqlite
path: /var/app/data/store.db
models:
- model: User
key: user_id
- model: Studio
key: studio_key
polymorphic_class_key: _class
cache:
enabled: true
max_size: 10000
ttl: 3600
monitoring:
enabled: true
Loading YAML configuration:
# Load environment-specific configuration
config = YAML.load_file("config/store.yml")[Rails.env]
# Convert model configurations to proper format
models = config["models"].map do |model_config|
{
model: model_config["model"].constantize,
key: model_config["key"].to_sym,
polymorphic_class_key: model_config["polymorphic_class_key"]&.to_sym
}.compact
end
store = Lutaml::Store.new(
adapter: config["adapter"],
models: models,
cache: config["cache"],
monitoring: config["monitoring"]
)
Event system
Lutaml::Store provides a comprehensive event system for monitoring and reacting to store operations.
Available events
The store emits events for all major operations:
-
:model_save- When models are saved -
:model_fetch- When models are fetched -
:model_update- When models are updated -
:model_destroy- When models are destroyed -
:model_save_all- When models are batch-saved to directory -
:model_import- When models are imported from directory into backend -
:model_export- When models are exported to a single file -
:model_load_error- When a file fails to load during load_all/import_all -
:composite_model_stored- When composite models are stored independently -
:polymorphic_model_resolved- When polymorphic models are resolved
Event listeners
Register event listeners to react to store operations:
# Register event listeners
store.on(:model_save) do |event_data|
puts "Saved #{event_data[:model].class.name} with key #{event_data[:key]}"
end
store.on(:model_update) do |event_data|
puts "Updated #{event_data[:model].class.name}: #{event_data[:changes]}"
end
store.on(:model_fetch) do |event_data|
puts "Fetched #{event_data[:model].class.name} from #{event_data[:source]}"
end
Comprehensive event monitoring:
# Audit trail implementation
audit_log = []
store.on(:model_save) do |data|
audit_log << {
action: :save,
model: data[:model].class.name,
key: data[:key],
timestamp: Time.now
}
end
store.on(:model_update) do |data|
audit_log << {
action: :update,
model: data[:model].class.name,
key: data[:key],
changes: data[:changes],
timestamp: Time.now
}
end
# Use the store
user = User.new(user_id: "user1", name: "John")
store.save(user)
store.update(model: User, user_id: "user1") do |u|
u.name = "Jane"
end
puts audit_log
# => [
# { action: :save, model: "User", key: "user1", timestamp: ... },
# { action: :update, model: "User", key: "user1", changes: {...}, timestamp: ... }
# ]
Performance and monitoring
Performance characteristics
Different backends have different performance profiles:
Memory Backend:
-
Read: O(1) - Hash lookup
-
Write: O(1) - Hash assignment
-
Memory usage: All data in RAM
FileSystem Backend:
-
Read: O(1) + file I/O
-
Write: O(1) + file I/O + serialization
-
Memory usage: Minimal (data on disk)
SQLite Backend:
-
Read: O(log n) - B-tree lookup
-
Write: O(log n) + transaction overhead
-
Memory usage: Configurable cache + minimal
Monitoring and statistics
Enable monitoring to track performance and usage:
store = Lutaml::Store.new(
adapter: :memory,
models: [{ model: User, key: :user_id }],
monitoring: { enabled: true }
)
# Get comprehensive statistics
stats = store.stats
puts stats
# => {
# models_registered: 1,
# total_operations: 150,
# operations: {
# save: 50,
# fetch: 80,
# update: 15,
# destroy: 5
# },
# performance: {
# save: { avg: 0.001, min: 0.0005, max: 0.002 },
# fetch: { avg: 0.0008, min: 0.0003, max: 0.0015 }
# },
# backend: "Memory",
# cache_hit_rate: 0.75
# }
Performance monitoring in production:
# Monitor cache performance
store.on(:cache_miss) do |data|
metrics.increment("store.cache.miss", tags: ["model:#{data[:model]}"])
end
store.on(:cache_hit) do |data|
metrics.increment("store.cache.hit", tags: ["model:#{data[:model]}"])
end
Error handling
Lutaml::Store defines specific error types for different failure scenarios:
Error types
Lutaml::Store::ModelNotRegisteredError-
Raised when attempting operations on unregistered models.
Lutaml::Store::InvalidKeyError-
Raised when key field doesn’t exist on model or key value is nil.
Lutaml::Store::PolymorphicUpdateError-
Raised when polymorphic model updates fail due to type conflicts.
Lutaml::Store::CompositeModelError-
Raised when composite model handling encounters issues.
Lutaml::Store::ConfigurationError-
Raised for invalid store or adapter configurations.
Error handling patterns
begin
store = Lutaml::Store.new(
adapter: :memory,
models: [
{ model: User, key: :invalid_field } # Field doesn't exist
]
)
rescue Lutaml::Store::ConfigurationError => e
puts "Configuration error: #{e.}"
end
begin
# Try to fetch unregistered model
result = store.fetch(model: UnregisteredModel, id: "test")
rescue Lutaml::Store::ModelNotRegisteredError => e
puts "Model not registered: #{e.}"
end
Comprehensive error handling:
def safe_store_operation
yield
rescue Lutaml::Store::ModelNotRegisteredError => e
logger.error "Unregistered model: #{e.}"
{ error: "Model not found", details: e. }
rescue Lutaml::Store::InvalidKeyError => e
logger.error "Invalid key: #{e.}"
{ error: "Invalid key", details: e. }
rescue Lutaml::Store::ConfigurationError => e
logger.error "Configuration error: #{e.}"
{ error: "Configuration issue", details: e. }
rescue => e
logger.error "Unexpected error: #{e.}"
{ error: "Internal error", details: e. }
end
Thread safety
All Lutaml::Store operations are thread-safe across all backends:
store = Lutaml::Store.new(
adapter: :memory,
models: [{ model: User, key: :user_id }]
)
# Safe to use from multiple threads
threads = 10.times.map do |i|
Thread.new do
user = User.new(user_id: "user#{i}", name: "User #{i}")
store.save(user)
retrieved = store.fetch(model: User, user_id: "user#{i}")
puts "Thread #{i}: #{retrieved.name}"
end
end
threads.each(&:join)
store = Lutaml::Store.new(
adapter: :sqlite,
models: [
{ model: User, key: :user_id },
{ model: Post, key: :post_id }
]
)
# Multiple threads can safely operate on different models
user_thread = Thread.new do
100.times do |i|
user = User.new(user_id: "user#{i}", name: "User #{i}")
store.save(user)
end
end
post_thread = Thread.new do
100.times do |i|
post = Post.new(post_id: "post#{i}", title: "Post #{i}")
store.save(post)
end
end
[user_thread, post_thread].each(&:join)
Development
After checking out the repo, run:
bin/setup # Install dependencies
bundle exec rspec # Run tests
bundle exec rubocop # Run linting
To install this gem onto your local machine, run:
bundle exec rake install
To release a new version, update the version number in version.rb, and then run:
bundle exec rake release
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/lutaml/lutaml-store.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License and copyright
This project is licensed under the MIT License. See the LICENSE file for details.
Copyright Ribose.