1use 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 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 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 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}