Passing data on
What you want to do
When the bank charges an order, it gives a receipt. You want the charge to give that receipt back, so that the shop has it once the journey ends, and so that the parcel can carry it when the shop ships the order.
The code
The charge now asks for the order's number as well as its price. The bank, still faked, gives a receipt, and the charge hands it on through a contributor, a parameter of run marked #[contributor].
/// Charges an order's price, and gives back the bank's receipt.
#[derive(Step)]
struct Charge {
#[input]
order: String,
#[input]
price: i64,
}
#[step]
impl Charge {
fn run(self, #[contributor] mut contributor: Contributor<'_>) -> Result<Outcome, Error> {
if self.price <= 0 {
return Self::fail("nothing-to-charge");
}
let receipt = bank_charges(&self.order, self.price);
contributor.contribute("receipt", receipt);
Self::succeed()
}
}Shipping is a second step. It books the carrier, which gives a tracking number at once, and hands that on the same way. The carrier, faked too, weighs the parcel only when it comes to collect it, and takes it only up to 20 kg. The receipt is one of the shipping's inputs, asked for like the order's number.
/// Ships an order, with its receipt in the parcel, and gives back its tracking number.
#[derive(Step)]
struct Ship {
#[input]
order: String,
#[input]
weight: i64,
#[input]
receipt: String,
}
#[step]
impl Ship {
fn run(self, #[contributor] mut contributor: Contributor<'_>) -> Result<Outcome, Error> {
let tracking = carrier_books(&self.order);
contributor.contribute("tracking", tracking);
if !carrier_collects(self.weight) {
return Self::fail("too-heavy");
}
println!("the parcel leaves with receipt {}", self.receipt);
Self::succeed()
}
}The workflow lists the two steps, one #[with_step(...)] line each:
/// The shop's workflow: it charges, then ships.
#[workflow(name = "orders")]
#[with_step(Charge, name = "charge")]
#[with_step(Ship, name = "ship")]
struct Orders;Each order now carries its number and its parcel's weight as well as its price:
/// What the shop knows about an order when its journey starts.
#[derive(InitialData)]
struct NewOrder {
order: String,
price: i64,
weight: i64,
}Once a journey has ended, the shop reads back the receipt and the tracking number into a struct of its own. A tracking number may be missing, so its field is an Option<String>.
/// What the shop reads back once the journey has ended.
#[derive(Output)]
struct Order {
receipt: String,
tracking: Option<String>,
}Two orders come in: Ann's, whose parcel weighs 2 kg, and Gil's, whose parcel weighs 60 kg.
fn main() -> Result<(), Box<dyn std::error::Error>> {
// The order, the customer, the price and the parcel's weight in kg.
for (order, customer, price, weight) in [("A-1", "Ann", 40, 2), ("A-7", "Gil", 45, 60)] {
println!("{customer}'s order:");
let instance = Orders::builder()
.initial_data(NewOrder {
order: order.to_owned(),
price,
weight,
})
.create()?;
let result = local_executor_for!(Orders).run(instance)?;
let data: Order = result.output()?;
let tracking = data.tracking.as_deref().unwrap_or("none");
println!(
"the journey {}, with receipt {} and tracking {tracking}",
result.status.kind(),
data.receipt,
);
}
Ok(())
}It prints:
Ann's order:
the bank charges 40 for order A-1
the carrier books order A-1
the parcel leaves with receipt R-A-1
the journey succeeded, with receipt R-A-1 and tracking T-A-1
Gil's order:
the bank charges 45 for order A-7
the carrier books order A-7
the journey failed, with receipt R-A-7 and tracking noneHow it works
- A step gives back what it did by contributing it. The contributor's
contributemethod takes a name and a value: here the receipt, underreceipt. What a step gives this way is a contribution, and its names need no declaring in advance. A step'sContributor<'_>belongs to one attempt, one run of the step: the'_ties the contributor to that attempt, and it serves no other. - The journey's data is its data bag. A journey holds its data in its data bag: values under names. It starts as the initial data, and a step's contributions are committed to it when its attempt succeeds. Only values go in: data that could be serialized, such as numbers, strings, and structs that derive
Cloneand Serde'sSerializeandDeserialize. - A later step reads a contribution as an ordinary input. The shipping asks for
receiptwith an#[input]field, just as it asks for the order's number, which came from the initial data. It doesn't know the charge made the receipt, and the charge doesn't know anyone reads it. A step never calls another step: it reads what an earlier one contributed, by name. - The steps run in the order the workflow lists them. The charge's line comes first, so the charge runs first, and once it has succeeded, the shipping runs. Nothing reorders them: to read what the shop does with an order, you read the workflow's list, top to bottom.
- A failed attempt's contributions are never committed. For Gil's order, the carrier booked the parcel and gave its tracking number, which the shipping contributed. Then the carrier refused to collect a parcel of 60 kg, and the shipping failed with
too-heavy. A step's contributions are committed to the data bag only when its attempt succeeds, so Gil's tracking number never reached it, and nothing in the shop can believe his parcel left. - The data bag is the journey's output. When a journey ends, its data bag, as it stands then, is its output, whether the journey succeeded or failed.
#[derive(Output)]letsresult.output()read it intoOrder, each field from the name it is called by. AnOptionfield isNonewhen its name is absent, astrackingis after Gil's journey. It returns aResult, since the data bag might lack what a field needs, and?passes such an error on. Ann's output holds her receipt and her tracking number. Gil's holds his receipt, since his charge succeeded, and no tracking number.
So the charge gives back its receipt, and the shipping uses it, without either knowing about the other. But each still reaches the outside world on its own: the charge calls the shop's one bank, and the shipping its one carrier, by name, inside its run. A step written that way can only ever use that bank and that carrier. To charge through another bank, or a pretend one, you would have to change the step.
Outcomes
We have shown how a step gives back what it did, and how a later step and the shop read it.
| The usual way | With Itinera |
|---|---|
| A function usually returns what it did to its caller, which hands it on to the next function itself. | A step contributes what it did, and a later step or the shop reads it by name, with no step calling another. |
| When a function fails halfway, what it already wrote often stays written, and undoing it is up to the code. | A failed attempt's contributions are never committed, so the data bag holds only what steps that succeeded gave. |