🏰 Stately
Type-safe state management with entity relationships and CRUD operations
Overview
Stately provides a framework for managing application configuration and state with built-in support for:
- 🔗 Entity Relationships - Reference entities inline or by ID
- 📝 CRUD Operations - Create, read, update, delete for all entity types
- 🔄 Serialization - Full serde support
- 📚 OpenAPI Schemas - Automatic schema generation with
utoipa - 🆔 Time-Sortable IDs - UUID v7 for naturally ordered identifiers
- 🚀 Web APIs - Optional Axum integration with generated handlers (more frameworks coming soon)
- 🔍 Search & Query - Built-in entity search across collections
- 🌍 Foreign Types - Use types from external crates in your state
Stately does not provide the configuration and structures that comprise the state. Instead it provides an ultra-thin container management strategy that provides seamless integration with @stately/ui.
Installation
Add to your Cargo.toml:
[]
= "0.3.1"
With Axum API generation:
[]
= { = "0.3.1", = ["axum"] }
Quick Start
Define Entities
Use the #[stately::entity] macro to define your domain entities:
use *;
Define State
Use the #[stately::state] macro to create your application state:
This generates:
StateEntryenum for entity type discriminationEntityenum for type-erased entity access- Collections with full CRUD operations
- Search and query methods
Collection Attributes
Use #[collection(...)] to customize how collections are generated:
// Can be a struct that implements `StateCollection`. This type alias is for simplicity.
type CustomStateCollectionImpl = ;
Without variant, the macro generates enum variant names from the entity type name. Use variant to:
- Avoid naming collisions when using the same entity type in multiple collections
- Control the names in generated
StateEntryandEntityenums - Improve API clarity (e.g.,
StateEntry::CachedSourceConfigvsStateEntry::SourceConfig)
Use the State
let mut state = new;
// Create entities
let source_id = state.sources.create;
// Reference entities
let pipeline = Pipeline ;
let pipeline_id = state.pipelines.create;
// Query
let = state.get_entity.unwrap;
// List all
let summaries = state.list_entities;
// Search
let results = state.search_entities;
// Update
state.pipelines.update?;
// Delete
state.pipelines.remove?;
📖 Examples
use *;
// Define your entities
// Define your application state
See the examples directory:
basic.rs- Core CRUD operations and entity relationshipsaxum_api.rs- Web API generation with Axum
Run examples:
Entity Relationships with Link<T>
The Link<T> type allows flexible entity references:
// Reference by ID
let link = create_ref;
// Inline embedding
let link = inline;
// Access
match &pipeline.source
Singleton Entities
For configuration that should have exactly one instance:
Foreign Type Support
Stately allows you to use types from external crates (foreign types) in your state by using the #[collection(foreign)] attribute. This is useful for managing third-party types like configuration formats, API responses, or other external data structures.
When you mark a collection as foreign, the #[stately::state] macro generates a ForeignEntity trait in your crate that you can implement on external types:
use Value;
// The macro generates this trait in your crate:
// pub trait ForeignEntity: Clone + Serialize + for<'de> Deserialize<'de> {
// fn name(&self) -> &str;
// fn description(&self) -> Option<&str> { None }
// fn summary(&self, id: EntityId) -> Summary { ... }
// }
// Now you can implement it on the external type
// Use like any other entity
let mut state = new;
let config = json!;
let id = state.json_configs.create;
Because ForeignEntity is generated in your crate (not in stately), you can implement it on types from external crates without violating Rust's orphan rules. The macro creates wrapper types in the Entity enum that delegate to your ForeignEntity implementation, ensuring full compatibility with state operations.
🌐 Web API Generation (Axum)
Generate a complete REST API with OpenAPI documentation:
// Now in scope:
// - Trait implementations
// - All endpoints, response, request, and query types and ResponseEvent enum
// - `link_aliases` module
// - `impl AppState` with all state methods
async
Event Middleware for Persistence
The axum_api macro generates a ResponseEvent enum and event_middleware() method for integrating with databases:
use mpsc;
// Your event enum that wraps ResponseEvent
// Implement From<ResponseEvent> for ApiEvent
let = channel;
let app = new
.nest
.layer
.with_state;
// Background task to handle events
spawn;
The axum_api macro generates:
- ✅ Complete REST API handlers as methods on your struct
- ✅ OpenAPI 3.0 documentation (with
openapiparameter) - ✅ Type-safe request/response types
- ✅
router()method andApiState::openapi()for docs - ✅
ResponseEventenum andevent_middleware()for event-driven persistence
Macro Parameters
#[stately::state(openapi)]- Enables OpenAPI schema generation for entities#[stately::axum_api(State, openapi, components = [...])]- First parameter: The state type name
openapi: Enable OpenAPI documentation generationcomponents = [...]: Additional types to include in OpenAPI schemas (e.g., Link types)
Generated API Routes
The axum_api macro generates these endpoints:
PUT /- Create a new entityGET /- Get all entitiesGET /list- List all entities by summaryGET /list/{type}- List all entities filtered by type by summaryGET /{id}?type=<type>- Get entity by ID and typePOST /{id}- Update an existing entityPATCH /{id}- Patch an existing entityDELETE /{entry}/{id}- Delete an entity
OpenAPI Documentation
Access the generated OpenAPI spec:
use OpenApi;
let openapi = openapi;
let json = openapi.to_json.unwrap;
Feature Flags
| Feature | Description | Default |
|---|---|---|
openapi |
Enable OpenAPI schema generation via utoipa |
✅ Yes |
axum |
Enable Axum web framework integration | ❌ No |
Entity Attributes
The #[stately::entity] macro implements the HasName trait and supports these attributes:
// Default: uses the "name" field
// Use a different field for the entity name
// Use a method to get the name
// Mark as singleton (returns "default" as the name)
Examples
See the examples directory:
basic.rs- Core functionality demonstrationaxum_api.rs- Web API generation
Run examples:
API Reference
Core Types
Collection<T>- A collection of entities with CRUD operationsSingleton<T>- A single entity instanceLink<T>- Reference to another entity (by ID or inline)EntityId- UUID v7 identifier for entitiesSummary- Lightweight entity summary for listings
Traits
HasName- Trait for providing entity names (implemented by#[stately::entity])StateEntity- Trait for all entity types (implemented by#[stately::state])StateCollection- Trait for entity collections (implemented by#[stately::state])
Macros
#[stately::entity]- Implements theHasNametrait for an entity type#[stately::state]- Define application state with entity collections
Architecture
Stately uses procedural macros to generate boilerplate at compile time:
#[stately::entity]implements theHasNametrait#[stately::state]generates:StateEntryenum for entity type discriminationEntityenum for type-erased entity wrapper- Collection fields with type-safe accessors
- CRUD operation methods
link_aliasesmodule withLink<T>type aliases
#[stately::axum_api(State, ...)]generates (optional):- REST API handler methods on your struct
router()method for Axum integration- OpenAPI documentation (when
openapiparameter is used) ResponseEventenum for CRUD operationsevent_middleware()method for event streaming
All generated code is type-safe and benefits from Rust's compile-time guarantees.
Generated Code
link_aliases Module (from #[stately::state]):
ResponseEvent Enum (from #[stately::axum_api]):
These enable type-safe event-driven architectures for persistence, logging, and system integration.
License
Licensed under the Apache License, Version 2.0. See LICENSE for details.