stately 0.2.0

Type-safe state management with entity relationships and CRUD operations
Documentation

🏰 Stately

Crates.io Documentation License Coverage

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
  • 🆔 Time-Sortable IDs - UUID v7 for naturally ordered identifiers
  • 🚀 Web APIs - Optional Axum integration with generated handlers

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:

[dependencies]
stately = "0.1"

With Axum API generation:

[dependencies]
stately = { version = "0.1", features = ["axum"] }

Quick Start

Define Entities

Use the #[stately::entity] macro to define your domain entities:

use stately::prelude::*;

#[stately::entity]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Pipeline {
    pub name: String,
    pub source: Link<SourceConfig>,
}

#[stately::entity]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct SourceConfig {
    pub name: String,
    pub url: String,
}

Define State

Use the #[stately::state] macro to create your application state:

#[stately::state]
pub struct AppState {
    pipelines: Pipeline,
    sources: SourceConfig,
}

This generates:

  • StateEntry enum for entity type discrimination
  • Entity enum for type-erased entity access
  • Collections with full CRUD operations
  • Search and query methods

Use the State

let mut state = AppState::new();

// Create entities
let source_id = state.sources.create(SourceConfig {
    name: "my-source".to_string(),
    url: "http://example.com".to_string(),
});

// Reference entities
let pipeline = Pipeline {
    name: "my-pipeline".to_string(),
    source: Link::create_ref(source_id.to_string()),
};

let pipeline_id = state.pipelines.create(pipeline);

// Query
let (id, entity) = state.get_entity(
    &pipeline_id.to_string(),
    StateEntry::Pipeline
).unwrap();

// List all
let summaries = state.list_entities(None);

// Search
let results = state.search_entities("my-pipeline");

// Update
state.pipelines.update(&pipeline_id.to_string(), updated_pipeline)?;

// Delete
state.pipelines.remove(&pipeline_id.to_string())?;

Entity Relationships with Link<T>

The Link<T> type allows flexible entity references:

// Reference by ID
let link = Link::create_ref("source-id-123");

// Inline embedding
let link = Link::inline(SourceConfig {
    name: "inline-source".to_string(),
    url: "http://example.com".to_string(),
});

// Access
match &pipeline.source {
    Link::Ref(id) => println!("References source: {}", id),
    Link::Inline(source) => println!("Inline source: {}", source.name),
}

Singleton Entities

For configuration that should have exactly one instance:

#[stately::entity(singleton, description = "Global settings")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Settings {
    pub max_connections: usize,
}

#[stately::state]
pub struct AppState {
    #[singleton]
    settings: Settings,
}

Web API Generation (Axum)

Generate a complete REST API with OpenAPI documentation:

#[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);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    axum::serve(listener, app).await.unwrap();
}

Event Middleware for Persistence

The axum_api macro generates a ResponseEvent enum and event_middleware() method for integrating with databases:

use tokio::sync::mpsc;

// Your event enum that wraps ResponseEvent
pub enum ApiEvent {
    StateEvent(ResponseEvent),
}

impl From<ResponseEvent> for ApiEvent {
    fn from(event: ResponseEvent) -> Self {
        ApiEvent::StateEvent(event)
    }
}

let (event_tx, mut event_rx) = mpsc::channel(100);

let app = axum::Router::new()
    .nest("/api/v1/entity", AppState::router(app_state.clone()))
    .layer(axum::middleware::from_fn(
        AppState::event_middleware::<ApiEvent>(event_tx)
    ))
    .with_state(app_state);

// Background task to handle events
tokio::spawn(async move {
    while let Some(ApiEvent::StateEvent(event)) = event_rx.recv().await {
        match event {
            ResponseEvent::Created { id, entity } => {
                // Persist to database after state update
                db.insert(id, entity).await.unwrap();
            }
            ResponseEvent::Updated { id, entity } => {
                db.update(id, entity).await.unwrap();
            }
            ResponseEvent::Deleted { id, entry } => {
                db.delete(id, entry).await.unwrap();
            }
        }
    }
});

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 generation
    • components = [...]: 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 entity
  • GET / - List all entities
  • GET /{id}?type=<type> - Get entity by ID and type
  • POST /{id} - Update an existing entity
  • PATCH /{id} - Patch an existing entity
  • DELETE /{entry}/{id} - Delete an entity

OpenAPI Documentation

Access the generated OpenAPI spec:

use utoipa::OpenApi;

let openapi = AppState::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 supports these attributes:

// Use a different field for the entity name
#[stately::entity(name_field = "title")]

// Mark as singleton (only one instance)
#[stately::entity(singleton)]

// Use a field for description
#[stately::entity(description_field = "info")]

// Use a static description
#[stately::entity(description = "A pipeline configuration")]

Examples

See the examples directory:

Run examples:

cargo run --example basic
cargo run --example axum_api --features axum

API Reference

Core Types

  • Collection<T> - A collection of entities with CRUD operations
  • Singleton<T> - A single entity instance
  • Link<T> - Reference to another entity (by ID or inline)
  • EntityId - UUID v7 identifier for entities
  • Summary - Lightweight entity summary for listings

Traits

  • StateEntity - Trait for all entity types (implemented by #[stately::entity])
  • StateCollection - Trait for entity collections (implemented by #[stately::state])
  • StatelyState - Trait for application state (implemented when using api = ["axum"])

Macros

  • #[stately::entity] - Define an entity type
  • #[stately::state] - Define application state with entity collections

Architecture

Stately uses procedural macros to generate boilerplate at compile time:

  1. #[stately::entity] implements the StateEntity trait
  2. #[stately::state] generates:
    • StateEntry enum for entity type discrimination
    • Entity enum for type-erased entity wrapper
    • Collection fields with type-safe accessors
    • CRUD operation methods
    • link_aliases module with Link<T> type aliases
  3. #[stately::axum_api(State)] generates (optional):
    • REST API handler methods on your struct
    • router() method for Axum integration
    • OpenAPI documentation (when openapi parameter is used)
    • ResponseEvent enum for CRUD operations
    • event_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]):

pub mod link_aliases {
    pub type PipelineLink = ::stately::Link<Pipeline>;
    pub type SourceLink = ::stately::Link<Source>;
    // ... one type alias for each entity in your state
}

ResponseEvent Enum (from #[stately::axum_api]):

pub enum ResponseEvent {
    Created { id: EntityId, entity: Entity },
    Updated { id: EntityId, entity: Entity },
    Deleted { id: EntityId, entry: StateEntry },
}

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.

Links