openapi_trait/lib.rs
1//! Generate typed Rust traits from `OpenAPI` specifications.
2//!
3//! This crate exposes the [`axum`] and [`client`] attribute macros, which read
4//! an `OpenAPI` specification file at compile time and generate inside the
5//! annotated `mod`.
6//!
7//! # Examples
8//!
9//! ```rust
10//! #[openapi_trait::axum("assets/testdata/petstore.openapi.yaml")]
11//! pub mod petstore {}
12//!
13//! use petstore::PetstoreApi as _;
14//!
15//! #[derive(Clone)]
16//! struct MyServer;
17//!
18//! #[derive(Clone)]
19//! struct AppState;
20//!
21//! impl petstore::PetstoreApi<AppState> for MyServer {
22//! type Error = petstore::NotImplemented;
23//!
24//! async fn get_pet_by_id(
25//! &self,
26//! req: petstore::GetPetByIdRequest,
27//! _auth: petstore::ApiKey,
28//! _state: axum::extract::State<AppState>,
29//! _headers: axum::http::HeaderMap,
30//! ) -> Result<petstore::GetPetByIdResponse, Self::Error> {
31//! Ok(petstore::GetPetByIdResponse::Status200(petstore::Pet {
32//! id: Some(req.pet_id),
33//! name: "doggie".into(),
34//! photo_urls: vec![],
35//! category: None,
36//! tags: None,
37//! status: None,
38//! }))
39//! }
40//! }
41//!
42//! let app: axum::Router = MyServer.router().with_state(AppState);
43//! ```
44//!
45//! The generated trait names come from the annotated module name, so `mod petstore {}`
46//! produces `petstore::PetstoreApi` and `petstore::PetstoreClient`.
47//!
48//! The `reqwest-client` feature is enabled by default. It adds [`ReqwestClient`],
49//! [`ReqwestClientCore`], and the [`reqwest`] re-export used by the generated blanket
50//! client implementation.
51//!
52//! # Validation
53//!
54//! The non-default `validation` feature makes every generated model type derive
55//! [`serde_valid::Validate`](https://docs.rs/serde_valid) and gain
56//! `#[validate(...)]` field attributes reflecting the schema's constraints
57//! (`minLength`, `minimum`, `pattern`, `minItems`, `uniqueItems`, …). Bring the
58//! `Validate` trait into scope (`serde_valid` is re-exported by this crate under
59//! the feature) and call `.validate()`:
60//!
61//! ```toml
62//! openapi-trait = { version = "0.1", features = ["validation"] }
63//! ```
64//!
65//! ```ignore
66//! use openapi_trait::serde_valid::Validate as _;
67//! widget.validate()?; // Err if any declared constraint is violated
68//! ```
69//!
70//! Validation is opt-in and non-enforcing — nothing calls `.validate()` for you,
71//! and with the feature off the generated code is unchanged.
72
73#[doc(inline)]
74pub use openapi_trait_axum::openapi_trait as axum;
75
76#[doc(inline)]
77pub use openapi_trait_client::openapi_trait as client;
78
79/// Derive support for user-owned reqwest client carrier structs.
80///
81/// The derive looks for fields named `client` and `base_url` by default.
82/// Override those conventions with `#[openapi_trait(client)]` and
83/// `#[openapi_trait(base_url)]` on the corresponding fields.
84#[cfg(feature = "reqwest-client")]
85#[doc(inline)]
86pub use openapi_trait_client::ReqwestClient;
87
88/// Shared accessors used by generated reqwest client implementations.
89#[cfg(feature = "reqwest-client")]
90pub trait ReqwestClientCore {
91 /// Return the reqwest client used for outbound requests.
92 fn reqwest_client(&self) -> &reqwest::Client;
93
94 /// Return the base URL prepended to generated operation paths.
95 fn base_url(&self) -> &str;
96}
97
98/// Per-request transport options applied on top of the operation's own
99/// parameters.
100///
101/// Every generated client method takes a `RequestOptions` argument, letting you
102/// attach extra HTTP headers or authentication to a single request without
103/// re-instantiating the underlying client. Pass [`RequestOptions::default`]
104/// (or [`RequestOptions::new`]) when you have nothing to add.
105///
106/// The builder methods are chainable:
107///
108/// ```rust
109/// # #[cfg(feature = "reqwest-client")] {
110/// let options = openapi_trait::RequestOptions::new()
111/// .bearer_auth("token-123")
112/// .header("X-Request-Id", "abc");
113/// # let _ = options;
114/// # }
115/// ```
116///
117/// Extra [`header`]s are applied after the operation's declared headers, so a
118/// header set here is sent in addition to (and after) any same-named operation
119/// header. Authentication set via [`bearer_auth`] or [`basic_auth`], by
120/// contrast, *replaces* the `Authorization` header from a configured security
121/// scheme, so per-request credentials deterministically win.
122///
123/// [`header`]: Self::header
124/// [`bearer_auth`]: Self::bearer_auth
125/// [`basic_auth`]: Self::basic_auth
126#[derive(Debug, Clone, Default)]
127pub struct RequestOptions {
128 /// Extra headers, applied in insertion order.
129 headers: Vec<(String, String)>,
130 /// `Authorization: Bearer <token>` to attach, if any.
131 bearer_token: Option<String>,
132 /// `Authorization: Basic` credentials (username, optional password).
133 basic_auth: Option<(String, Option<String>)>,
134}
135
136impl RequestOptions {
137 /// Create an empty set of request options.
138 #[must_use]
139 pub fn new() -> Self {
140 Self::default()
141 }
142
143 /// Add an extra header to the request.
144 ///
145 /// Invalid header names or values are surfaced by reqwest when the request
146 /// is sent, matching `reqwest::RequestBuilder::header` semantics.
147 #[must_use]
148 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
149 self.headers.push((name.into(), value.into()));
150 self
151 }
152
153 /// Attach an `Authorization: Bearer <token>` header to the request.
154 ///
155 /// This replaces any `Authorization` header a configured security scheme
156 /// would otherwise set, so the per-request token always wins. Calling it
157 /// clears any credentials previously set via [`basic_auth`](Self::basic_auth).
158 #[must_use]
159 pub fn bearer_auth(mut self, token: impl Into<String>) -> Self {
160 self.bearer_token = Some(token.into());
161 self.basic_auth = None;
162 self
163 }
164
165 /// Attach `Authorization: Basic` credentials to the request.
166 ///
167 /// This replaces any `Authorization` header a configured security scheme
168 /// would otherwise set, so the per-request credentials always win. Calling
169 /// it clears any token previously set via [`bearer_auth`](Self::bearer_auth).
170 #[must_use]
171 pub fn basic_auth(mut self, username: impl Into<String>, password: Option<String>) -> Self {
172 self.basic_auth = Some((username.into(), password));
173 self.bearer_token = None;
174 self
175 }
176
177 /// Whether these options carry per-request authentication.
178 ///
179 /// Returns `true` once [`bearer_auth`](Self::bearer_auth) or
180 /// [`basic_auth`](Self::basic_auth) has been set. The generated client uses
181 /// this to skip its "missing credential" pre-flight check: a request that
182 /// supplies its own credentials is valid even when the client was built
183 /// without a configured security scheme, letting you authenticate a single
184 /// request without baking credentials into the client.
185 #[must_use]
186 pub const fn provides_auth(&self) -> bool {
187 self.bearer_token.is_some() || self.basic_auth.is_some()
188 }
189
190 /// Apply these options to a reqwest request builder.
191 ///
192 /// Used by the generated reqwest-backed client implementation; you should
193 /// not normally need to call it directly.
194 #[cfg(feature = "reqwest-client")]
195 pub fn apply(self, mut request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
196 for (name, value) in self.headers {
197 request = request.header(name.as_str(), value.as_str());
198 }
199 // Build the `Authorization` value ourselves and apply it with replace
200 // (rather than append) semantics, so a per-request credential overrides
201 // any `Authorization` header a security scheme already set instead of
202 // sending a duplicate. `bearer_auth`/`basic_auth` keep the two mutually
203 // exclusive, so at most one branch runs.
204 if let Some(token) = self.bearer_token {
205 match reqwest::header::HeaderValue::try_from(format!("Bearer {token}")) {
206 Ok(mut value) => {
207 value.set_sensitive(true);
208 request = replace_authorization(request, value);
209 }
210 // Fall back to reqwest so an invalid token surfaces at send time.
211 Err(_) => request = request.bearer_auth(token),
212 }
213 } else if let Some((username, password)) = self.basic_auth {
214 use base64::Engine as _;
215 let credentials = password.as_ref().map_or_else(
216 || format!("{username}:"),
217 |password| format!("{username}:{password}"),
218 );
219 let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
220 match reqwest::header::HeaderValue::try_from(format!("Basic {encoded}")) {
221 Ok(mut value) => {
222 value.set_sensitive(true);
223 request = replace_authorization(request, value);
224 }
225 Err(_) => request = request.basic_auth(username, password),
226 }
227 }
228 request
229 }
230}
231
232/// Set `value` as the request's `Authorization` header, replacing any value an
233/// earlier layer (such as a security scheme) already set rather than appending a
234/// duplicate. Applying a single-entry [`HeaderMap`](reqwest::header::HeaderMap)
235/// via `RequestBuilder::headers` uses reqwest's replace semantics.
236#[cfg(feature = "reqwest-client")]
237fn replace_authorization(
238 request: reqwest::RequestBuilder,
239 value: reqwest::header::HeaderValue,
240) -> reqwest::RequestBuilder {
241 let mut headers = reqwest::header::HeaderMap::with_capacity(1);
242 headers.insert(reqwest::header::AUTHORIZATION, value);
243 request.headers(headers)
244}
245
246/// Sibling of [`ReqwestClientCore`] for clients that carry credentials.
247///
248/// Implemented automatically by [`ReqwestClient`] when the carrier struct has
249/// a field annotated `#[openapi_trait(auth)]` (or named `auth`). The generic
250/// `A` is the generated `{Mod}AuthState` struct for the spec.
251#[cfg(feature = "reqwest-client")]
252pub trait ReqwestClientAuth<A> {
253 /// Borrow the auth-state struct holding configured credentials.
254 fn auth_state(&self) -> &A;
255}
256
257#[cfg(feature = "reqwest-client")]
258pub use percent_encoding;
259#[cfg(feature = "reqwest-client")]
260pub use reqwest;
261
262pub use base64;
263pub use chrono;
264/// Re-export of [`form_urlencoded`], used by generated axum server code to
265/// decode raw query strings when applying `OpenAPI` `style`/`explode` rules.
266pub use form_urlencoded;
267pub use uuid;
268
269/// Re-export of [`serde_valid`], backing the `#[validate(...)]` attributes on
270/// generated model types when the `validation` feature is enabled.
271///
272/// Bring [`serde_valid::Validate`] into scope to call `model.validate()` on the
273/// generated structs and enums.
274#[cfg(feature = "validation")]
275pub use serde_valid;