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;