The Itinera bookRust edition

Part 2: Getting started, Chapter 20

An optional input

What you want to do

Some orders come with a discount, and most do not, so you want the shop to charge the price less the discount, or the full price when there is none. Working out what an order costs and charging a card are two rules of the business, so you want each in a step of its own: the charge should charge an amount, and know nothing of prices or discounts.

The code

The pricing is a new step. It asks for the order's price, and for the discount as an Option<i64>, an optional input. It gives back the price less the discount, or the full price when the discount is None, under amount.

Rust, from the program ch20_an_optional_input
/// Works out what an order costs: its price, less its discount if it has one, given back as the
/// amount to charge.
#[derive(Step)]
struct Price {
    #[input]
    price: i64,
    #[input]
    discount: Option<i64>,
}

#[step]
impl Price {
    fn run(self, #[contributor] mut contributor: Contributor<'_>) -> Result<Outcome, Error> {
        contributor.contribute("amount", self.price - self.discount.unwrap_or(0));
        Self::succeed()
    }
}

The charge now asks for that amount, and charges it. There is no price or discount in it.

Rust, from the program ch20_an_optional_input
/// Charges an order's amount to its card through the payment client, and gives back its receipt,
/// and the gift card's code when the order is a gift card.
#[derive(Step)]
struct Charge {
    #[input]
    order: String,
    #[input]
    amount: i64,
    #[input]
    card: String,
    #[input]
    gift_card: bool,
}

#[step]
impl Charge {
    fn run(
        self,
        #[service] payments: &dyn Payments,
        #[contributor] mut contributor: Contributor<'_>,
    ) -> Result<Outcome, Error> {
        if self.amount <= 0 {
            return Self::fail("nothing-to-charge");
        }
        let Some(receipt) = payments.charge(&self.order, &self.card, self.amount)? else {
            return Self::fail("card-expired");
        };
        contributor.contribute("receipt", receipt);
        if self.gift_card {
            contributor.contribute("code", format!("G-{}", self.order));
        }
        Self::succeed()
    }
}

The payment client it asks for is chapter 14's Payments.

The workflow lists the pricing first, under the name price, so the charge reads the amount it gave back:

Rust, from the program ch20_an_optional_input
/// The shop's workflow: it works out what the order costs, then charges it, repairing a charge
/// that fails and auditing one that succeeds, then ships, and shipping may be tried once more.
/// Once a journey has ended, the customer is told, through the channel the shop configures.
#[workflow(name = "orders", reporters = [Log, Desk], policies = [Notify])]
#[with_step(Price, name = "price")]
#[with_step(Charge, name = "charge", rescue = RenewCard, policies = [Audit, Outage])]
#[with_step(Ship, name = "ship", retry_budget = 1, policies = [HeavyParcels])]
struct Orders {
    channel: &'static str,
}

The other steps, policies and reporters it names are as earlier chapters show them: RenewCard in chapter 14, the shipping, outage and heavy_parcels in chapter 17, the log in chapter 18, notify in chapter 19 and the desk in chapter 11.

The audit of chapter 15 wrote down the order's price, which is no longer what was charged. It now asks for the amount, from the data bag:

Rust, from the program ch20_an_optional_input
/// Writes an audit line for every step it is attached to that succeeds: the amount the order was
/// charged, and the receipt the step gave back.
#[derive(Default)]
struct Audit;

#[step_policy(name = "audit")]
impl Audit {
    #[on_step_success]
    fn charged(
        &self,
        #[step_name] step: StepName,
        #[from_workflow] amount: i64,
        #[from_step] receipt: String,
        #[contributor] mut contributor: Contributor<'_>,
    ) -> Result<Option<OnSuccess>, Error> {
        contributor.contribute("audit", format!("{step} took {amount}, receipt {receipt}"));
        Self::carry_on()
    }
}

The initial data has a discount, as an Option<i64> too. Ann's order comes with a discount of 10, and the others with none.

Rust, from the program ch20_an_optional_input
/// What the shop knows about an order when its journey starts.
#[derive(InitialData)]
struct NewOrder {
    order: String,
    customer: String,
    price: i64,
    discount: Option<i64>,
    weight: i64,
    card: String,
    gift_card: bool,
}

The rest is chapter 19's shop, and it runs the same five orders. It prints:

