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
ยงQuick Start
Define your entities using the [entity] macro:
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,
}The state macro generates:
StateEntryenum - discriminator for entity typesEntityenum - type-erased wrapper for all entitieslink_aliasesmodule - type aliases likePipelineLink = Link<Pipeline>
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())?;ยง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 event-driven
persistence:
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());
// 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)
))
.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 } => {
// Update in database
}
ResponseEvent::Deleted { id, entry } => {
// Delete from database
}
}
}
});
}The middleware is generic and can convert to your own event types:
enum MyEvent {
Api(ResponseEvent),
// ... other variants
}
impl From<ResponseEvent> for MyEvent {
fn from(event: ResponseEvent) -> Self {
MyEvent::Api(event)
}
}
// Use with your custom event type
let app = axum::Router::new()
.layer(axum::middleware::from_fn(
AppState::event_middleware::<MyEvent>(event_tx)
))
.with_state(app_state);The axum_api macro generates:
- Handler methods on your struct (
create_entity,list_entities, etc.) router()method returning configured Axum routerResponseEventenum with Created, Updated, and Deleted variantsevent_middleware()method for event-driven persistenceOpenAPIdocumentation whenopenapiparameter is specified
ยงGenerated Code Reference
ยงWhat the state Macro Generates
When you use #[stately::state] on your struct, the macro generates:
-
StateEntryenum - Used to specify entity types in queries:rust,ignore pub enum StateEntry { Pipeline, Source, Sink, } -
Entityenum - Type-erased wrapper for all entity types:rust,ignore pub enum Entity { Pipeline(Pipeline), Source(SourceConfig), Sink(SinkConfig), } -
link_aliasesmodule - Convenient type aliases forLink<T>:rust,ignore pub mod link_aliases { pub type PipelineLink = ::stately::Link<Pipeline>; pub type SourceLink = ::stately::Link<SourceConfig>; pub type SinkLink = ::stately::Link<SinkConfig>; }
ยง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:
create_entity(),list_entities(),get_entity_by_id()update_entity(),patch_entity_by_id(),remove_entity()
-
router()method - Returns configured Axum router with all routes -
ResponseEventenum - Events emitted after CRUD operations:rust,ignore pub enum ResponseEvent { Created { id: EntityId, entity: Entity }, Updated { id: EntityId, entity: Entity }, Deleted { id: EntityId, entry: StateEntry }, } -
event_middleware()method - Generic middleware for event-driven persistence:โFn(...) + Clone where T: From<ResponseEvent> + Send + 'static ``` -
OpenAPItrait (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 Axum
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::StateCollection;pub use traits::StateEntity;pub use hashbrown;
Modulesยง
- collection
- Collection and Singleton types for managing entities
- 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