The Itinera bookRust edition

Part 4: Running it for real, Chapter 37

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:

Rust, from the program ch37_listing
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:

Rust, from the program ch37_listing
/// 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:

What ch37_listing prints, recorded in ch37_listing.txt
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 carriers

Mistakes that don't compile

Each program below is a small shop with one mistake, and none of them compiles. The shipping listed twice:

Rust, the program ch37_duplicate_step_name. Does not compile
//! 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(())
}
What the compiler says about ch37_duplicate_step_name, recorded in ch37_duplicate_step_name.txt
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:

Rust, the program ch37_hook_defined_twice. Does not compile
//! 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(())
}
What the compiler says about ch37_hook_defined_twice, recorded in ch37_hook_defined_twice.txt
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 here

A policy that tells the customer through the Messages role, on a workflow that doesn't provide it:

Rust, the program ch37_role_not_provided. Does not compile
//! 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(())
}
What the compiler says about ch37_role_not_provided, recorded in ch37_role_not_provided.txt
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:

Rust, the program ch37_invalid_lifecycle. Does not compile
//! 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(())
}
What the compiler says about ch37_invalid_lifecycle, recorded in ch37_invalid_lifecycle.txt
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:

Rust, the program ch37_not_a_value. Does not compile
//! 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(())
}
What the compiler says about ch37_not_a_value, recorded in ch37_not_a_value.txt
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:

Rust, the program ch37_result_not_awaited. Does not compile
//! 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(())
}
What the compiler says about ch37_result_not_awaited, recorded in ch37_result_not_awaited.txt
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

  1. 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_rescue and nothing else.
  2. 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 retried and a few more, all listed in 3.6, Configuration errors. Every violation found is reported at once, not only the first.
  3. 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.
  4. 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, so on step failure can't return OnSuccess. Everything that enters the data bag must implement Value, so it must be Clone, and serialise and deserialise with serde. And the run of 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.