What ch20_an_optional_input prints, recorded in ch20_an_optional_input.txt
Ann's order:
log orders A-1 #1 journey_started card, customer, discount, gift_card, order, price, weight
log orders A-1 #2 attempt_started price, attempt 1
log orders A-1 #3 step_succeeded price
log orders A-1 #4 contribution_committed amount
log orders A-1 #5 attempt_started charge, attempt 1
log orders A-1 #6 service_provided charge gets payments from bank, a ch20_an_optional_input::Bank
the bank charges 30 for order A-1
log orders A-1 #7 step_succeeded charge
log orders A-1 #8 contribution_committed receipt
log orders A-1 #9 hook_called audit: on step success of charge
log orders A-1 #10 contribution_committed audit
log orders A-1 #11 attempt_started ship, attempt 1
log orders A-1 #12 service_provided ship gets carrier from carriers, a ch20_an_optional_input::MainCarrier
the main carrier books order A-1
log orders A-1 #13 step_info ship: the parcel leaves with receipt R-A-1
log orders A-1 #14 step_succeeded ship
log orders A-1 #15 contribution_committed tracking
log orders A-1 #16 optional_input_absent code
email to Ann: Order A-1 is on its way, tracking T-A-1.
log orders A-1 #17 hook_called notify: on workflow success
log orders A-1 #18 journey_succeeded
the journey succeeded, with receipt R-A-1 and tracking T-A-1
audit: charge took 30, receipt R-A-1
Gil's order:
log orders A-7 #1 journey_started card, customer, gift_card, order, price, weight
log orders A-7 #2 attempt_started price, attempt 1
log orders A-7 #3 optional_input_absent discount
log orders A-7 #4 step_succeeded price
log orders A-7 #5 contribution_committed amount
log orders A-7 #6 attempt_started charge, attempt 1
log orders A-7 #7 service_provided charge gets payments from bank, a ch20_an_optional_input::Bank
the bank charges 45 for order A-7
log orders A-7 #8 step_succeeded charge
log orders A-7 #9 contribution_committed receipt
log orders A-7 #10 hook_called audit: on step success of charge
log orders A-7 #11 contribution_committed audit
log orders A-7 #12 attempt_started ship, attempt 1
log orders A-7 #13 service_provided ship gets carrier from carriers, a ch20_an_optional_input::MainCarrier
the main carrier books order A-7
log orders A-7 #14 step_failed ship: too-heavy
log orders A-7 #15 hook_called heavy_parcels: on step retry of ship
log orders A-7 #16 step_retrying ship
log orders A-7 #17 attempt_started ship, attempt 2
log orders A-7 #18 service_provided ship gets carrier from carriers, a ch20_an_optional_input::BackupCarrier
the backup carrier books order A-7
log orders A-7 #19 step_info ship: the parcel leaves with receipt R-A-7
log orders A-7 #20 step_succeeded ship
log orders A-7 #21 contribution_committed tracking
log orders A-7 #22 optional_input_absent code
email to Gil: Order A-7 is on its way, tracking B-A-7.
log orders A-7 #23 hook_called notify: on workflow success
log orders A-7 #24 journey_succeeded
the journey succeeded, with receipt R-A-7 and tracking B-A-7
audit: charge took 45, receipt R-A-7
Jo's order:
log orders A-8 #1 journey_started card, customer, gift_card, order, price, weight
log orders A-8 #2 attempt_started price, attempt 1
log orders A-8 #3 optional_input_absent discount
log orders A-8 #4 step_succeeded price
log orders A-8 #5 contribution_committed amount
log orders A-8 #6 attempt_started charge, attempt 1
log orders A-8 #7 service_provided charge gets payments from bank, a ch20_an_optional_input::Bank
the bank declines card jo-2024: it has expired
log orders A-8 #8 step_failed charge: card-expired
log orders A-8 #9 step_rescuing charge: failure, rescued by charge_rescue
log orders A-8 #10 attempt_started charge_rescue, attempt 1
the bank says card jo-2024 was replaced by jo-2029
log orders A-8 #11 step_succeeded charge_rescue
log orders A-8 #12 contribution_committed card
log orders A-8 #13 data_overwritten card
log orders A-8 #14 step_rescued charge by charge_rescue
log orders A-8 #15 attempt_started charge, attempt 2
the bank charges 30 for order A-8
log orders A-8 #16 step_succeeded charge
log orders A-8 #17 contribution_committed receipt
log orders A-8 #18 hook_called audit: on step success of charge
log orders A-8 #19 contribution_committed audit
log orders A-8 #20 attempt_started ship, attempt 1
log orders A-8 #21 service_provided ship gets carrier from carriers, a ch20_an_optional_input::MainCarrier
the main carrier books order A-8
log orders A-8 #22 step_info ship: the parcel leaves with receipt R-A-8
log orders A-8 #23 step_succeeded ship
log orders A-8 #24 contribution_committed tracking
log orders A-8 #25 optional_input_absent code
email to Jo: Order A-8 is on its way, tracking T-A-8.
log orders A-8 #26 hook_called notify: on workflow success
log orders A-8 #27 journey_succeeded
the journey succeeded, with receipt R-A-8 and tracking T-A-8
audit: charge took 30, receipt R-A-8
Kim's order:
log orders A-9 #1 journey_started card, customer, gift_card, order, price, weight
log orders A-9 #2 attempt_started price, attempt 1
log orders A-9 #3 optional_input_absent discount
log orders A-9 #4 step_succeeded price
log orders A-9 #5 contribution_committed amount
log orders A-9 #6 attempt_started charge, attempt 1
log orders A-9 #7 service_provided charge gets payments from bank, a ch20_an_optional_input::Bank
the bank charges 25 for order A-9
log orders A-9 #8 step_succeeded charge
log orders A-9 #9 contribution_committed receipt
log orders A-9 #10 contribution_committed code
log orders A-9 #11 hook_called audit: on step success of charge
log orders A-9 #12 contribution_committed audit
log orders A-9 #13 attempt_started ship, attempt 1
log orders A-9 #14 service_provided ship gets carrier from carriers, a ch20_an_optional_input::MainCarrier
log orders A-9 #15 step_skipped ship: nothing-to-ship
log orders A-9 #16 contributions_discarded ship
log orders A-9 #17 optional_input_absent tracking
email to Kim: Order A-9 is paid: your gift card's code is G-A-9.
log orders A-9 #18 hook_called notify: on workflow success
log orders A-9 #19 journey_succeeded
the journey succeeded, with receipt R-A-9 and tracking none
audit: charge took 25, receipt R-A-9
Lee's order:
log orders A-10 #1 journey_started card, customer, gift_card, order, price, weight
log orders A-10 #2 attempt_started price, attempt 1
log orders A-10 #3 optional_input_absent discount
log orders A-10 #4 step_succeeded price
log orders A-10 #5 contribution_committed amount
log orders A-10 #6 attempt_started charge, attempt 1
log orders A-10 #7 service_provided charge gets payments from bank, a ch20_an_optional_input::Bank
the bank declines card lee-2019: it has expired
log orders A-10 #8 step_failed charge: card-expired
log orders A-10 #9 step_rescuing charge: failure, rescued by charge_rescue
log orders A-10 #10 attempt_started charge_rescue, attempt 1
the bank says nothing replaced card lee-2019
log orders A-10 #11 step_failed charge_rescue: not-replaced
log orders A-10 #12 step_given_up charge_rescue: failure
email to Lee: Sorry, order A-10 could not be completed.
log orders A-10 #13 hook_called notify: on workflow failure
log orders A-10 #14 journey_failed charge_rescue
desk: order A-10 failed at charge_rescue
the journey failed, with receipt none and tracking none
audit: none

