Skip to main content

actions_and_inputs/invoice/
mod.rs

1//! The invoice action shared by the action and execution-control examples.
2
3use 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}