The Itinera bookRust edition

Part 2: Getting started, Chapter 21

Deciding where a step's data comes from

What you want to do

The warehouse now describes each order's parcel as one value, its weight and its size, but the shipping asks only for a weight, and shouldn't have to know how the warehouse describes a parcel. You want the shop's workflow to take the weight out of the parcel and give it to the shipping.

The code

The parcel is a struct of the shop's own. It derives Clone, and Serialize and Deserialize from the serde crate, added with its derive feature, so that it is a value that the data bag can hold:

Rust, from the program ch21_deciding_where_a_steps_data_comes_from
/// How the warehouse describes an order's parcel: its weight in kg, and its length, width and
/// height in cm.
#[derive(Clone, Serialize, Deserialize)]
struct Parcel {
    weight: i64,
    size: [i64; 3],
}

Each order's initial data holds the parcel, in place of the weight it held before:

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

The shipping doesn't change at all: it is still chapter 17's. The workflow gives it the weight with an input adapter: a method of its #[workflow] block marked #[input_adapter(steps = ["ship"])]. The executor asks it about each input of the shipping as the shipping is built, and tells it which one with its parameter marked #[key]. It returns Ok of an Option<AnyValue>, where AnyValue holds a value of any type. For weight, it reads the parcel from the data bag and supplies its weight with Self::supply(...), which is Some of it. For any other input, it returns Self::from_data_bag(), which is None, and that input is read from the data bag as before.

Rust, from the program ch21_deciding_where_a_steps_data_comes_from
/// Gives the shipping the weight of the order's parcel, as the warehouse describes it. Every
/// other input of the shipping is read from the data bag.
#[input_adapter(steps = ["ship"])]
fn parcels(
    &self,
    #[key] key: &str,
    #[from_workflow] parcel: Parcel,
) -> Result<Option<AnyValue>, Error> {
    if key != "weight" {
        return Self::from_data_bag();
    }
    Self::supply(parcel.weight)
}

The log is chapter 18's, and now also says which input an adapter supplied, to which step:

Rust, from the program ch21_deciding_where_a_steps_data_comes_from
EventBody::InputAdapterSupplied {
    step, key, adapter, ..
} => Some(format!("{} gets {key} from {adapter}", step.step)),

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

What ch21_deciding_where_a_steps_data_comes_from prints, recorded in ch21_deciding_where_a_steps_data_comes_from.txt
Ann's order:
log orders A-1 #1 journey_started card, customer, discount, gift_card, order, parcel, price
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 ch21_deciding_where_a_steps_data_comes_from::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 input_adapter_supplied ship gets weight from parcels
log orders A-1 #13 service_provided ship gets carrier from carriers, a ch21_deciding_where_a_steps_data_comes_from::MainCarrier
the main carrier books order A-1
log orders A-1 #14 step_info ship: the parcel leaves with receipt R-A-1
log orders A-1 #15 step_succeeded ship
log orders A-1 #16 contribution_committed tracking
log orders A-1 #17 optional_input_absent code
email to Ann: Order A-1 is on its way, tracking T-A-1.
log orders A-1 #18 hook_called notify: on workflow success
log orders A-1 #19 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, parcel, price
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 ch21_deciding_where_a_steps_data_comes_from::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 input_adapter_supplied ship gets weight from parcels
log orders A-7 #14 service_provided ship gets carrier from carriers, a ch21_deciding_where_a_steps_data_comes_from::MainCarrier
the main carrier books order A-7
log orders A-7 #15 step_failed ship: too-heavy
log orders A-7 #16 hook_called heavy_parcels: on step retry of ship
log orders A-7 #17 step_retrying ship
log orders A-7 #18 attempt_started ship, attempt 2
log orders A-7 #19 input_adapter_supplied ship gets weight from parcels
log orders A-7 #20 service_provided ship gets carrier from carriers, a ch21_deciding_where_a_steps_data_comes_from::BackupCarrier
the backup carrier books order A-7
log orders A-7 #21 step_info ship: the parcel leaves with receipt R-A-7
log orders A-7 #22 step_succeeded ship
log orders A-7 #23 contribution_committed tracking
log orders A-7 #24 optional_input_absent code
email to Gil: Order A-7 is on its way, tracking B-A-7.
log orders A-7 #25 hook_called notify: on workflow success
log orders A-7 #26 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, parcel, price
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 ch21_deciding_where_a_steps_data_comes_from::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 input_adapter_supplied ship gets weight from parcels
log orders A-8 #22 service_provided ship gets carrier from carriers, a ch21_deciding_where_a_steps_data_comes_from::MainCarrier
the main carrier books order A-8
log orders A-8 #23 step_info ship: the parcel leaves with receipt R-A-8
log orders A-8 #24 step_succeeded ship
log orders A-8 #25 contribution_committed tracking
log orders A-8 #26 optional_input_absent code
email to Jo: Order A-8 is on its way, tracking T-A-8.
log orders A-8 #27 hook_called notify: on workflow success
log orders A-8 #28 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, parcel, price
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 ch21_deciding_where_a_steps_data_comes_from::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 input_adapter_supplied ship gets weight from parcels
log orders A-9 #15 service_provided ship gets carrier from carriers, a ch21_deciding_where_a_steps_data_comes_from::MainCarrier
log orders A-9 #16 step_skipped ship: nothing-to-ship
log orders A-9 #17 contributions_discarded ship
log orders A-9 #18 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 #19 hook_called notify: on workflow success
log orders A-9 #20 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, parcel, price
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 ch21_deciding_where_a_steps_data_comes_from::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. Without an adapter, an input is read from the data bag under its key. Every input so far came from there: a step asking for weight got the value under weight. Now there is no weight in the data bag, only parcel.
  2. An input adapter satisfies what a step needs. The shipping says what it needs, a weight in kg, and the adapter gives it exactly that, so the shipping never sees the parcel, its size or the warehouse's way of describing it. A step says what it needs; the workflow decides where each value comes from.
  3. It belongs to the workflow. It is named after its method, parcels, and attached by name to the steps it serves, here only the shipping. Each workflow knows its own data, so the same shipping could run in a workflow whose orders carry their weight in another form.
  4. It is asked once for each input, as the step is built. For each of the shipping's inputs, the adapter either supplies a value, which the shipping gets, or returns Self::from_data_bag(). It supplies only weight, so input_adapter_supplied ship gets weight from parcels appears once in each attempt of the shipping, before it runs: twice for Gil, whose shipping has two attempts, and once for Kim, though her shipping then skips.
  5. It adapts the data's form, never the business. Taking one field out of a value is one of its uses. Others are converting a value to the type a step asks for, and fetching data from somewhere else, because it is expensive to get or is kept elsewhere, only when a step needs it. Working out what an order costs is a rule of the business, so it stays in the pricing, a step of its own.
  6. It reads, and never writes. An adapter contributes nothing: what it supplies goes to the step being built, never to the data bag. What it supplies is recorded as an input_adapter_supplied event, naming the step, the key and the adapter, without the value itself.

The shop now charges, repairs, ships, audits and tells its customers, each in the place where it belongs. But now and then the website sends an order a second time, and the shop would charge it again, and book another parcel.

Outcomes

We have shown how the workflow gives a step the data it asks for, in the form it asks for it.

The usual way With Itinera
A function that needs one field of a larger value often takes the whole value, and comes to depend on how its callers describe their data. The shipping asks for a weight, and the workflow's input adapter takes it out of the warehouse's parcel.