A first journey
What you want to do
An order comes in, and you want the shop to charge it: run the charge of chapter 4 with the order's price, and know how it went.
The code
The charge is chapter 4's step, unchanged. To run it, the shop needs three things around it.
The first is a workflow: the list of steps that run for every order. For now, the shop's workflow has one step.
/// The shop's workflow: for now, it only charges.
#[workflow(name = "orders")]
#[with_step(Charge, name = "charge")]
struct Orders;The second is what the shop knows about an order when it comes in, here only its price, written as a plain struct. It is the order's initial data.
/// What the shop knows about an order when its journey starts.
#[derive(InitialData)]
struct NewOrder {
price: i64,
}The third is the running itself. For each order, the shop creates an instance of the workflow with that order's initial data, and hands it to an executor, which runs it. Two orders come in: Ann's, of 40, and Hal's, a free sample that costs nothing.
fn main() -> Result<(), Box<dyn std::error::Error>> {
// The customer, and the price of their order.
for (customer, price) in [("Ann", 40), ("Hal", 0)] {
println!("{customer}'s order:");
let instance = Orders::builder()
.initial_data(NewOrder { price })
.create()?;
let result = local_executor_for!(Orders).run(instance)?;
println!("the journey {}", result.status.kind());
}
Ok(())
}It prints:
Ann's order:
the bank charges 40
the journey succeeded
Hal's order:
the journey failedHow it works
- A workflow lists its steps, and names them.
#[workflow(name = "orders")]declares the structOrdersas a workflow namedorders, and each#[with_step(...)]line under it lists a step: its type, and its name in this workflow, herecharge. The struct holds nothing; the attributes are the whole declaration. A step never knows its own name, nor which workflow runs it. - The initial data is where a journey's data starts.
#[derive(InitialData)]makes each field ofNewOrdera piece of data under the field's name, so the price is there under the nameprice. That is the name the charge's input asks for, and that is how the charge gets its price: when the step is built, each of its inputs is found by its name in the journey's data, which starts as the initial data. The step still doesn't know where its price came from. - One instance for each order.
Orders::builder().initial_data(...).create()creates an instance of the workflow with one order's data. It returns aResult, because creating an instance can fail in ways this shop never meets, and?passes such an error on tomain. The next order gets an instance of its own. - The executor runs the instance as a journey. A journey is one run of an instance, from its first step to its end.
local_executor_for!(Orders), a macro fromitinera::executor, is how you get an executor: it makes the local executor that comes withitinera, for the workflow you name. The local executor runs the journey right there, inside the call to itsrun, and returns once the journey has ended. It builds the charge with the order's price, calls the charge'srun, and decides what follows from its outcome. Itsruntakes the instance by value, so an instance runs once: once it has run, it is gone. - The result says how the journey ended. The executor's
runreturnsResult<JourneyResult, Refusal>. ItsErrside, a refusal, is only for a journey that could not start at all, and neither order meets one. A journey that started always gives aJourneyResult, andresult.status.kind()says how it ended, heresucceededorfailed. A journey that failed is stillOk: a failed order is a business outcome, not an error of your program. - What follows a step's outcome is not the step's decision. Ann's charge succeeded, and since it was the workflow's last step, her journey succeeded. Hal's charge failed with
nothing-to-charge, so the bank was never called, and his journey failed. The charge only reported the failure. Failing the journey because of it is what the executor does with a failed step by default, the same way for every step of every workflow.
So the shop charges its orders. But look at what Ann's journey gives back: only that it succeeded. The bank charged her 40, and the shop knows that only because the faked bank printed it. A business rule has to give a result the rest of the shop can use, and the charge has no way to give one yet.
Outcomes
We have run a step for an order, as a journey, and read how it ended.
| The usual way | With Itinera |
|---|---|
| Code usually calls a business function directly, so each call site gathers what the function needs and passes it in. | The shop hands the executor an instance with the order's data, and the executor builds the step and runs it. |
| A failed order often comes back as an error, mixed with the program's own errors, so the caller has to tell them apart. | A failed journey is an ordinary result, and run returns an error only for a journey that could not start. |