stately 0.5.0

Type-safe state management with entity relationships and CRUD operations
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
# 🏰 Stately

[![Crates.io](https://img.shields.io/crates/v/stately.svg)](https://crates.io/crates/stately)
[![Documentation](https://docs.rs/stately/badge.svg)](https://docs.rs/stately)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://github.com/GeorgeLeePatterson/stately/tree/main/LICENSE)
[![Coverage](https://codecov.io/gh/GeorgeLeePatterson/stately/branch/main/graph/badge.svg)](https://codecov.io/gh/GeorgeLeePatterson/stately)

> 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 with `utoipa`
- 🆔 **Time-Sortable IDs** - UUID v7 for naturally ordered identifiers
- 🚀 **Web APIs** - Optional Axum integration with generated handlers (more frameworks coming soon)
- 🔍 **Search & Query** - Built-in entity search across collections
- 🌍 **Foreign Types** - Use types from external crates in your state

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 [@statelyjs/stately](https://www.npmjs.com/package/@statelyjs/stately).

## Install

Add to your `Cargo.toml`:

```toml
[dependencies]
stately = "0.5.0"
```

With Axum API generation:

```toml
[dependencies]
stately = { version = "0.5.0", features = ["axum"] }
```

## Quick Start

### Define Entities

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

```rust
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:

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

This generates:
- `StateEntry` enum for entity type discrimination
- `Entity` enum for type-erased entity access
- `ForeignEntity` trait for externally defined entities
- Wrappers for renamed entities and foreign entities
- Collections with full CRUD operations
- Search and query methods

### Collection Attributes

Use `#[collection(...)]` to customize how collections are generated:

```rust
// Can be a struct that implements `StateCollection`. This type alias is for simplicity.
type CustomStateCollectionImpl = Collection<SourceConfig>;

#[stately::state]
pub struct AppState {
    #[collection] // Default, same as omitting
    pipelines: Pipeline,

    #[collection(CustomStateCollectionImpl)]
    sources: SourceConfig,

    // variant = "..." sets the name used in the StateEntry and Entity enums
    // Useful when multiple collections use the same entity type
    #[collection(CustomStateCollectionImpl, variant = "CachedSourceConfig")]
    sources_cached: SourceConfig,

    // foreign allows using types from external crates
    #[collection(foreign)]
    configs: serde_json::Value,
}
```

Without `variant`, the macro generates enum variant names from the entity type name. Use `variant` to:
- Avoid naming collisions when using the same entity type in multiple collections
- Control the names in generated `StateEntry` and `Entity` enums
- Improve API clarity (e.g., `StateEntry::CachedSourceConfig` vs `StateEntry::SourceConfig`)

### Use the State

```rust
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())?;
```

## 📖 Examples

```rust
use stately::prelude::*;

// Define your entities
#[stately::entity]
#[derive(Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub struct Pipeline {
    pub name: String,
    pub source: Link<SourceConfig>,
    pub sink: Link<SinkConfig>,
}

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

#[stately::entity]
#[derive(Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub struct SinkConfig {
    pub name: String,
    pub destination: String,
}

// Define your application state
#[stately::state(openapi)]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct AppState {
    pipelines: Pipeline,
    sources: SourceConfig,
    sinks: SinkConfig,
}

fn main() {
    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(),
    });

    let sink_id = state.sinks.create(SinkConfig {
        name: "my-sink".to_string(),
        destination: "s3://my-bucket/output".to_string(),
    });

    // Create a pipeline referencing the source and sink
    let mut 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.clone());

    // Query entities
    if let Some((id, entity)) = state.get_entity(&pipeline_id.to_string(), StateEntry::Pipeline) {
        println!("Found pipeline: {:?}", entity);
    }

    // List all entities
    let summaries = state.list_entities(None);
    for (entry, entities) in summaries {
        println!("{}: {} entities", entry.as_ref(), entities.len());
    }

    // Update
    pipeline.name = "my-pipeline-updated".to_string();
    state.pipelines.update(&pipeline_id.to_string(), pipeline)?;

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

    // Search across collections
    let results = state.search_entities("pipeline");
}
```

See the [examples](https://github.com/GeorgeLeePatterson/stately/tree/main/crates/stately/examples/) directory:

- [`basic.rs`]https://github.com/GeorgeLeePatterson/stately/tree/main/crates/stately/examples/basic.rs - Core CRUD operations and entity relationships
- [`axum_api.rs`]https://github.com/GeorgeLeePatterson/stately/tree/main/crates/stately/examples/axum_api.rs - Web API generation with Axum
- [`doc_expand.rs`]https://github.com/GeorgeLeePatterson/stately/tree/main/crates/stately/examples/doc_expand.rs - Example used to generate documentation

Run examples:

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

## Entity Relationships with `Link<T>`

The `Link<T>` type allows flexible entity references:

```rust
// 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:

```rust
#[stately::entity]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Settings {
    pub max_connections: usize,
}

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

## Foreign Type Support

Stately allows you to use types from external crates (foreign types) in your state by using the `#[collection(foreign)]` attribute. This is useful for managing third-party types like configuration formats, API responses, or other external data structures.

When you mark a collection as `foreign`, the `#[stately::state]` macro generates a `ForeignEntity` trait in your crate that you can implement on external types:

```rust
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",
    "description": "A JSON configuration"
});
let id = state.json_configs.create(config);
```

Because `ForeignEntity` is generated in your crate (not in stately), you can implement it on types from external crates without violating Rust's orphan rules. The macro creates wrapper types in the `Entity` enum that delegate to your `ForeignEntity` implementation, ensuring full compatibility with state operations.

## 🌐 Web API Generation (Axum currently)

Generate a complete REST API with OpenAPI documentation:

```rust
#[stately::state(openapi)]
pub struct State {
    pipelines: Pipeline,
}

#[stately::axum_api(State, openapi(components = [link_aliases::PipelineLink]))]
pub struct ApiState {}

// Now in scope:
// - Trait implementations
// - All endpoints, response, request, and query types and ResponseEvent enum
// - `link_aliases` module
// - `impl AppState` with all state methods

#[tokio::main]
async fn main() {
    let app_state = ApiState::new(State::new());

    let app = axum::Router::new()
        .nest("/api/v1/entity", ApiState::router(app_state.clone()))
        .with_state(app_state);

    // Generated routes:
    // PUT    /api/v1/entity - Create entity
    // GET    /api/v1/entity - List all entities by StateEntry
    // GET    /api/v1/entity/list - List all entities as summaries
    // GET    /api/v1/entity/list/{type} - List all entities filtered by type as summaries
    // GET    /api/v1/entity/{id}?type=<type> - Get entity by ID
    // POST   /api/v1/entity/{id} - Update entity
    // PATCH  /api/v1/entity/{id} - Patch entity
    // DELETE /api/v1/entity/{entry}/{id} - Delete entity

    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:

```rust
use tokio::sync::mpsc;

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

// Implement From<ResponseEvent> for ApiEvent
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", ApiState::router(app_state.clone()))
    .layer(axum::middleware::from_fn(
        ApiState::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 {
            // Persist to database after state update
            ResponseEvent::Created { id, entity } => db.insert(id, entity).await,
            ResponseEvent::Updated { id, entity } => db.update(id, entity).await,
            ResponseEvent::Deleted { id, entry } => db.delete(id, entry).await,
        }
    }
});
```

The `axum_api` macro generates:
- ✅ Complete REST API handlers as methods on your struct
- ✅ OpenAPI 3.0 documentation (with `openapi` parameter)
- ✅ Type-safe request/response types
-`router()` method and `ApiState::openapi()` for docs
-`ResponseEvent` enum and `event_middleware()` for event-driven persistence

### 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
  - `openapi(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 /` - Get all entities
- `GET /list` - List all entities by summary
- `GET /list/{type}` - List all entities filtered by type by summary
- `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:

```rust
use utoipa::OpenApi;

#[stately::state(openapi)]
pub struct State {
    pipelines: Pipeline,
}

#[stately::axum_api(State, openapi(components = [link_aliases::PipelineLink]))]
pub struct ApiState {}

let openapi = ApiState::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 implements the `HasName` trait and supports these attributes:

```rust
// Default: uses the "name" field
#[stately::entity]

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

// Use a method to get the name
#[stately::entity(name_method = "get_identifier")]
```

## 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

- **`HasName`** - Trait for providing entity names (implemented by `#[stately::entity]`)
- **`StateEntity`** - Trait for all entity types (implemented by `#[stately::state]`)
- **`StateCollection`** - Trait for entity collections (implemented by `#[stately::state]`)

### Macros

- **`#[stately::entity]`** - Implements the `HasName` trait for 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 `HasName` trait
2. **`#[stately::state]`** generates:
   - `StateEntry` enum for entity type discrimination
   - `Entity` enum for type-erased entity wrapper
   - `ForeignEntity` for external entities
   - 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]`):

```rust
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]`):

```rust
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](https://github.com/GeorgeLeePatterson/stately/tree/main/LICENSE) for details.

## Links

- [Documentation]https://docs.rs/stately
- [Crates.io]https://crates.io/crates/stately
- [Repository]https://github.com/georgeleepatterson/stately
- [npm]https://www.npmjs.com/package/@statelyjs/stately