Expand description
Β§Stately
Type-safe state management with entity relationships and CRUD operations.
Β§Overview
Stately is a framework for managing application configuration and state with built-in support for:
- π Entity Relationships - Reference entities inline or by ID using
Link<T> - π CRUD Operations - Full create, read, update, delete for all entity types
- π Serialization - Complete serde support for JSON, YAML, and more
- π
OpenAPISchemas - Automatic schema generation withutoipa - π Time-Sortable IDs - UUID v7 for naturally ordered entity identifiers
- π Web APIs - Optional Axum integration with generated REST handlers
- π Search & Query - Built-in entity search across collections
- π Foreign Types - Use types from external crates in your state
Β§Quick Start
Define your entities using the entity macro. Itβs not strictly necessary, but
conviently implements the HasName trait:
use stately::prelude::*;
#[stately::entity]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Pipeline {
pub name: String,
pub source: Link<SourceConfig>,
pub sink: Link<SinkConfig>,
}
#[stately::entity]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct SourceConfig {
pub name: String,
pub url: String,
}
#[stately::entity]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct SinkConfig {
pub name: String,
pub destination: String,
}Create your application state using the state macro:
#[stately::state]
pub struct AppState {
pipelines: Pipeline,
sources: SourceConfig,
sinks: SinkConfig,
}Use your state with full type safety:
let mut state = AppState::new();
// Create entities
let source_id = state.sources.create(SourceConfig {
name: "my-source".to_string(),
url: "http://example.com/data".to_string(),
});
// Reference entities by ID
let pipeline = Pipeline {
name: "my-pipeline".to_string(),
source: Link::create_ref(source_id.to_string()),
sink: Link::create_ref(sink_id.to_string()),
};
let pipeline_id = state.pipelines.create(pipeline);
// Query entities
let (id, entity) = state.get_entity(&pipeline_id.to_string(), StateEntry::Pipeline).unwrap();
// List all entities
let summaries = state.list_entities(None);
// Search entities
let results = state.search_entities("pipeline");
// Update entities
state.pipelines.update(&pipeline_id.to_string(), updated_pipeline)?;
// Delete entities
state.pipelines.remove(&pipeline_id.to_string())?;Β§Foreign Type Support
Use types from external crates in your state with the #[collection(foreign)] attribute.
When you mark a collection as foreign, the #[stately::state] macro generates a
ForeignEntity trait in your crate that you implement on the
external type:
use serde_json::Value;
#[stately::state]
pub struct AppState {
#[collection(foreign, variant = "JsonConfig")]
json_configs: 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
impl ForeignEntity for Value {
fn name(&self) -> &str {
self.get("name").and_then(|v| v.as_str()).unwrap_or("unnamed")
}
fn description(&self) -> Option<&str> {
self.get("description").and_then(|v| v.as_str())
}
}
// Use like any other entity
let mut state = AppState::new();
let config = serde_json::json!({"name": "my-config"});
let id = state.json_configs.create(config);Because ForeignEntity is generated in your crate, you can
implement it on types from external crates without violating Rustβs orphan rules. The macro
creates wrapper types that delegate to your ForeignEntity implementation.
Β§Web API Generation
Generate complete REST APIs with OpenAPI documentation using the axum feature:
#[stately::state(openapi)]
pub struct State {
pipelines: Pipeline,
}
#[stately::axum_api(State, openapi, components = [link_aliases::PipelineLink])]
pub struct AppState {}
#[tokio::main]
async fn main() {
let app_state = AppState::new(State::new());
let app = axum::Router::new()
.nest("/api/v1/entity", AppState::router(app_state.clone()))
.with_state(app_state);
// Routes automatically generated:
// PUT /api/v1/entity - Create entity
// GET /api/v1/entity - List entities
// GET /api/v1/entity/{id}?type=<type> - Get entity
// POST /api/v1/entity/{id} - Update entity
// PATCH /api/v1/entity/{id} - Patch entity
// DELETE /api/v1/entity/{entry}/{id} - Delete entity
// OpenAPI spec available at:
let openapi = AppState::openapi();
}Β§Event-Driven Persistence
The axum_api macro generates a ResponseEvent enum and
middleware for crud events. The middleware is generic and can convert to your own event types:
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (event_tx, mut event_rx) = mpsc::channel(100);
let app_state = AppState::new(State::new());
enum MyEvent {
Api(ResponseEvent),
// ... other variants
}
impl From<ResponseEvent> for MyEvent {
fn from(event: ResponseEvent) -> Self { MyEvent::Api(event) }
}
// Attach event middleware - handlers emit events after state updates
let app = axum::Router::new()
.nest("/api/v1/entity", AppState::router(app_state.clone()))
.layer(axum::middleware::from_fn(AppState::event_middleware(event_tx)))
.layer(axum::middleware::from_fn(AppState::event_middleware::<MyEvent>(event_tx)))
.with_state(app_state);
// Handle events in background task
tokio::spawn(async move {
while let Some(event) = event_rx.recv().await {
match event {
ResponseEvent::Created { id, entity } => { /* Persist to database */ }
ResponseEvent::Updated { id, entity } => { /* Persist to database */ }
ResponseEvent::Deleted { id, entry } => { /* Persist to database */ }
}
}
});
}Β§Generated Code Reference
To see a comprehensive demonstration of the code generated by the macros, refer to the
demo module. It is derived from the doc_expand example
Β§What the state Macro Generates
When you use #[stately::state] on your struct, the macro generates:
StateEntryenum - Used to specify entity types in queriesEntityenum - Type-erased wrapper for all entity typeslink_aliasesmodule - Convenient type aliases forLink<T>ForeignEntitytrait - Trait for entities in external crates.
Β§What the axum_api Macro Generates
When you use #[stately::axum_api(State)], the macro generates:
- Handler methods - REST API handlers as methods on your struct:
ApiState::router()- Returns Axum router with all routes configuredResponseEventenum - Events emitted after CRUD operations:ApiState::event_middleware()method - Generic middleware for crudOpenAPItrait (whenopenapiparameter used) - Implementsutoipa::OpenApi
Β§Feature Flags
openapi(default) - EnableOpenAPIschema generation viautoipaaxum- Enable Axum web framework integration (impliesopenapi)
Β§Examples
See the examples directory for:
basic.rs- Core CRUD operations and entity relationshipsaxum_api.rs- Web API generation with Axumdoc_expand.rs- Example used to generatedemofor reference
Re-exportsΒ§
pub use collection::Collection;pub use collection::Singleton;pub use entity::EntityId;pub use entity::Summary;pub use error::Error;pub use error::Result;pub use link::Link;pub use traits::HasName;pub use traits::StateCollection;pub use traits::StateEntity;pub use hashbrown;pub use tokio;
ModulesΒ§
- collection
- Collection and Singleton types for managing entities
- demo
- Auto-generated code examples showing what the stately macros generate.
- entity
- Entity types and identifiers
- error
- Error types for stately
- link
- Entity linking - reference entities inline or by ID
- prelude
- Prelude module for convenient imports
- traits
- Core traits for state management