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
  • 🌍 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:

  1. StateEntry enum - Used to specify entity types in queries
  2. Entity enum - Type-erased wrapper for all entity types
  3. link_aliases module - Convenient type aliases for Link<T>
  4. ForeignEntity trait - Trait for entities in external crates.

Β§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:
  2. ApiState::router() - Returns Axum router with all routes configured
  3. ResponseEvent enum - Events emitted after CRUD operations:
  4. ApiState::event_middleware() method - Generic middleware for crud
  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
  • doc_expand.rs - Example used to generate demo for 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 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

Attribute MacrosΒ§

axum_api
Generates Axum API integration for a state wrapper struct.
entity
Implements the HasName trait for an entity struct.
state
Generates application state with entity collections.