roas_arazzo_executor/lib.rs
1//! Executes OpenAPI Arazzo workflows.
2//!
3//! An Arazzo description is a program: ordered steps that call API
4//! operations, assert on the responses, name outputs, and branch on
5//! success or failure. [`roas-arazzo`](https://crates.io/crates/roas-arazzo)
6//! parses and validates one; this crate runs it.
7//!
8//! ```no_run
9//! # use roas_arazzo::v1_1::Description;
10//! # use roas_arazzo_executor::{Options, execute};
11//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
12//! # let description: Description = serde_json::from_str("{}")?;
13//! # let mut client = roas_arazzo_executor::testing::Fake::default();
14//! let options = Options::new().workflow("buyPet");
15//! let report = execute(&description, &options, &mut client)?;
16//! println!("{report}");
17//! # Ok(()) }
18//! ```
19//!
20//! ## No IO of its own
21//!
22//! The engine decides *what* to send and asks a client to send it, so
23//! the same engine runs under a blocking client, an async one, or a fake
24//! that never touches a network. Implement [`HttpClient`] (or
25//! [`AsyncHttpClient`]), or enable the `reqwest` feature for ready-made
26//! ones.
27//!
28//! Source descriptions are loaded the same way: fetching them is IO, so
29//! the caller supplies the parsed documents through
30//! [`Options::source`].
31//!
32//! ## What it does not do yet
33//!
34//! AsyncAPI steps (`channelPath` / `action`), XPath criteria and
35//! selectors, `inputs` schema validation, and parallel `dependsOn`
36//! execution. Each is reported where it is met rather than passed over,
37//! so a run never looks successful because something was skipped.
38
39mod criterion;
40mod expression;
41mod http;
42mod operation;
43mod report;
44mod run;
45mod select;
46
47pub mod testing;
48
49#[cfg(feature = "reqwest")]
50mod client;
51
52pub use criterion::CriterionError;
53pub use expression::ExpressionError;
54pub use http::{
55 AsyncHttpClient, ClientError, HttpClient, HttpRequest, HttpResponse, SendFuture, SleepFuture,
56};
57pub use report::{
58 CriterionOutcome, ExecutionError, ExecutionReport, Outcome, Performed, StepRecord,
59};
60pub use run::{Options, Progress, Run};
61pub use select::SelectError;
62
63#[cfg(feature = "reqwest")]
64pub use client::Client;
65
66use roas_arazzo::v1_1::Description;
67
68/// Run a workflow, performing every request with `client`.
69///
70/// The workflow is [`Options::workflow`], or the first one in the
71/// description. The report says what each step did; a step that fails
72/// its criteria is part of the report, not an error.
73///
74/// # Errors
75///
76/// [`ExecutionError`] when the run cannot continue: an unknown workflow,
77/// an operation that cannot be resolved, an expression that names
78/// nothing, a client failure, or a limit reached.
79pub fn execute<C: HttpClient + ?Sized>(
80 description: &Description,
81 options: &Options,
82 client: &mut C,
83) -> Result<ExecutionReport, ExecutionError> {
84 let mut run = Run::start(description, options)?;
85 loop {
86 match run.advance()? {
87 Progress::Send(request) => {
88 let response = client.send(&request).map_err(ExecutionError::from)?;
89 run.supply(response)?;
90 }
91 Progress::Wait(duration) => std::thread::sleep(duration),
92 Progress::Done(report) => return Ok(*report),
93 }
94 }
95}
96
97/// Run a workflow, performing every request with an async `client`.
98///
99/// The same engine as [`execute`]; only the waiting differs.
100///
101/// # Errors
102///
103/// As [`execute`].
104pub async fn execute_async<C: AsyncHttpClient + ?Sized>(
105 description: &Description,
106 options: &Options,
107 client: &mut C,
108) -> Result<ExecutionReport, ExecutionError> {
109 let mut run = Run::start(description, options)?;
110 loop {
111 match run.advance()? {
112 Progress::Send(request) => {
113 let response = client.send(&request).await.map_err(ExecutionError::from)?;
114 run.supply(response)?;
115 }
116 Progress::Wait(duration) => client.sleep(duration).await,
117 Progress::Done(report) => return Ok(*report),
118 }
119 }
120}
121
122/// Run an Arazzo v1.0 description, upconverting it to v1.1 first.
123///
124/// # Errors
125///
126/// As [`execute`].
127#[cfg(feature = "v1_0")]
128pub fn execute_v1_0<C: HttpClient + ?Sized>(
129 description: &roas_arazzo::v1_0::Description,
130 options: &Options,
131 client: &mut C,
132) -> Result<ExecutionReport, ExecutionError> {
133 execute(&Description::from(description.clone()), options, client)
134}