actions_and_inputs/invoice/
mod.rs1use std::mem;
4use std::pin::Pin;
5use std::task::{Context, Poll};
6
7use app_kit::AppError;
8use app_kit::workflow::{
9 ActionBackend, ActionDeclaration, ActionEvent, ActionMode, ActionRequest, StateChange,
10 WorkflowState,
11};
12use futures_util::Stream;
13use serde_json::{Map, Value, json};
14
15pub(super) struct InvoiceAction {
16 declaration: ActionDeclaration,
17}
18
19pub(super) enum InvoiceSource {
20 Ready {
21 state: WorkflowState,
22 inputs: Map<String, Value>,
23 },
24 Completed {
25 result: Map<String, Value>,
26 total: u64,
27 },
28 Done,
29}
30
31impl InvoiceAction {
32 pub(super) fn new() -> Self {
33 Self {
34 declaration: ActionDeclaration {
35 key: "invoice".into(),
36 revision: "r1".into(),
37 reads: vec!["unit_price".into()],
38 writes: vec!["total".into()],
39 required_inputs: vec!["quantity".into()],
40 optional_inputs: vec!["label".into()],
41 bound_inputs: Map::from_iter([("currency".into(), json!("USD"))]),
42 default_inputs: Map::from_iter([("label".into(), json!("order"))]),
43 tags: vec!["billing".into()],
44 source: Some("invoice: unit_price * quantity; explicit total assignment".into()),
45 mode: ActionMode::Both,
46 },
47 }
48 }
49}
50
51impl ActionBackend for InvoiceAction {
52 type Stream = InvoiceSource;
53
54 fn declaration(&self) -> &ActionDeclaration {
55 &self.declaration
56 }
57
58 fn start(&self, request: ActionRequest) -> Self::Stream {
59 InvoiceSource::Ready {
60 state: request.state,
61 inputs: request.inputs,
62 }
63 }
64}
65
66impl InvoiceSource {
67 fn calculate(state: WorkflowState, inputs: Map<String, Value>) -> Result<Self, AppError> {
68 let price = state
69 .values()
70 .get("unit_price")
71 .and_then(Value::as_u64)
72 .ok_or_else(|| {
73 AppError::InvalidInput("unit_price must be an unsigned integer".into())
74 })?;
75 let quantity = inputs
76 .get("quantity")
77 .and_then(Value::as_u64)
78 .ok_or_else(|| AppError::InvalidInput("quantity must be an unsigned integer".into()))?;
79 let total = price
80 .checked_mul(quantity)
81 .ok_or_else(|| AppError::InvalidInput("invoice total overflowed".into()))?;
82 let mut result = inputs;
83 result.insert("total".into(), json!(total));
84 Ok(Self::Completed { result, total })
85 }
86}
87
88impl Stream for InvoiceSource {
89 type Item = Result<ActionEvent, AppError>;
90
91 fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
92 let source = self.get_mut();
93 Poll::Ready(match mem::replace(source, Self::Done) {
94 Self::Ready { state, inputs } => match Self::calculate(state, inputs) {
95 Ok(completed) => {
96 *source = completed;
97 Some(Ok(ActionEvent::Partial {
98 result: Map::from_iter([("progress".into(), json!("calculated"))]),
99 }))
100 }
101 Err(error) => Some(Err(error)),
102 },
103 Self::Completed { result, total } => Some(Ok(ActionEvent::Completed {
104 result,
105 writes: vec![StateChange::Assign {
106 values: Map::from_iter([("total".into(), json!(total))]),
107 }],
108 })),
109 Self::Done => None,
110 })
111 }
112}