Mistakes caught before a journey
What you want to do
The shop's workflow is now put together from many parts: steps and their names, policies attached to them, an input adapter, services and their providers, a rescue step and roles. You want to see how it is put together without running an order, and to know that a mistake in putting it together is caught before any order runs.
The code
How the workflow is put together
The shop is the one chapter 36 runs; its workflow is the one chapter 23 declares, with chapter 35's provider for cards. main prints its listing, read from the workflow's declaration, one line for each step, with Workflow from itinera::workflow in scope:
for listed in Orders::descriptor().listing() {
println!("{}", entry(&listed));
}entry writes each step's line from what the listing says about it, and services, with service for each one, writes the services of a step or of its rescue step:
/// A service in the listing: its key, and the provider that serves it.
fn service(listed: &ListedService) -> String {
format!("{} from {}", listed.key, listed.provider)
}
/// The services of a listing's entry, separated by commas.
fn services(listed: &[ListedService]) -> String {
listed.iter().map(service).collect::<Vec<_>>().join(", ")
}
/// One entry of the listing: a step's position and name, its policies, its adapter, its services
/// with the provider that serves each, and its rescue step with its services.
fn entry(listed: &ListedStep) -> String {
let mut entry = format!("{}. {}", listed.position, listed.step);
if !listed.policies.is_empty() {
let policies: Vec<String> = listed.policies.iter().map(ToString::to_string).collect();
entry.push_str(&format!(", with {}", policies.join(", ")));
}
if let Some(adapter) = &listed.adapter {
entry.push_str(&format!("; adapted by {adapter}"));
}
if !listed.services.is_empty() {
entry.push_str(&format!("; services: {}", services(&listed.services)));
}
if let Some(rescue) = &listed.rescue {
entry.push_str(&format!(
"; rescued by {}, services: {}",
rescue.step,
services(&rescue.services)
));
}
entry
}It prints:
1. lookup, with already_completed; services: order_book from ledger
2. price
3. charge, with audit, outage; services: payments from bank; rescued by charge_rescue, services: cards from cards
4. ship, with failed_shipping; adapted by parcels; services: carrier from carriersMistakes that don't compile
Each program below is a small shop with one mistake, and none of them compiles. The shipping listed twice:
//! Two steps of one workflow under the same name.
use itinera::error::Error;
use itinera::executor::local_executor_for;
use itinera::step::{Outcome, Step, step};
use itinera::workflow::workflow;
/// Charges an order.
#[derive(Step)]
struct Charge;
#[step]
impl Charge {
fn run(self) -> Result<Outcome, Error> {
Self::succeed()
}
}
/// Ships an order through the carrier.
#[derive(Step)]
struct Ship;
#[step]
impl Ship {
fn run(self) -> Result<Outcome, Error> {
Self::succeed()
}
}
/// The shop's workflow, with `ship` listed twice.
#[workflow(name = "orders")]
#[with_step(Charge, name = "charge")]
#[with_step(Ship, name = "ship")]
#[with_step(Ship, name = "ship", retry_budget = 1)]
struct Orders;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = local_executor_for!(Orders).run(Orders::builder().create()?)?;
println!("the journey {}", result.status.kind());
Ok(())
}error: duplicate step name: ship, which an earlier `#[with_step]` line already gives
--> compile-errors/ch37_duplicate_step_name.rs:34:26
|
34 | #[with_step(Ship, name = "ship", retry_budget = 1)]
| ^^^^^^Two policies of the charge that both define on step success:
//! Two policies of one step that both define `on step success`.
use itinera::error::Error;
use itinera::executor::local_executor_for;
use itinera::journey::Contributor;
use itinera::policy::{OnSuccess, step_policy};
use itinera::step::{Outcome, Step, step};
use itinera::workflow::workflow;
/// Charges an order.
#[derive(Step)]
struct Charge;
#[step]
impl Charge {
fn run(self, #[contributor] mut contributor: Contributor<'_>) -> Result<Outcome, Error> {
contributor.contribute("charged", 40);
Self::succeed()
}
}
/// Audits every successful charge.
#[derive(Default)]
struct Audit;
#[step_policy(name = "audit")]
impl Audit {
#[on_step_success]
fn audited(&self, #[from_step] charged: i64) -> Result<Option<OnSuccess>, Error> {
println!("audit: charged {charged}");
Self::carry_on()
}
}
/// Writes a receipt for every successful charge.
#[derive(Default)]
struct Receipts;
#[step_policy(name = "receipts")]
impl Receipts {
#[on_step_success]
fn written(&self, #[from_step] charged: i64) -> Result<Option<OnSuccess>, Error> {
println!("receipt: R-{charged}");
Self::carry_on()
}
}
/// The shop's workflow, with both policies attached to the charge.
#[workflow(name = "orders")]
#[with_step(Charge, name = "charge", policies = [Audit, Receipts])]
struct Orders;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = local_executor_for!(Orders).run(Orders::builder().create()?)?;
println!("the journey {}", result.status.kind());
Ok(())
}error[E0080]: evaluation panicked: hook defined twice on step charge: `on step success`, which two of its policies define
--> compile-errors/ch37_hook_defined_twice.rs:50:57
|
50 | #[with_step(Charge, name = "charge", policies = [Audit, Receipts])]
| ^^^^^^^^ evaluation of `_::_` failed hereA policy that tells the customer through the Messages role, on a workflow that doesn't provide it:
//! A policy that needs a role the workflow does not provide.
use itinera::error::Error;
use itinera::executor::local_executor_for;
use itinera::policy::workflow_policy;
use itinera::step::{Outcome, Step, step};
use itinera::workflow::workflow;
/// Charges an order.
#[derive(Step)]
struct Charge;
#[step]
impl Charge {
fn run(self) -> Result<Outcome, Error> {
Self::succeed()
}
}
/// The shop's way of telling a customer something.
trait Messages {
fn tell(&self, customer: &str, message: &str);
}
/// Tells the customer once the order is complete.
#[derive(Default)]
struct Notify;
#[workflow_policy(name = "notify")]
impl Notify {
#[on_workflow_success]
fn completed(
&self,
#[from_workflow] customer: String,
#[role] messages: &dyn Messages,
) -> Result<(), Error> {
messages.tell(&customer, "your order is complete");
Ok(())
}
}
/// The shop's workflow. It can tell a customer something, but does not provide it as a role.
#[workflow(name = "orders", policies = [Notify])]
#[with_step(Charge, name = "charge")]
struct Orders;
impl Messages for Orders {
fn tell(&self, customer: &str, message: &str) {
println!("to {customer}: {message}");
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = local_executor_for!(Orders).run(Orders::builder().create()?)?;
println!("the journey {}", result.status.kind());
Ok(())
}error[E0277]: the workflow `Orders` does not provide the role `(dyn Messages + 'static)`
--> compile-errors/ch37_role_not_provided.rs:43:41
|
43 | #[workflow(name = "orders", policies = [Notify])]
| ^^^^^^ this workflow does not provide the role
|
help: the trait `Provides<(dyn Messages + 'static)>` is not implemented for `Orders`
--> compile-errors/ch37_role_not_provided.rs:45:1
|
45 | struct Orders;
| ^^^^^^^^^^^^^
= note: a workflow provides a role by implementing `Provides<(dyn Messages + 'static)>`, which `#[role]` does on its implementation of the role's trait
help: the trait `itinera::policy::WorkflowPolicy<__ItineraWorkflow>` is implemented for `Notify`
--> compile-errors/ch37_role_not_provided.rs:29:1
|
29 | #[workflow_policy(name = "notify")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: required for `Notify` to implement `itinera::policy::WorkflowPolicy<Orders>`
--> compile-errors/ch37_role_not_provided.rs:29:1
|
29 | #[workflow_policy(name = "notify")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound introduced here
30 | impl Notify {
| ^^^^^^
= note: this error originates in the attribute macro `workflow_policy` (in Nightly builds, run with -Z macro-backtrace for more info)An on step failure hook that tries to finish the journey as succeeded, which only on step success may do:
//! A hook that returns a lifecycle it may not return: finishing the journey after a failure.
use itinera::error::Error;
use itinera::executor::local_executor_for;
use itinera::policy::{FailWorkflow, step_policy};
use itinera::step::{Outcome, Step, step};
use itinera::workflow::workflow;
/// Charges an order, and fails when there is nothing to charge.
#[derive(Step)]
struct Charge;
#[step]
impl Charge {
fn run(self) -> Result<Outcome, Error> {
Self::fail("nothing-to-charge")
}
}
/// Tries to end the journey as succeeded when the charge fails.
#[derive(Default)]
struct Forgive;
#[step_policy(name = "forgive")]
impl Forgive {
#[on_step_failure]
fn forgiven(&self) -> Result<Option<FailWorkflow>, Error> {
Self::finish_workflow()
}
}
/// The shop's workflow.
#[workflow(name = "orders")]
#[with_step(Charge, name = "charge", policies = [Forgive])]
struct Orders;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = local_executor_for!(Orders).run(Orders::builder().create()?)?;
println!("the journey {}", result.status.kind());
Ok(())
}error[E0308]: mismatched types
--> compile-errors/ch37_invalid_lifecycle.rs:28:9
|
27 | fn forgiven(&self) -> Result<Option<FailWorkflow>, Error> {
| ----------------------------------- expected `Result<Option<itinera::policy::FailWorkflow>, itinera::error::Error>` because of return type
28 | Self::finish_workflow()
| ^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<Option<FailWorkflow>, Error>`, found `Result<Option<OnSuccess>, Error>`
|
= note: expected enum `Result<Option<itinera::policy::FailWorkflow>, _>`
found enum `Result<Option<OnSuccess>, _>`A step that contributes its connection to the bank, which isn't a value:
//! A step that contributes something that is not a value: its connection to the bank.
use itinera::error::Error;
use itinera::executor::local_executor_for;
use itinera::journey::Contributor;
use itinera::step::{Outcome, Step, step};
use itinera::workflow::workflow;
/// A connection to the customer's bank, faked.
struct BankConnection {
bank: &'static str,
}
/// Charges an order, and hands its bank connection on.
#[derive(Step)]
struct Charge;
#[step]
impl Charge {
fn run(self, #[contributor] mut contributor: Contributor<'_>) -> Result<Outcome, Error> {
let connection = BankConnection {
bank: "the customer's bank",
};
contributor.contribute("bank", connection);
Self::succeed()
}
}
/// The shop's workflow.
#[workflow(name = "orders")]
#[with_step(Charge, name = "charge")]
struct Orders;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = local_executor_for!(Orders).run(Orders::builder().create()?)?;
println!("the journey {}", result.status.kind());
Ok(())
}error[E0277]: the trait bound `BankConnection: Value` is not satisfied
--> compile-errors/ch37_not_a_value.rs:24:40
|
24 | contributor.contribute("bank", connection);
| ---------- ^^^^^^^^^^ unsatisfied trait bound
| |
| required by a bound introduced by this call
|
help: the trait `serde_core::ser::Serialize` is not implemented for `BankConnection`
--> compile-errors/ch37_not_a_value.rs:10:1
|
10 | struct BankConnection {
| ^^^^^^^^^^^^^^^^^^^^^
= help: the following other types implement trait `serde_core::ser::Serialize`:
&'a T
&'a mut T
()
(T,)
(T0, T1)
(T0, T1, T2)
(T0, T1, T2, T3)
(T0, T1, T2, T3, T4)
and 131 others
= note: required for `BankConnection` to implement `Value`
note: required by a bound in `Contributor::<'a>::contribute`
--> itinera-rs/crates/itinera-core/src/journey/contributor.rs:66:26
|
66 | pub fn contribute<T: Value>(&mut self, key: impl Into<String>, value: T) {
| ^^^^^ required by this bound in `Contributor::<'a>::contribute`
error[E0277]: the trait bound `BankConnection: Value` is not satisfied
--> compile-errors/ch37_not_a_value.rs:24:40
|
24 | contributor.contribute("bank", connection);
| ---------- ^^^^^^^^^^ the trait `Clone` is not implemented for `BankConnection`
| |
| required by a bound introduced by this call
|
= note: required for `BankConnection` to implement `Value`
note: required by a bound in `Contributor::<'a>::contribute`
--> itinera-rs/crates/itinera-core/src/journey/contributor.rs:66:26
|
66 | pub fn contribute<T: Value>(&mut self, key: impl Into<String>, value: T) {
| ^^^^^ required by this bound in `Contributor::<'a>::contribute`
help: consider annotating `BankConnection` with `#[derive(Clone)]`
|
10 + #[derive(Clone)]
11 | struct BankConnection {
|
error[E0277]: the trait bound `BankConnection: Value` is not satisfied
--> compile-errors/ch37_not_a_value.rs:24:40
|
24 | contributor.contribute("bank", connection);
| ---------- ^^^^^^^^^^ unsatisfied trait bound
| |
| required by a bound introduced by this call
|
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `BankConnection`
--> compile-errors/ch37_not_a_value.rs:10:1
|
10 | struct BankConnection {
| ^^^^^^^^^^^^^^^^^^^^^
= help: the following other types implement trait `serde_core::de::Deserialize<'de>`:
&'a Path
&'a [u8]
&'a str
()
(T,)
(T0, T1)
(T0, T1, T2)
(T0, T1, T2, T3)
and 138 others
= note: required for `BankConnection` to implement `serde_core::de::DeserializeOwned`
= note: required for `BankConnection` to implement `Value`
note: required by a bound in `Contributor::<'a>::contribute`
--> itinera-rs/crates/itinera-core/src/journey/contributor.rs:66:26
|
66 | pub fn contribute<T: Value>(&mut self, key: impl Into<String>, value: T) {
| ^^^^^ required by this bound in `Contributor::<'a>::contribute`An asynchronous workflow whose journey is run without being awaited:
//! An asynchronous workflow whose journey is run without awaiting it.
use itinera::error::Error;
use itinera::executor::local_executor_for;
use itinera::step::{Outcome, Step, step};
use itinera::workflow::workflow;
/// Charges an order, asynchronously.
#[derive(Step)]
struct Charge;
#[step]
impl Charge {
async fn run(self) -> Result<Outcome, Error> {
Self::succeed()
}
}
/// The shop's workflow, asynchronous because its steps are.
#[workflow(name = "orders")]
#[with_step(Charge, name = "charge")]
struct Orders;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = local_executor_for!(Orders).run(Orders::builder().create()?)?;
println!("the journey {}", result.status.kind());
Ok(())
}error[E0277]: the `?` operator can only be applied to values that implement `Try`
--> compile-errors/ch37_result_not_awaited.rs:26:18
|
26 | let result = local_executor_for!(Orders).run(Orders::builder().create()?)?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the `?` operator cannot be applied to type `impl Future<Output = Result<JourneyResult, Refusal>>`
|
= help: the nightly-only, unstable trait `Try` is not implemented for `impl Future<Output = Result<JourneyResult, Refusal>>`
help: consider `await`ing on the `Future`
|
26 | let result = local_executor_for!(Orders).run(Orders::builder().create()?).await?;
| ++++++How it works
- The listing says how the workflow is put together. It lists each step in order, with its position, the policies attached to it, its input adapter, each service it asks for with the provider that serves it, and its rescue step with that step's own services. It is read from the declaration: nothing runs, and no provider is called. The listing alone shows, for example, that the card registry serves
charge_rescueand nothing else. - A mistake in putting a workflow together is a violation. Checking how a workflow is put together, before any journey, is called admission, and the specification names each mistake it catches:
duplicate step name,hook defined twice,service not provided,rescued step retriedand a few more, all listed in 3.6, Configuration errors. Every violation found is reported at once, not only the first. - With the attributes, a violation is a compile error.
#[workflow]checks the declaration while the program compiles, so the first two programs never run, and their messages name the violation. Some messages say "evaluation panicked": the check runs in a constant the compiler evaluates, so it is still a compile error, and nothing panics when the program runs. - Some mistakes can't be written at all. Rust's types rule out the last four. A policy that asks for a role needs a workflow implementing
Provides<dyn Messages>, which#[role]writes. Each hook's return type lists only the lifecycles that hook may return, soon step failurecan't returnOnSuccess. Everything that enters the data bag must implementValue, so it must beClone, and serialise and deserialise with serde. And therunof an asynchronous workflow's executor returns a future, on which?doesn't compile until it is awaited.
Each of these is a compile error because the attributes write the workflow's declaration for you, and check it as they write it. But you haven't seen what they write.
Outcomes
We have shown how the listing says how a workflow is put together, and how a mistake in putting it together is caught before any order runs.
| The usual way | With Itinera |
|---|---|
| Which hooks run after a step, or which client serves a repair, is often learned only by reading every module. | The listing names each step's policies, its adapter, and the services of its rescue step. |
| A wiring mistake, such as two handlers for one event, often shows only when a request takes that path in production. | The program doesn't compile, and the message names the mistake. |