Giving the rescue step a service of its own
What you want to do
The bank now answers which card replaced an expired one from its card registry, a system of its own in each region, and its library hands back the registry's client already boxed. You want the rescue step to ask the registry instead of the payment system, and no other step to have it.
The code
The shop is chapter 34's. The registry is a service with a trait of its own, Cards, and the bank's library makes its client for a region:
/// A card registry: it says which card replaced one, if any did.
trait Cards: Send + Sync {
fn replacement(&self, card: &str) -> Option<String>;
}
/// The bank's own library: it connects to the bank's card registry for a region, and hands back
/// its client, boxed.
fn card_registry(region: &str) -> Box<dyn Cards> {
Box::new(Registry {
region: region.to_owned(),
this_year: 2026,
})
}/// A card registry: it says which card replaced one, if any did. Its answer comes over the network.
#[async_trait]
trait Cards: Send + Sync {
async fn replacement(&self, card: &str) -> Option<String>;
}
/// The bank's own library: it connects to the bank's card registry for a region, and hands back
/// its client, boxed.
fn card_registry(region: &str) -> Box<dyn Cards> {
Box::new(Registry {
region: region.to_owned(),
this_year: 2026,
})
}The rescue step asks for the registry under the key cards, though it calls it registry, and reads the order's card, under the key card, as expired:
#[derive(RescueStep)]
struct RenewCard {
#[input("card")]
expired: String,
#[reason]
reason: Reason,
}
#[rescue_step]
impl RenewCard {
fn run(
self,
#[service("cards")] registry: &dyn Cards,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<RescueOutcome, Error> {
if self.reason.code() != "card-expired" {
return Self::fail("not-repairable");
}
let Some(card) = registry.replacement(&self.expired) else {
return Self::fail("not-replaced");
};
contributor.contribute("card", card);
Self::succeed()
}
}#[derive(RescueStep)]
struct RenewCard {
#[input("card")]
expired: String,
#[reason]
reason: Reason,
}
#[rescue_step]
impl RenewCard {
async fn run(
self,
#[service("cards")] registry: &dyn Cards,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<RescueOutcome, Error> {
if self.reason.code() != "card-expired" {
return Self::fail("not-repairable");
}
let Some(card) = registry.replacement(&self.expired).await else {
return Self::fail("not-replaced");
};
contributor.contribute("card", card);
Self::succeed()
}
}The workflow's provider for cards is attached to the rescue step alone, by its name, and hands over the library's boxed client with the region it is for:
/// The bank's card registry in Europe, for the charge's rescue step only.
#[provider(steps = ["charge_rescue"])]
fn cards(&self) -> Result<Provided<dyn Cards>, Error> {
Ok(Provided::from_box(card_registry("eu")).with_data("eu".to_owned()))
}The log's detail now hands the data a provider supplied to supplied, in its arm for service_provided:
EventBody::ServiceProvided {
step,
key,
provider,
implementation,
data,
..
} => Some(format!(
"{} gets {key} from {provider}, a {implementation}{}",
step.step,
supplied(data.as_ref())
)),supplied writes the data when it is text, and nothing otherwise:
/// The data a provider supplied with a service, when it is text.
fn supplied(data: Option<&AnyValue>) -> String {
data.and_then(|data| data.downcast_ref::<String>())
.map(|data| format!(", with {data}"))
.unwrap_or_default()
}In the asynchronous form, Cards and its implementation take #[async_trait], and the rescue step awaits replacement. order is chapter 33's, journey chapter 26's and Audited chapter 34's. main runs Jo's order, whose expired card the rescue step renews, and prints:
Jo's order:
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 ch35_giving_the_rescue_step_a_service_of_its_own::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 ch35_giving_the_rescue_step_a_service_of_its_own::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 ch35_giving_the_rescue_step_a_service_of_its_own::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 ch35_giving_the_rescue_step_a_service_of_its_own::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-8log 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 ch35_giving_the_rescue_step_a_service_of_its_own_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 ch35_giving_the_rescue_step_a_service_of_its_own_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 ch35_giving_the_rescue_step_a_service_of_its_own_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 ch35_giving_the_rescue_step_a_service_of_its_own_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-8How it works
- A request can name its key. Until now, a step's input and service were found under the name of the field or the parameter.
#[service("cards")]asks for the service under the keycards, and#[input("card")]reads the data bag'scardinto the fieldexpired. The step names what it holds for its own code, and the key says what the journey calls it. - A provider can serve some steps only.
steps = ["charge_rescue"]attaches the registry's provider to the rescue step, named by its own name in the workflow, as chapter 14 showed. The bank's provider, chapter 12's, attached to no step, still serves every step that asks forpayments, which is now the charge alone. - A provider can hand over a service already boxed.
Provided::from_boxtakes theBox<dyn Cards>the bank's library made, as it is. The box hides the client's own type, soservice_provided, event 16, names the implementation as the trait,dyn …::Cards, whereprovide!named the type inside, as chapter 8 showed. - A provider can supply data with a service.
with_datagives the region along with the client, andservice_providedcarries it. The log now writes the data a provider supplied after the implementation:with eu. It says which registry served the rescue step, which the boxed type can't. Like everything an event carries, it is for the reporters: the rescue step never sees it.
Jo's card is now renewed by the registry, and only the rescue step can reach it. Jo's order came complete, with its number and its customer's name. But the website has started sending orders before they are finished: one arrives without an order number, another without the customer's name.
Outcomes
We have shown how a step names the keys of what it asks for, and how a provider serves some steps only, with a service already boxed and data that says which one it is.
| The usual way | With Itinera |
|---|---|
| A function's parameters often have to be named as its callers name the values, or a layer between them renames everything. | A step names its inputs and services for its own code, and says under which key the journey has each. |
| A client that a vendor's library builds often hides its type, so nothing records which one a request used. | The provider hands the boxed client over as it is, with data saying which one it is, on service_provided. |