stepflow_action/
action.rs

1use stepflow_base::{ObjectStoreContent, ObjectStoreFiltered, generate_id_type, IdError};
2use stepflow_data::{StateData, StateDataFiltered, value::Value, var::{Var, VarId}};
3use stepflow_step::{Step};
4use crate::ActionError;
5
6mod action_string_template;
7pub use action_string_template::StringTemplateAction;
8
9mod action_htmlform;
10pub use action_htmlform::{HtmlFormAction, HtmlFormConfig};
11
12mod action_set_data;
13pub use action_set_data::SetDataAction;
14
15generate_id_type!(ActionId);
16
17/// The result of [`Action::start()`]
18#[derive(Debug, Clone)]
19pub enum ActionResult {
20  /// The action requires the caller to fulfill the [`Step`](stepflow_step::Step)'s outputs.
21  /// The value's meaning is [`Action`] dependent.
22  /// When the caller obtains the output data (i.e. with a form), it can then advance the `Session`.
23  /// ```
24  /// # use stepflow_action::ActionResult;
25  /// # use stepflow_data::value::StringValue;
26  /// # fn respond_with_redirect(uri: &StringValue) {}
27  /// # let action_result = ActionResult::StartWith(StringValue::try_new("name-form").unwrap().boxed());
28  /// if let ActionResult::StartWith(uri) = action_result {
29  ///   respond_with_redirect(uri.downcast::<StringValue>().unwrap())
30  /// }
31  /// ```
32  StartWith(Box<dyn Value>),
33
34  /// The action fulfilled the ouputs with the results in the [`StateData`].
35  Finished(StateData),
36
37  /// The action was not able to fulfill the ouputs as a result of a normal condition
38  /// such as a minimum time duration. This should not be used for error situations.
39  CannotFulfill,
40}
41
42impl PartialEq for ActionResult {
43    fn eq(&self, other: &Self) -> bool {
44      match (self, other) {
45        (ActionResult::StartWith(val), ActionResult::StartWith(val_other)) => {
46          val == val_other
47        },
48        (ActionResult::Finished(data), ActionResult::Finished(data_other)) => {
49          data == data_other
50        },
51        (ActionResult::CannotFulfill, ActionResult::CannotFulfill) => {
52          true
53        },
54        (ActionResult::StartWith(_), _) |
55        (ActionResult::Finished(_), _) |
56        (ActionResult::CannotFulfill, _) => {
57          false
58        },
59      }
60    }
61}
62
63/// `Action`s fulfill the outputs of a [`Step`]
64pub trait Action: std::fmt::Debug + stepflow_base::as_any::AsAny {
65  /// Get the ID for the Action
66  fn id(&self) -> &ActionId;
67
68  /// Start the action for a [`Step`]
69  ///
70  /// `step_data` and `vars` only have access to input and output data declared by the Step.
71  fn start(&mut self, step: &Step, step_name: Option<&str>, step_data: &StateDataFiltered, vars: &ObjectStoreFiltered<Box<dyn Var + Send + Sync>, VarId>)
72    -> Result<ActionResult, ActionError>;
73}
74
75// implement downcast helpers that have trait bounds to make it a little safer
76impl dyn Action + Send + Sync {
77  pub fn downcast<T>(&self) -> Option<&T>
78    where T: Action + std::any::Any
79  {
80    self.as_any().downcast_ref::<T>()
81  }
82  pub fn is<T>(&self) -> bool 
83    where T: Action + std::any::Any
84  {
85    self.as_any().is::<T>()
86  }
87}
88
89impl ObjectStoreContent for Box<dyn Action + Sync + Send> {
90    type IdType = ActionId;
91
92    fn new_id(id_val: u16) -> Self::IdType {
93      ActionId::new(id_val)
94    }
95
96    fn id(&self) -> &Self::IdType {
97      self.as_ref().id()
98    }
99}
100
101// setup for useful vars for testing.
102#[cfg(test)]
103pub fn test_action_setup<'a>() -> (Step, StateData, stepflow_base::ObjectStore<Box<dyn Var + Send + Sync>, VarId>, VarId, Box<dyn stepflow_data::value::Value>) {
104  // setup var
105  let mut var_store: stepflow_base::ObjectStore<Box<dyn Var + Send + Sync>, VarId> = stepflow_base::ObjectStore::new();
106  let var_id = var_store.insert_new(|id| Ok(stepflow_data::var::StringVar::new(id).boxed())).unwrap();
107  let var = var_store.get(&var_id).unwrap();
108
109  // set a value in statedata
110  let state_val = stepflow_data::value::StringValue::try_new("hi").unwrap().boxed();
111  let mut state_data = StateData::new();
112  state_data.insert(var, state_val.clone()).unwrap();
113
114  let step = Step::new(stepflow_step::StepId::new(2), None, vec![]);
115  (step, state_data, var_store, var_id, state_val)
116}
117
118#[cfg(test)]
119mod tests {
120  use stepflow_test_util::test_id;
121  use stepflow_data::{StateData, value::TrueValue};
122  use super::{ActionId, HtmlFormAction, SetDataAction, ActionResult};
123
124  #[test]
125  fn eq() {
126    let result_start = ActionResult::StartWith(TrueValue::new().boxed());
127    let result_finish = ActionResult::Finished(StateData::new());
128    let result_cannot = ActionResult::CannotFulfill;
129
130    assert_eq!(result_start, result_start);
131    assert_ne!(result_start, result_finish);
132    assert_ne!(result_start, result_cannot);
133
134    assert_eq!(result_finish, result_finish);
135    assert_ne!(result_finish, result_cannot);
136  }
137
138  #[test]
139  fn downcast() {
140    let action = HtmlFormAction::new(test_id!(ActionId), Default::default()).boxed();
141    assert!(action.is::<HtmlFormAction>());
142    assert!(!action.is::<SetDataAction>());
143  }
144}