Crate stately

Crate stately 

Source
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
  • ๐Ÿ“š OpenAPI Schemas - Automatic schema generation with utoipa
  • ๐Ÿ†” 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:

  • StateEntry enum - discriminator for entity types
  • Entity enum - type-erased wrapper for all entities
  • link_aliases module - type aliases like PipelineLink = 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 router
  • ResponseEvent enum with Created, Updated, and Deleted variants
  • event_middleware() method for event-driven persistence
  • OpenAPI documentation when openapi parameter is specified

ยงGenerated Code Reference

ยงWhat the state Macro Generates

When you use #[stately::state] on your struct, the macro generates:

  1. StateEntry enum - Used to specify entity types in queries: rust,ignore pub enum StateEntry { Pipeline, Source, Sink, }

  2. Entity enum - Type-erased wrapper for all entity types: rust,ignore pub enum Entity { Pipeline(Pipeline), Source(SourceConfig), Sink(SinkConfig), }

  3. link_aliases module - Convenient type aliases for Link<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:

  1. 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()
  2. router() method - Returns configured Axum router with all routes

  3. ResponseEvent enum - 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 }, }

  4. event_middleware() method - Generic middleware for event-driven persistence:

    โ“˜
    Fn(...) + Clone where T: From<ResponseEvent> + Send + 'static ```
    
  5. OpenAPI trait (when openapi parameter used) - Implements utoipa::OpenApi

ยงFeature Flags

  • openapi (default) - Enable OpenAPI schema generation via utoipa
  • axum - Enable Axum web framework integration (implies openapi)

ยงExamples

See the examples directory for:

  • basic.rs - Core CRUD operations and entity relationships
  • axum_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 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

Attribute Macrosยง

entity
Derives the StateEntity trait for a struct.
state
Generates application state with entity collections.