Skip to main content

state/
state.rs

1//! Immutable state changes, local typed data, and explicit JSON codecs.
2//! Run with `cargo run -p app-kit --example state`.
3
4use std::rc::Rc;
5
6use app_kit::AppError;
7use app_kit::workflow::{StateChange, StateSerialization, StateTyping, WorkflowState};
8use serde_json::{Map, Value, json};
9
10struct CountData {
11    count: i64,
12    labels: Vec<Value>,
13}
14
15struct CountPolicy {
16    maximum: i64,
17}
18
19impl StateTyping for CountPolicy {
20    // The behavior owner is Send + Sync. Its locally constructed Data need not be.
21    type Data = Rc<CountData>;
22
23    fn construct_data(&self, state: &WorkflowState) -> Result<Self::Data, AppError> {
24        let count = state.values().get("count").and_then(Value::as_i64);
25        let labels = state.values().get("labels").and_then(Value::as_array);
26        match (count, labels) {
27            (Some(count), Some(labels)) => Ok(Rc::new(CountData {
28                count,
29                labels: labels.clone(),
30            })),
31            _ => Err(AppError::InvalidInput(
32                "count and labels have invalid types".into(),
33            )),
34        }
35    }
36
37    fn construct_state(&self, data: &Self::Data) -> Result<WorkflowState, AppError> {
38        WorkflowState::new(Map::from_iter([
39            ("count".into(), json!(data.count)),
40            ("labels".into(), json!(data.labels)),
41        ]))
42    }
43
44    fn validate_state(&self, state: &WorkflowState) -> Result<(), AppError> {
45        let data = self.construct_data(state)?;
46        if !(0..=self.maximum).contains(&data.count) {
47            return Err(AppError::InvalidInput(
48                "count is outside its allowed range".into(),
49            ));
50        }
51        Ok(())
52    }
53}
54
55fn main() -> Result<(), AppError> {
56    let policy = CountPolicy { maximum: 100 };
57    let initial = WorkflowState::new(Map::from_iter([
58        ("count".into(), json!(1)),
59        ("labels".into(), json!([])),
60    ]))?;
61    let counted = initial.apply(
62        &StateChange::Increment {
63            values: vec![("count".into(), json!(3)), ("count".into(), json!(4))],
64        },
65        &policy,
66    )?;
67    let labeled = counted.apply(
68        &StateChange::Append {
69            values: vec![
70                ("labels".into(), json!("draft")),
71                ("labels".into(), json!("queued")),
72            ],
73        },
74        &policy,
75    )?;
76    assert_eq!(initial.values()["count"], 1);
77    assert_eq!(labeled.values()["count"], 8);
78    assert_eq!(labeled.values()["labels"], json!(["draft", "queued"]));
79
80    // A later invalid entry rejects the complete operation, including count + 1.
81    let rejected = labeled.apply(
82        &StateChange::Increment {
83            values: vec![("count".into(), json!(1)), ("labels".into(), json!(1))],
84        },
85        &policy,
86    );
87    assert!(matches!(rejected, Err(AppError::InvalidInput(_))));
88    assert_eq!(labeled.values()["count"], 8);
89
90    // This registration is process-global. This standalone example owns its
91    // process; an application should choose registrations during its setup.
92    StateSerialization::global().register_field(
93        "labels".into(),
94        |value| Ok::<_, &'static str>(json!({"version": 1, "items": value})),
95        |value| value.get("items").cloned().ok_or("missing labels.items"),
96    )?;
97    let encoded = labeled.serialize()?;
98    assert_eq!(
99        encoded["labels"],
100        json!({"version":1,"items":["draft","queued"]})
101    );
102    let decoded = WorkflowState::deserialize(encoded.clone())?;
103    assert_eq!(decoded, labeled);
104
105    StateSerialization::global().register_tag("example-money".into(), |value| {
106        value.get("minor").cloned().ok_or("missing money.minor")
107    })?;
108    let tagged = WorkflowState::deserialize(Map::from_iter([(
109        "amount".into(),
110        json!({"__burr_serde__":"example-money","minor":120}),
111    )]))?;
112    assert_eq!(tagged.values()["amount"], 120);
113    println!("{}", Value::Object(encoded));
114    Ok(())
115}