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#[doc(inline)]
53pub use openapi_trait_axum::openapi_trait as axum;
54
55#[doc(inline)]
56pub use openapi_trait_client::openapi_trait as client;
57
58/// Derive support for user-owned reqwest client carrier structs.
59///
60/// The derive looks for fields named `client` and `base_url` by default.
61/// Override those conventions with `#[openapi_trait(client)]` and
62/// `#[openapi_trait(base_url)]` on the corresponding fields.
63#[cfg(feature = "reqwest-client")]
64#[doc(inline)]
65pub use openapi_trait_client::ReqwestClient;
66
67/// Shared accessors used by generated reqwest client implementations.
68#[cfg(feature = "reqwest-client")]
69pub trait ReqwestClientCore {
70    /// Return the reqwest client used for outbound requests.
71    fn reqwest_client(&self) -> &reqwest::Client;
72
73    /// Return the base URL prepended to generated operation paths.
74    fn base_url(&self) -> &str;
75}
76
77/// Per-request transport options applied on top of the operation's own
78/// parameters.
79///
80/// Every generated client method takes a `RequestOptions` argument, letting you
81/// attach extra HTTP headers or authentication to a single request without
82/// re-instantiating the underlying client. Pass [`RequestOptions::default`]
83/// (or [`RequestOptions::new`]) when you have nothing to add.
84///
85/// The builder methods are chainable:
86///
87/// ```rust
88/// # #[cfg(feature = "reqwest-client")] {
89/// let options = openapi_trait::RequestOptions::new()
90///     .bearer_auth("token-123")
91///     .header("X-Request-Id", "abc");
92/// # let _ = options;
93/// # }
94/// ```
95///
96/// Extra [`header`]s are applied after the operation's declared headers, so a
97/// header set here is sent in addition to (and after) any same-named operation
98/// header. Authentication set via [`bearer_auth`] or [`basic_auth`], by
99/// contrast, *replaces* the `Authorization` header from a configured security
100/// scheme, so per-request credentials deterministically win.
101///
102/// [`header`]: Self::header
103/// [`bearer_auth`]: Self::bearer_auth
104/// [`basic_auth`]: Self::basic_auth
105#[derive(Debug, Clone, Default)]
106pub struct RequestOptions {
107    /// Extra headers, applied in insertion order.
108    headers: Vec<(String, String)>,
109    /// `Authorization: Bearer <token>` to attach, if any.
110    bearer_token: Option<String>,
111    /// `Authorization: Basic` credentials (username, optional password).
112    basic_auth: Option<(String, Option<String>)>,
113}
114
115impl RequestOptions {
116    /// Create an empty set of request options.
117    #[must_use]
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Add an extra header to the request.
123    ///
124    /// Invalid header names or values are surfaced by reqwest when the request
125    /// is sent, matching `reqwest::RequestBuilder::header` semantics.
126    #[must_use]
127    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
128        self.headers.push((name.into(), value.into()));
129        self
130    }
131
132    /// Attach an `Authorization: Bearer <token>` header to the request.
133    ///
134    /// This replaces any `Authorization` header a configured security scheme
135    /// would otherwise set, so the per-request token always wins. Calling it
136    /// clears any credentials previously set via [`basic_auth`](Self::basic_auth).
137    #[must_use]
138    pub fn bearer_auth(mut self, token: impl Into<String>) -> Self {
139        self.bearer_token = Some(token.into());
140        self.basic_auth = None;
141        self
142    }
143
144    /// Attach `Authorization: Basic` credentials to the request.
145    ///
146    /// This replaces any `Authorization` header a configured security scheme
147    /// would otherwise set, so the per-request credentials always win. Calling
148    /// it clears any token previously set via [`bearer_auth`](Self::bearer_auth).
149    #[must_use]
150    pub fn basic_auth(mut self, username: impl Into<String>, password: Option<String>) -> Self {
151        self.basic_auth = Some((username.into(), password));
152        self.bearer_token = None;
153        self
154    }
155
156    /// Apply these options to a reqwest request builder.
157    ///
158    /// Used by the generated reqwest-backed client implementation; you should
159    /// not normally need to call it directly.
160    #[cfg(feature = "reqwest-client")]
161    pub fn apply(self, mut request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
162        for (name, value) in self.headers {
163            request = request.header(name.as_str(), value.as_str());
164        }
165        // Build the `Authorization` value ourselves and apply it with replace
166        // (rather than append) semantics, so a per-request credential overrides
167        // any `Authorization` header a security scheme already set instead of
168        // sending a duplicate. `bearer_auth`/`basic_auth` keep the two mutually
169        // exclusive, so at most one branch runs.
170        if let Some(token) = self.bearer_token {
171            match reqwest::header::HeaderValue::try_from(format!("Bearer {token}")) {
172                Ok(mut value) => {
173                    value.set_sensitive(true);
174                    request = replace_authorization(request, value);
175                }
176                // Fall back to reqwest so an invalid token surfaces at send time.
177                Err(_) => request = request.bearer_auth(token),
178            }
179        } else if let Some((username, password)) = self.basic_auth {
180            use base64::Engine as _;
181            let credentials = password.as_ref().map_or_else(
182                || format!("{username}:"),
183                |password| format!("{username}:{password}"),
184            );
185            let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
186            match reqwest::header::HeaderValue::try_from(format!("Basic {encoded}")) {
187                Ok(mut value) => {
188                    value.set_sensitive(true);
189                    request = replace_authorization(request, value);
190                }
191                Err(_) => request = request.basic_auth(username, password),
192            }
193        }
194        request
195    }
196}
197
198/// Set `value` as the request's `Authorization` header, replacing any value an
199/// earlier layer (such as a security scheme) already set rather than appending a
200/// duplicate. Applying a single-entry [`HeaderMap`](reqwest::header::HeaderMap)
201/// via `RequestBuilder::headers` uses reqwest's replace semantics.
202#[cfg(feature = "reqwest-client")]
203fn replace_authorization(
204    request: reqwest::RequestBuilder,
205    value: reqwest::header::HeaderValue,
206) -> reqwest::RequestBuilder {
207    let mut headers = reqwest::header::HeaderMap::with_capacity(1);
208    headers.insert(reqwest::header::AUTHORIZATION, value);
209    request.headers(headers)
210}
211
212/// Sibling of [`ReqwestClientCore`] for clients that carry credentials.
213///
214/// Implemented automatically by [`ReqwestClient`] when the carrier struct has
215/// a field annotated `#[openapi_trait(auth)]` (or named `auth`). The generic
216/// `A` is the generated `{Mod}AuthState` struct for the spec.
217#[cfg(feature = "reqwest-client")]
218pub trait ReqwestClientAuth<A> {
219    /// Borrow the auth-state struct holding configured credentials.
220    fn auth_state(&self) -> &A;
221}
222
223#[cfg(feature = "reqwest-client")]
224pub use percent_encoding;
225#[cfg(feature = "reqwest-client")]
226pub use reqwest;
227
228pub use base64;
229pub use chrono;
230pub use uuid;