How it works

  1. An input's type says whether it is required. An input of any type but Option is required, as every input so far has been. An input of type Option<T> is optional: the step is built with Some of the value when the data bag holds one, and with None when it holds none.
  2. Initial data leaves out what an order doesn't have. A field of type Option<T> that is None adds no key to the data bag. Ann's journey starts with discount among its keys; Gil's, Jo's, Kim's and Lee's don't.
  3. An absent optional input is recorded. When the pricing is built for an order without a discount, the executor records optional_input_absent, naming discount, as it does for a hook's request in chapter 18. The pricing has one attempt, so each journey without a discount records it once.
  4. The step decides what the absence means. For the pricing, no discount means the full price, so it uses unwrap_or(0). The executor only says the value isn't there; it is the step's business to know what to do without it. A step that can work without a value asks for it as optional; one that can't asks for it as required.
  5. Each rule of the business is a step. The bank charged Ann 30, the price less her discount, but the charge never saw either: it charged the amount the pricing contributed, read from the data bag like any input. A shop that priced its orders another way, with a voucher or a price per kilogram, would change the pricing, and use the same charge. A step holds one rule of the business; two rules are two steps, and the workflow puts them in order.
  6. The audit writes down what was charged. It reads amount with #[from_workflow], since the pricing contributed it and the charge didn't, so it writes down what was charged, whatever it was worked out from.

The warehouse is changing how it describes parcels, though. From now on, each order comes with its parcel as one value, holding its weight and its size, and the shipping, which asks only for a weight, can't read that.

Outcomes

We have shown how a step asks for a value it can do without, and how the pricing became a step of its own.

The usual way With Itinera
An optional value is often passed as a default, such as a discount of 0, so the code cannot tell "no discount" from "a discount of nothing". An optional input is None when the data bag has no value, and the executor records that it was absent, by key.
The code that charges a card often works out the amount too, so a new way of pricing changes the charging. The pricing and the charge are two steps, and the charge only charges the amount it is given.