Skip to main content

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;