Declaring by hand
What you want to do
The attributes have written the shop's workflow for you since the first chapters, and checked it as they wrote it. You want to see what they write, and to write part of the shop that way yourself: for a declaration put together when the program starts, from the shop's configuration, or for a mistake you'd rather read as a value than as a compile error.
The code
The shop by hand
The shop is the one chapter 36 runs, declared in the long form: without the attributes, for its pricing step, its audit and its workflow. The pricing is two things: the step, which runs once and implements Step, and what builds it for each attempt, which implements StepFactory, says what the step needs and builds it from what was resolved. Each input is named once, by a constant holding its key and its type, such as Input<i64> or OptionalInput<i64>, which both needs and build use, and Resolved hands over what was resolved for the attempt:
const PRICE: Input<i64> = Input::new("price");
const DISCOUNT: OptionalInput<i64> = OptionalInput::new("discount");
/// Works out what an order costs, for one attempt: its price, less its discount if it has one,
/// given back as the amount to charge.
struct Price<'a> {
price: i64,
discount: Option<i64>,
contributor: Contributor<'a>,
}
impl Step for Price<'_> {
fn run(mut self) -> Result<Outcome, Error> {
let amount = self.price - self.discount.unwrap_or(0);
self.contributor.contribute("amount", amount);
Ok(Outcome::success())
}
}
/// Builds the pricing for each attempt, from what it needs.
struct Pricing;
impl StepFactory for Pricing {
type Step<'a> = Price<'a>;
fn needs(&self) -> StepNeeds {
StepNeeds::new()
.input(&PRICE)
.optional_input(&DISCOUNT)
.contributor()
}
fn build<'a>(&'a self, got: &mut Resolved<'a>) -> Result<Price<'a>, Error> {
Ok(Price {
price: got.input(&PRICE)?,
discount: got.optional_input(&DISCOUNT)?,
contributor: got.contributor()?,
})
}
}const PRICE: Input<i64> = Input::new("price");
const DISCOUNT: OptionalInput<i64> = OptionalInput::new("discount");
/// Works out what an order costs, for one attempt: its price, less its discount if it has one,
/// given back as the amount to charge.
struct Price<'a> {
price: i64,
discount: Option<i64>,
contributor: Contributor<'a>,
}
impl AsyncStep for Price<'_> {
async fn run(mut self) -> Result<Outcome, Error> {
let amount = self.price - self.discount.unwrap_or(0);
self.contributor.contribute("amount", amount);
Ok(Outcome::success())
}
}
/// Builds the pricing for each attempt, from what it needs.
struct Pricing;
impl AsyncStepFactory for Pricing {
type Step<'a> = Price<'a>;
fn needs(&self) -> StepNeeds {
StepNeeds::new()
.input(&PRICE)
.optional_input(&DISCOUNT)
.contributor()
}
fn build<'a>(&'a self, got: &mut Resolved<'a, Asynchronous>) -> Result<Price<'a>, Error> {
Ok(Price {
price: got.input(&PRICE)?,
discount: got.optional_input(&DISCOUNT)?,
contributor: got.contributor()?,
})
}
}The audit implements the trait of its one hook, OnStepSuccess<Orders>: its needs lists what the hook requests, as a HookNeeds, and the hook reads it from Requested when it is called, with the journey ID, the step's name and the attempt, which every step hook gets:
const AMOUNT: Input<i64> = Input::new("amount");
const RECEIPT: Input<String> = Input::new("receipt");
/// Writes an audit line for every step it is attached to that succeeds: the journey, the attempt
/// that took the money, the amount the order was charged, and the receipt the step gave back.
struct Audit;
impl OnStepSuccess<Orders> for Audit {
fn needs() -> HookNeeds<StepSuccess> {
HookNeeds::new()
.from_workflow(&AMOUNT)
.from_step(&RECEIPT)
.contributor()
}
fn on_step_success(
&self,
mut got: Requested<'_, Orders, StepSuccess>,
) -> Result<Option<OnSuccess>, Error> {
let journey = got.journey_id();
let step = got.step_name();
let attempt = got.attempt();
let amount = got.from_workflow(&AMOUNT)?;
let receipt = got.from_step(&RECEIPT)?;
got.contributor()?.contribute(
"audit",
format!("{journey}: {step} took {amount} on attempt {attempt}, receipt {receipt}"),
);
Ok(None)
}
}const AMOUNT: Input<i64> = Input::new("amount");
const RECEIPT: Input<String> = Input::new("receipt");
/// Writes an audit line for every step it is attached to that succeeds: the journey, the attempt
/// that took the money, the amount the order was charged, and the receipt the step gave back.
struct Audit;
impl AsyncOnStepSuccess<Orders> for Audit {
fn needs() -> HookNeeds<StepSuccess> {
HookNeeds::new()
.from_workflow(&AMOUNT)
.from_step(&RECEIPT)
.contributor()
}
async fn on_step_success(
&self,
mut got: Requested<'_, Orders, StepSuccess, Asynchronous>,
) -> Result<Option<OnSuccess>, Error> {
let journey = got.journey_id();
let step = got.step_name();
let attempt = got.attempt();
let amount = got.from_workflow(&AMOUNT)?;
let receipt = got.from_step(&RECEIPT)?;
got.contributor()?.contribute(
"audit",
format!("{journey}: {step} took {amount} on attempt {attempt}, receipt {receipt}"),
);
Ok(None)
}
}Orders implements each role's trait, and Provides for it. The ID generator, the input adapter and the providers are its plain methods, each reading what it requested from its own Requested, such as Provider<PerAttempt>, which carries the attempt:
/// The shop's workflow, whose own value is the channel the shop tells its customers through.
struct Orders {
channel: &'static str,
}
impl Refunds for Orders {
fn refund(&self, receipt: &str, amount: i64) {
println!("the bank gives {amount} back on receipt {receipt}");
}
}
impl Provides<dyn Refunds> for Orders {
fn role(&self) -> &(dyn Refunds + 'static) {
self
}
}
impl Messages for Orders {
fn tell(&self, customer: &str, message: &str) {
println!("{} to {customer}: {message}", self.channel);
}
}
impl Provides<dyn Messages> for Orders {
fn role(&self) -> &(dyn Messages + 'static) {
self
}
}
const PARCEL: Input<Parcel> = Input::new("parcel");
impl Orders {
/// Names each journey after its order.
fn order_number(&self, data: &DataBag) -> Result<String, Error> {
Ok(data.required("order")?)
}
/// 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.
fn parcels(
&self,
mut got: Requested<'_, Self, InputAdapter>,
) -> Result<Option<AnyValue>, Error> {
if got.key() != "weight" {
return Ok(None);
}
let parcel = got.from_workflow(&PARCEL)?;
Ok(Some(AnyValue::new(parcel.weight)))
}
/// The order book, which has the orders completed before today.
fn ledger(
&self,
_: Requested<'_, Self, Provider<PerJourney>>,
) -> Result<Provided<dyn OrderBook>, Error> {
Ok(provide!(Ledger {
completed: vec!["A-13"],
}))
}
/// The bank, as the payment client of the journey.
fn bank(
&self,
_: Requested<'_, Self, Provider<PerJourney>>,
) -> Result<Provided<dyn Payments>, Error> {
Ok(provide!(Bank { this_year: 2026 }))
}
/// The bank's card registry in Europe, for the charge's rescue step only.
fn cards(
&self,
_: Requested<'_, Self, Provider<PerJourney>>,
) -> Result<Provided<dyn Cards>, Error> {
Ok(Provided::from_box(card_registry("eu")).with_data("eu".to_owned()))
}
/// A carrier for every attempt of a step: the main carrier first, the backup carrier after.
fn carriers(
&self,
got: Requested<'_, Self, Provider<PerAttempt>>,
) -> Result<Provided<dyn Carrier>, Error> {
if got.attempt() == NonZeroU32::MIN {
return Ok(provide!(MainCarrier));
}
Ok(provide!(BackupCarrier))
}
}/// The shop's workflow, whose own value is the channel the shop tells its customers through.
struct Orders {
channel: &'static str,
}
#[async_trait]
impl Refunds for Orders {
async fn refund(&self, receipt: &str, amount: i64) {
network().await;
println!("the bank gives {amount} back on receipt {receipt}");
}
}
impl Provides<dyn Refunds> for Orders {
fn role(&self) -> &(dyn Refunds + 'static) {
self
}
}
#[async_trait]
impl Messages for Orders {
async fn tell(&self, customer: &str, message: &str) {
network().await;
println!("{} to {customer}: {message}", self.channel);
}
}
impl Provides<dyn Messages> for Orders {
fn role(&self) -> &(dyn Messages + 'static) {
self
}
}
const PARCEL: Input<Parcel> = Input::new("parcel");
impl Orders {
/// Names each journey after its order.
fn order_number(&self, data: &DataBag) -> Result<String, Error> {
Ok(data.required("order")?)
}
/// 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.
fn parcels(
&self,
mut got: Requested<'_, Self, InputAdapter>,
) -> Result<Option<AnyValue>, Error> {
if got.key() != "weight" {
return Ok(None);
}
let parcel = got.from_workflow(&PARCEL)?;
Ok(Some(AnyValue::new(parcel.weight)))
}
/// The order book, which has the orders completed before today.
async fn ledger(
&self,
_: Requested<'_, Self, Provider<PerJourney>, Asynchronous>,
) -> Result<Provided<dyn OrderBook>, Error> {
Ok(provide!(Ledger {
completed: vec!["A-13"],
}))
}
/// The bank, as the payment client of the journey.
async fn bank(
&self,
_: Requested<'_, Self, Provider<PerJourney>, Asynchronous>,
) -> Result<Provided<dyn Payments>, Error> {
Ok(provide!(Bank { this_year: 2026 }))
}
/// The bank's card registry in Europe, for the charge's rescue step only.
async fn cards(
&self,
_: Requested<'_, Self, Provider<PerJourney>, Asynchronous>,
) -> Result<Provided<dyn Cards>, Error> {
Ok(Provided::from_box(card_registry("eu")).with_data("eu".to_owned()))
}
/// A carrier for every attempt of a step: the main carrier first, the backup carrier after.
async fn carriers(
&self,
got: Requested<'_, Self, Provider<PerAttempt>, Asynchronous>,
) -> Result<Provided<dyn Carrier>, Error> {
if got.attempt() == NonZeroU32::MIN {
return Ok(provide!(MainCarrier));
}
Ok(provide!(BackupCarrier))
}
}Parcel is chapter 21's, OrderBook is chapter 22's, Payments and Carrier are chapter 12's, Messages chapter 19's, Refunds chapter 23's, and Cards and card_registry are chapter 35's.
And the workflow is put together by its builder, part by part, each service under a constant holding its key and its trait. The other steps and policies are still declared with the attributes, and go into the builder through their descriptor, and the rescue step through rescued_by:
const ORDER_BOOK: Service<dyn OrderBook> = Service::new("order_book");
const PAYMENTS: Service<dyn Payments> = Service::new("payments");
const CARDS: Service<dyn Cards> = Service::new("cards");
const CARRIER: Service<dyn Carrier> = Service::new("carrier");
/// Declares the shop's workflow, part by part, and checks it. It looks the order up, and finishes
/// an order already completed. Otherwise 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, its charge refunded if it is given up. Once a journey has ended, the customer
/// is told.
fn orders() -> Result<WorkflowDescriptor<Orders>, Violations> {
let audit = StepPolicyDescriptor::new("audit", || Audit).on_step_success();
let parcels = InputAdapterDescriptor::new("parcels", step_name!("ship"), Orders::parcels)
.needing(HookNeeds::new().from_workflow(&PARCEL));
let cards = ServiceProviderDescriptor::new("cards", &CARDS, Orders::cards)
.step(step_name!("charge_rescue"));
WorkflowDescriptor::builder("orders")
.step(Lookup::descriptor(step_name!("lookup")).policy(AlreadyCompleted::descriptor()))
.step(StepDescriptor::new(step_name!("price"), Pricing))
.step(
Charge::descriptor(step_name!("charge"))
.policy(audit)
.policy(Outage::descriptor())
.rescued_by::<RenewCard>(),
)
.step(
Ship::descriptor(step_name!("ship"))
.retry_budget(1)
.policy(FailedShipping::descriptor()),
)
.policy(Notify::descriptor())
.reporter::<Log>()
.reporter::<Desk>()
.id_generator(Orders::order_number)
.input_adapter(parcels)
.provider(ServiceProviderDescriptor::new(
"ledger",
&ORDER_BOOK,
Orders::ledger,
))
.provider(ServiceProviderDescriptor::new(
"bank",
&PAYMENTS,
Orders::bank,
))
.provider(cards)
.provider(ServiceProviderDescriptor::per_attempt(
"carriers",
&CARRIER,
Orders::carriers,
))
.build()
}const ORDER_BOOK: Service<dyn OrderBook> = Service::new("order_book");
const PAYMENTS: Service<dyn Payments> = Service::new("payments");
const CARDS: Service<dyn Cards> = Service::new("cards");
const CARRIER: Service<dyn Carrier> = Service::new("carrier");
/// Declares the shop's asynchronous workflow, part by part, and checks it. It looks the order up,
/// and finishes an order already completed. Otherwise 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, its charge refunded if it is given up. Once a journey has
/// ended, the customer is told.
fn orders() -> Result<WorkflowDescriptor<Orders, Asynchronous>, Violations> {
let audit = StepPolicyDescriptor::new_async("audit", || Audit).on_step_success();
let parcels = InputAdapterDescriptor::new("parcels", step_name!("ship"), Orders::parcels)
.needing(HookNeeds::new().from_workflow(&PARCEL));
let cards = ServiceProviderDescriptor::new_async("cards", &CARDS, Orders::cards)
.step(step_name!("charge_rescue"));
WorkflowDescriptor::async_builder("orders")
.step(Lookup::descriptor(step_name!("lookup")).policy(AlreadyCompleted::descriptor()))
.step(StepDescriptor::new_async(step_name!("price"), Pricing))
.step(
Charge::descriptor(step_name!("charge"))
.policy(audit)
.policy(Outage::descriptor())
.rescued_by::<RenewCard>(),
)
.step(
Ship::descriptor(step_name!("ship"))
.retry_budget(1)
.policy(FailedShipping::descriptor()),
)
.policy(Notify::descriptor())
.reporter::<Desk>()
.async_reporter::<Log>()
.id_generator(Orders::order_number)
.input_adapter(parcels)
.provider(ServiceProviderDescriptor::new_async(
"ledger",
&ORDER_BOOK,
Orders::ledger,
))
.provider(ServiceProviderDescriptor::new_async(
"bank",
&PAYMENTS,
Orders::bank,
))
.provider(cards)
.provider(ServiceProviderDescriptor::per_attempt_async(
"carriers",
&CARRIER,
Orders::carriers,
))
.build()
}The rest is as earlier chapters show it: the lookup and already_completed in chapter 22, the charge in chapter 33, RenewCard in chapter 35, the shipping in chapter 17, outage in chapter 34, failed_shipping in chapter 23, notify in chapter 19, the log in chapter 33, chapter 35 and chapter 36, and the desk in chapter 11.
In the asynchronous form, the pricing implements AsyncStep and AsyncStepFactory, the audit AsyncOnStepSuccess, the roles take #[async_trait], and every descriptor is made with its asynchronous constructor, such as WorkflowDescriptor::async_builder.
main calls orders() once, before any journey, and makes each order's instance with orders.instance(Orders { channel: "email" }):
let created = orders
.instance(Orders { channel: "email" })
.reporters_global_metadata("customer", customer.to_owned())
.initial_data(NewOrder {
order: order.map(str::to_owned),
customer: customer.to_owned(),
price,
discount,
parcel: Parcel { weight, size },
card: card.map(str::to_owned),
gift_card,
})
.create();/// An instance of the shop's workflow for an order paid with this card.
fn order(
orders: &WorkflowDescriptor<Orders, Asynchronous>,
order: Option<&str>,
customer: &str,
price: i64,
discount: Option<i64>,
parcel: Parcel,
card: &str,
) -> Result<Instance<Orders, Asynchronous>, Box<dyn std::error::Error>> {
Ok(orders
.instance(Orders { channel: "email" })
.reporters_global_metadata("customer", customer.to_owned())
.initial_data(NewOrder {
order: order.map(str::to_owned),
customer: customer.to_owned(),
price,
discount,
parcel,
card: Some(card.to_owned()),
gift_card: false,
})
.create()?)
}
/// Runs an order's journey with an executor of its own.
async fn journey(instance: Instance<Orders, Asynchronous>) -> Result<JourneyResult, Refusal> {
AsyncLocalExecutor::new().run(instance).await
}In the asynchronous form, order makes the instance that way, journey runs it with AsyncLocalExecutor::new(), and main reads back only the audit line, as Audited, from chapter 34. main runs Ann's order and Jo's, and prints:
Ann's order A-1:
log orders A-1 for Ann #1 journey_started card, customer, discount, gift_card, order, parcel, price
log orders A-1 for Ann #2 attempt_started lookup, attempt 1
log orders A-1 for Ann #3 service_provided lookup gets order_book from ledger, a ch38_declaring_by_hand::Ledger
the order book has no order A-1 yet
log orders A-1 for Ann #4 step_succeeded lookup
log orders A-1 for Ann #5 contribution_committed completed
log orders A-1 for Ann #6 hook_called already_completed: on step success of lookup
log orders A-1 for Ann #7 attempt_started price, attempt 1
log orders A-1 for Ann #8 step_succeeded price
log orders A-1 for Ann #9 contribution_committed amount
log orders A-1 for Ann #10 attempt_started charge, attempt 1
log orders A-1 for Ann #11 service_provided charge gets payments from bank, a ch38_declaring_by_hand::Bank
the bank charges 30 for order A-1
log orders A-1 for Ann, receipt R-A-1 #12 global_metadata_set receipt
log orders A-1 for Ann, receipt R-A-1 #13 step_succeeded charge
log orders A-1 for Ann, receipt R-A-1 #14 contribution_committed receipt
log orders A-1 for Ann, receipt R-A-1 #15 hook_called audit: on step success of charge
log orders A-1 for Ann, receipt R-A-1 #16 contribution_committed audit
log orders A-1 for Ann, receipt R-A-1 #17 attempt_started ship, attempt 1
log orders A-1 for Ann, receipt R-A-1 #18 input_adapter_supplied ship gets weight from parcels
log orders A-1 for Ann, receipt R-A-1 #19 service_provided ship gets carrier from carriers, a ch38_declaring_by_hand::MainCarrier
the main carrier books order A-1
log orders A-1 for Ann, receipt R-A-1 #20 step_info ship: the parcel leaves with receipt R-A-1
log orders A-1 for Ann, receipt R-A-1 #21 step_succeeded ship
log orders A-1 for Ann, receipt R-A-1 #22 contribution_committed tracking
log orders A-1 for Ann, receipt R-A-1 #23 optional_input_absent code
email to Ann: Order A-1 is on its way, tracking T-A-1.
log orders A-1 for Ann, receipt R-A-1 #24 hook_called notify: on workflow success
log orders A-1 for Ann, receipt R-A-1 #25 journey_succeeded
the journey succeeded, with receipt R-A-1 and tracking T-A-1
audit: A-1: charge took 30 on attempt 1, receipt R-A-1
Jo's order A-8:
log orders A-8 for Jo #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-8 for Jo #2 attempt_started lookup, attempt 1
log orders A-8 for Jo #3 service_provided lookup gets order_book from ledger, a ch38_declaring_by_hand::Ledger
the order book has no order A-8 yet
log orders A-8 for Jo #4 step_succeeded lookup
log orders A-8 for Jo #5 contribution_committed completed
log orders A-8 for Jo #6 hook_called already_completed: on step success of lookup
log orders A-8 for Jo #7 attempt_started price, attempt 1
log orders A-8 for Jo #8 optional_input_absent discount
log orders A-8 for Jo #9 step_succeeded price
log orders A-8 for Jo #10 contribution_committed amount
log orders A-8 for Jo #11 attempt_started charge, attempt 1
log orders A-8 for Jo #12 service_provided charge gets payments from bank, a ch38_declaring_by_hand::Bank
the bank declines card jo-2024: it has expired
log orders A-8 for Jo #13 step_failed charge: card-expired
log orders A-8 for Jo #14 step_rescuing charge: failure, rescued by charge_rescue
log orders A-8 for Jo #15 attempt_started charge_rescue, attempt 1
log orders A-8 for Jo #16 service_provided charge_rescue gets cards from cards, a dyn ch38_declaring_by_hand::Cards, with eu
the card registry in eu says card jo-2024 was replaced by jo-2029
log orders A-8 for Jo #17 step_succeeded charge_rescue
log orders A-8 for Jo #18 contribution_committed card
log orders A-8 for Jo #19 data_overwritten card
log orders A-8 for Jo #20 step_rescued charge by charge_rescue
log orders A-8 for Jo #21 attempt_started charge, attempt 2
the bank charges 30 for order A-8
log orders A-8 for Jo, receipt R-A-8 #22 global_metadata_set receipt
log orders A-8 for Jo, receipt R-A-8 #23 step_succeeded charge
log orders A-8 for Jo, receipt R-A-8 #24 contribution_committed receipt
log orders A-8 for Jo, receipt R-A-8 #25 hook_called audit: on step success of charge
log orders A-8 for Jo, receipt R-A-8 #26 contribution_committed audit
log orders A-8 for Jo, receipt R-A-8 #27 attempt_started ship, attempt 1
log orders A-8 for Jo, receipt R-A-8 #28 input_adapter_supplied ship gets weight from parcels
log orders A-8 for Jo, receipt R-A-8 #29 service_provided ship gets carrier from carriers, a ch38_declaring_by_hand::MainCarrier
the main carrier books order A-8
log orders A-8 for Jo, receipt R-A-8 #30 step_info ship: the parcel leaves with receipt R-A-8
log orders A-8 for Jo, receipt R-A-8 #31 step_succeeded ship
log orders A-8 for Jo, receipt R-A-8 #32 contribution_committed tracking
log orders A-8 for Jo, receipt R-A-8 #33 optional_input_absent code
email to Jo: Order A-8 is on its way, tracking T-A-8.
log orders A-8 for Jo, receipt R-A-8 #34 hook_called notify: on workflow success
log orders A-8 for Jo, receipt R-A-8 #35 journey_succeeded
the journey succeeded, with receipt R-A-8 and tracking T-A-8
audit: A-8: charge took 30 on attempt 2, receipt R-A-8Ann's order A-1:
log orders A-1 for Ann #1 journey_started card, customer, discount, gift_card, order, parcel, price
log orders A-1 for Ann #2 attempt_started lookup, attempt 1
log orders A-1 for Ann #3 service_provided lookup gets order_book from ledger, a ch38_declaring_by_hand_async::Ledger
the order book has no order A-1 yet
log orders A-1 for Ann #4 step_succeeded lookup
log orders A-1 for Ann #5 contribution_committed completed
log orders A-1 for Ann #6 hook_called already_completed: on step success of lookup
log orders A-1 for Ann #7 attempt_started price, attempt 1
log orders A-1 for Ann #8 step_succeeded price
log orders A-1 for Ann #9 contribution_committed amount
log orders A-1 for Ann #10 attempt_started charge, attempt 1
log orders A-1 for Ann #11 service_provided charge gets payments from bank, a ch38_declaring_by_hand_async::Bank
the bank charges 30 for order A-1
log orders A-1 for Ann, receipt R-A-1 #12 global_metadata_set receipt
log orders A-1 for Ann, receipt R-A-1 #13 step_succeeded charge
log orders A-1 for Ann, receipt R-A-1 #14 contribution_committed receipt
log orders A-1 for Ann, receipt R-A-1 #15 hook_called audit: on step success of charge
log orders A-1 for Ann, receipt R-A-1 #16 contribution_committed audit
log orders A-1 for Ann, receipt R-A-1 #17 attempt_started ship, attempt 1
log orders A-1 for Ann, receipt R-A-1 #18 input_adapter_supplied ship gets weight from parcels
log orders A-1 for Ann, receipt R-A-1 #19 service_provided ship gets carrier from carriers, a ch38_declaring_by_hand_async::MainCarrier
the main carrier books order A-1
log orders A-1 for Ann, receipt R-A-1 #20 step_info ship: the parcel leaves with receipt R-A-1
log orders A-1 for Ann, receipt R-A-1 #21 step_succeeded ship
log orders A-1 for Ann, receipt R-A-1 #22 contribution_committed tracking
log orders A-1 for Ann, receipt R-A-1 #23 optional_input_absent code
email to Ann: Order A-1 is on its way, tracking T-A-1.
log orders A-1 for Ann, receipt R-A-1 #24 hook_called notify: on workflow success
log orders A-1 for Ann, receipt R-A-1 #25 journey_succeeded
order A-1 succeeded
audit: A-1: charge took 30 on attempt 1, receipt R-A-1
Jo's order A-8:
log orders A-8 for Jo #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-8 for Jo #2 attempt_started lookup, attempt 1
log orders A-8 for Jo #3 service_provided lookup gets order_book from ledger, a ch38_declaring_by_hand_async::Ledger
the order book has no order A-8 yet
log orders A-8 for Jo #4 step_succeeded lookup
log orders A-8 for Jo #5 contribution_committed completed
log orders A-8 for Jo #6 hook_called already_completed: on step success of lookup
log orders A-8 for Jo #7 attempt_started price, attempt 1
log orders A-8 for Jo #8 optional_input_absent discount
log orders A-8 for Jo #9 step_succeeded price
log orders A-8 for Jo #10 contribution_committed amount
log orders A-8 for Jo #11 attempt_started charge, attempt 1
log orders A-8 for Jo #12 service_provided charge gets payments from bank, a ch38_declaring_by_hand_async::Bank
the bank declines card jo-2024: it has expired
log orders A-8 for Jo #13 step_failed charge: card-expired
log orders A-8 for Jo #14 step_rescuing charge: failure, rescued by charge_rescue
log orders A-8 for Jo #15 attempt_started charge_rescue, attempt 1
log orders A-8 for Jo #16 service_provided charge_rescue gets cards from cards, a dyn ch38_declaring_by_hand_async::Cards, with eu
the card registry in eu says card jo-2024 was replaced by jo-2029
log orders A-8 for Jo #17 step_succeeded charge_rescue
log orders A-8 for Jo #18 contribution_committed card
log orders A-8 for Jo #19 data_overwritten card
log orders A-8 for Jo #20 step_rescued charge by charge_rescue
log orders A-8 for Jo #21 attempt_started charge, attempt 2
the bank charges 30 for order A-8
log orders A-8 for Jo, receipt R-A-8 #22 global_metadata_set receipt
log orders A-8 for Jo, receipt R-A-8 #23 step_succeeded charge
log orders A-8 for Jo, receipt R-A-8 #24 contribution_committed receipt
log orders A-8 for Jo, receipt R-A-8 #25 hook_called audit: on step success of charge
log orders A-8 for Jo, receipt R-A-8 #26 contribution_committed audit
log orders A-8 for Jo, receipt R-A-8 #27 attempt_started ship, attempt 1
log orders A-8 for Jo, receipt R-A-8 #28 input_adapter_supplied ship gets weight from parcels
log orders A-8 for Jo, receipt R-A-8 #29 service_provided ship gets carrier from carriers, a ch38_declaring_by_hand_async::MainCarrier
the main carrier books order A-8
log orders A-8 for Jo, receipt R-A-8 #30 step_info ship: the parcel leaves with receipt R-A-8
log orders A-8 for Jo, receipt R-A-8 #31 step_succeeded ship
log orders A-8 for Jo, receipt R-A-8 #32 contribution_committed tracking
log orders A-8 for Jo, receipt R-A-8 #33 optional_input_absent code
email to Jo: Order A-8 is on its way, tracking T-A-8.
log orders A-8 for Jo, receipt R-A-8 #34 hook_called notify: on workflow success
log orders A-8 for Jo, receipt R-A-8 #35 journey_succeeded
order A-8 succeeded
audit: A-8: charge took 30 on attempt 2, receipt R-A-8A workflow put together wrongly
This small shop has steps and policies of its own, and parcels_for declares its adapter:
/// Charges an order's price.
#[derive(Step)]
struct Charge {
#[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");
}
contributor.contribute("charged", self.price);
Self::succeed()
}
}
/// Ships an order's parcel, of this weight in kg, through the carrier.
#[derive(Step)]
struct Ship {
#[input]
#[expect(dead_code, reason = "the shipping is declared, never run")]
weight: i64,
}
#[step]
impl Ship {
fn run(self) -> Result<Outcome, Error> {
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,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<Option<OnSuccess>, Error> {
contributor.contribute("audit", format!("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,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<Option<OnSuccess>, Error> {
contributor.contribute("receipt", format!("R-{charged}"));
Self::carry_on()
}
}
/// The shop's workflow, which holds nothing of its own.
struct Orders;
/// How the warehouse describes an order's parcel: its weight in kg, and its size in cm.
#[derive(Clone, Serialize, Deserialize)]
struct Parcel {
weight: i64,
size: [i64; 3],
}
const PARCEL: Input<Parcel> = Input::new("parcel");
/// Gives the shipping the weight of the order's parcel. Every other input is read from the data
/// bag.
fn parcels(
_: &Orders,
mut got: Requested<'_, Orders, InputAdapter>,
) -> Result<Option<AnyValue>, Error> {
if got.key() != "weight" {
return Ok(None);
}
Ok(Some(AnyValue::new(got.from_workflow(&PARCEL)?.weight)))
}
/// Declares the parcels adapter, attached to the step of this name.
fn parcels_for(step: StepName) -> InputAdapterDescriptor<Orders> {
InputAdapterDescriptor::new("parcels", step, parcels)
.needing(HookNeeds::new().from_workflow(&PARCEL))
}It is declared by hand with three mistakes: ship listed twice, two policies of the charge that both define on step success, and the parcels adapter attached to shipping, a step the workflow doesn't have:
let refused = WorkflowDescriptor::builder("orders")
.step(
Charge::descriptor(step_name!("charge"))
.policy(Audit::descriptor())
.policy(Receipts::descriptor()),
)
.step(Ship::descriptor(step_name!("ship")))
.step(Ship::descriptor(step_name!("ship")).retry_budget(1))
.input_adapter(parcels_for(step_name!("shipping")))
.build();The program compiles. build returns the violations instead of the workflow, and main reads them:
match refused {
Ok(_) => println!("the workflow was built"),
Err(violations) => {
println!("the workflow was refused:");
for violation in &violations {
println!(" {violation}");
}
}
}Fixed, it builds, and main prints its listing:
let built = WorkflowDescriptor::builder("orders")
.step(Charge::descriptor(step_name!("charge")).policy(Audit::descriptor()))
.step(Ship::descriptor(step_name!("ship")).retry_budget(1))
.input_adapter(parcels_for(step_name!("ship")))
.build();
match built {
Ok(orders) => {
println!("the fixed workflow was built:");
for listed in orders.listing() {
println!(" {}. {}", listed.position, listed.step);
}
}
Err(violations) => println!("still refused: {violations}"),
}It prints:
the workflow was refused:
duplicate step name: ship
hook defined twice: on step success on step charge
input adapter for unknown step: parcels on shipping
the fixed workflow was built:
1. charge
2. shipHow it works
- Both forms make the same descriptors. A workflow is a workflow descriptor: its name, a step descriptor for each step in order, its policies, reporters, ID generator, input adapters and providers. The attributes write them from your types, and the long form writes them by hand. The executor runs either the same way.
- Each attribute stands for a trait or a builder.
#[step]makes the step'sStepDescriptor, throughdescriptor(name), from what#[derive(Step)]says the step needs.#[step_policy]implements a trait for each hook and makes aStepPolicyDescriptor.#[provider]makes aServiceProviderDescriptor, and#[role]implementsProvidesfor the workflow.#[workflow(...)]builds theWorkflowDescriptorwithWorkflowDescriptor::builder, once. Andlocal_executor_for!(Orders)stands forLocalExecutor::new(), orAsyncLocalExecutor::new()for an asynchronous workflow. - A step is built afresh for every attempt. The factory builds
Pricefrom what was resolved for that attempt, which is whyPrice<'a>borrows its contributor: it lives for one attempt. Nothing carries over to the next. buildchecks the declaration, as admission. It returnsResult<WorkflowDescriptor<Orders>, Violations>: the workflow, or every violation found, each displayed as the specification names it. The attributes run the same checks while the program compiles, as chapter 37 showed; the long form runs them when the program starts.- The two forms mix. Most of the shop here is still declared with the attributes. Write by hand only the part that needs it.
- When you need the long form. A declaration put together from configuration when the program starts, such as a gift-wrapping step only in the shops that offer it; violations read as values; a step or policy generic over a type, which the attributes refuse; or a program built without the attributes at all, with
default-features = falseonitinera, which turns off itsmacrosoption. Otherwise, the attributes say the same in fewer lines.
The shop can now declare every part of its workflow, in either form. What the book hasn't said is how to decide, for a new rule of the shop, which part it belongs in.
Outcomes
We have shown what the attributes write, how to write it by hand, and how a workflow declared by hand reports its mistakes.
| The usual way | With Itinera |
|---|---|
| Wiring written by a framework's annotations is often hidden in code nobody reads. | The attributes write public descriptors, which you can write yourself, part by part. |
| A wiring mistake found at start-up often stops at the first one. | build returns every violation, as values. |