Writing a client
#[openapi_trait::client(...)] generates the same models as the server macro,
plus a transport-agnostic {Mod}Client trait:
#[openapi_trait::client("examples/pets.openapi.yaml")]
pub mod pets {}
You can implement that trait yourself, or use the generated reqwest implementation.
Implementing the trait yourself
Every method takes the operation’s request plus an
Option<openapi_trait::RequestOptions> and returns the operation’s response
enum:
/// An in-memory stand-in for the real API.
struct FakePets;
impl pets::PetsClient for FakePets {
type Error = std::convert::Infallible;
async fn list_pets(
&self,
_req: pets::list_pets::Request,
_options: Option<openapi_trait::RequestOptions>,
) -> Result<pets::list_pets::Response, Self::Error> {
Ok(pets::list_pets::Response::Status200(vec![]))
}
async fn create_pet(
&self,
req: pets::create_pet::Request,
_options: Option<openapi_trait::RequestOptions>,
) -> Result<pets::create_pet::Response, Self::Error> {
Ok(pets::create_pet::Response::Status201(pets::Pet {
id: 1,
name: req.body.name,
status: req.body.status,
}))
}
async fn get_pet(
&self,
_req: pets::get_pet::Request,
_options: Option<openapi_trait::RequestOptions>,
) -> Result<pets::get_pet::Response, Self::Error> {
Ok(pets::get_pet::Response::Status404)
}
}
Code that depends on the trait doesn’t care which implementation it gets, so fakes like this one make good test doubles:
/// Code written against the trait works with any implementation.
async fn pet_name<C: pets::PetsClient>(client: &C, pet_id: i64) -> Option<String> {
match client
.get_pet(pets::get_pet::Request { pet_id }, None)
.await
{
Ok(pets::get_pet::Response::Status200(pet)) => Some(pet.name),
_ => None,
}
}
Unlike the server trait, client methods have no default bodies. You implement every operation.
The reqwest client
With the default reqwest-client feature, the macro also generates a blanket
{Mod}Client implementation. Any type can use it by deriving
openapi_trait::ReqwestClient:
/// Holds everything the generated client implementation needs.
#[derive(Clone, openapi_trait::ReqwestClient)]
struct PetsHttpClient {
/// The HTTP client used for every request.
#[openapi_trait(client)]
http: openapi_trait::reqwest::Client,
/// Base URL that operation paths are appended to.
#[openapi_trait(base_url)]
base_url: String,
/// Credentials for the spec's security schemes.
#[openapi_trait(auth)]
auth: pets::PetsAuthState,
}
| Field attribute | Field type | Required |
|---|---|---|
#[openapi_trait(client)] | openapi_trait::reqwest::Client | yes |
#[openapi_trait(base_url)] | String | yes |
#[openapi_trait(auth)] | {Mod}AuthState | when the spec declares security schemes |
A field named auth is picked up without the attribute.
Then call operations through the trait:
use pets::PetsClient as _;
use pets::PetsClientAuth as _;
let client = PetsHttpClient {
http: openapi_trait::reqwest::Client::new(),
base_url: "http://127.0.0.1:3000".into(),
auth: pets::PetsAuthState::default(),
}
.with_bearer_auth("secret");
let created = client
.create_pet(
pets::create_pet::Request {
body: pets::NewPet {
name: "doggie".into(),
status: Some(pets::PetStatus::Available),
},
},
None,
)
.await?;
println!("created: {created:?}");
let all = client
.list_pets(pets::list_pets::Request { limit: Some(10) }, None)
.await?;
println!("listed: {all:?}");
The generated implementation returns Reqwest{Mod}ClientError, an enum
covering transport errors (Transport), response bodies that don’t parse as
the declared type (Decode, with the serde_json error as its source),
missing credentials, malformed response headers, and status codes the spec
doesn’t declare (UnexpectedStatus, which includes the body). When the
operation has a default response, an undeclared status isn’t an error: it’s
decoded into Response::Default { status, body, … }.
The enum is #[non_exhaustive], so variants can be added without a breaking
change. That only matters for crates that depend on the one containing the
generated module: inside it, a match can still list every variant.
Per-request headers and auth
Pass Some(RequestOptions) to add headers or override auth for a single call
without changing the client:
let pet = client
.get_pet(
pets::get_pet::Request { pet_id: 1 },
Some(
openapi_trait::RequestOptions::new()
.bearer_auth("another-token")
.header("X-Request-Id", "abc-789"),
),
)
.await?;
RequestOptions provides header(name, value), bearer_auth(token) and
basic_auth(username, Option<password>). Options are applied after the
operation’s declared headers and credentials:
bearer_authandbasic_authreplace anyAuthorizationheader set by the client’s security scheme. They also satisfy the credential check, so a call can succeed even if the client has no stored credentials.headerappends to the request.
Binary downloads
Operations that return a binary body give you an openapi_trait::ByteStream,
a futures_core::Stream of bytes::Bytes chunks. Read it incrementally instead
of buffering the whole response.
Without reqwest
To generate only the transport-agnostic trait and drop the reqwest
dependency:
[dependencies]
openapi-trait = { version = "1", default-features = false }