Skip to main content

openapi_trait_client/
lib.rs

1//! Client backend proc-macro for `openapi-trait`.
2//!
3//! This crate is not intended for direct use. Use the
4//! [`openapi-trait`](https://docs.rs/openapi-trait) crate instead, which
5//! re-exports the [`openapi_trait`] attribute macro from here as
6//! `openapi_trait::client`.
7
8/// Code generation helpers for the attribute macro.
9mod codegen;
10/// Derive expansion helpers for `ReqwestClient`.
11mod reqwest_derive;
12
13use proc_macro::TokenStream;
14use proc_macro2::Span;
15use quote::quote;
16use syn::{parse_macro_input, DeriveInput, ItemMod, LitStr};
17
18/// Generates transport-agnostic Rust client traits from an `OpenAPI` specification file.
19///
20/// Apply this attribute to a `mod` block. The macro reads the `OpenAPI`
21/// document at the given path (resolved relative to `CARGO_MANIFEST_DIR`) at
22/// compile time and replaces the module's contents with:
23///
24/// - Schema structs derived from `components/schemas`
25/// - A `{OperationId}Request` struct per operation (bundles path, query,
26///   header params and the request body)
27/// - Per-operation `{OperationId}Response` enums whose variants map to HTTP
28///   status codes
29/// - A `{ModName}Client` trait with one method per operation (keyed by
30///   `operationId`)
31/// - When the `reqwest-client` cargo feature is enabled through
32///   `openapi-trait`, a blanket reqwest-backed implementation for any user
33///   type deriving [`ReqwestClient`]
34///
35/// The generated trait name is derived from the annotated module name, so
36/// `mod petstore {}` produces `petstore::PetstoreClient`.
37///
38/// # Debugging
39///
40/// Set the `OPENAPI_TRAIT_DEBUG` environment variable to dump a prettyprinted
41/// copy of the code this macro generates (one level deep, without recursively
42/// expanding nested derives). Use `1`/`true` to write to a default directory
43/// (`$OUT_DIR/openapi-trait-debug`, or the system temp dir), or set it to a
44/// directory path to choose the location. The resolved file path is printed to
45/// stderr during the build.
46#[proc_macro_attribute]
47pub fn openapi_trait(attr: TokenStream, item: TokenStream) -> TokenStream {
48    let path_lit = parse_macro_input!(attr as LitStr);
49    run_macro(&path_lit, item, cfg!(feature = "reqwest-client"))
50}
51
52/// Derive the reqwest client carrier trait for a user-owned struct.
53///
54/// By default, the derive expects named fields called `client` and `base_url`.
55/// You can override those conventions with field attributes:
56/// `#[openapi_trait(client)]` and `#[openapi_trait(base_url)]`.
57#[proc_macro_derive(ReqwestClient, attributes(openapi_trait))]
58pub fn derive_reqwest_client(item: TokenStream) -> TokenStream {
59    let input = parse_macro_input!(item as DeriveInput);
60
61    match reqwest_derive::expand_reqwest_client(input) {
62        Ok(tokens) => tokens.into(),
63        Err(error) => error.to_compile_error().into(),
64    }
65}
66
67/// Expand `#[openapi_trait("...")]` into a generated client module.
68fn run_macro(path_lit: &LitStr, item: TokenStream, include_reqwest: bool) -> TokenStream {
69    let module = parse_macro_input!(item as ItemMod);
70    let mod_ident = &module.ident;
71    let mod_vis = &module.vis;
72
73    let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") else {
74        return syn::Error::new(
75            Span::call_site(),
76            "CARGO_MANIFEST_DIR is not set; cannot resolve spec path",
77        )
78        .to_compile_error()
79        .into();
80    };
81
82    let spec_path = std::path::PathBuf::from(&manifest_dir).join(path_lit.value());
83    let spec_path_str = spec_path.to_string_lossy().into_owned();
84
85    let content = match std::fs::read_to_string(&spec_path) {
86        Ok(value) => value,
87        Err(error) => {
88            let msg = format!("cannot read OpenAPI spec `{spec_path_str}`: {error}");
89            return syn::Error::new(path_lit.span(), msg)
90                .to_compile_error()
91                .into();
92        }
93    };
94
95    let openapi: openapiv3::OpenAPI = match serde_yaml::from_str(&content) {
96        Ok(value) => value,
97        Err(error) => {
98            let msg = format!("cannot parse OpenAPI spec `{spec_path_str}`: {error}");
99            return syn::Error::new(path_lit.span(), msg)
100                .to_compile_error()
101                .into();
102        }
103    };
104
105    let body = codegen::generate_client(mod_ident, &openapi, include_reqwest);
106
107    let expanded = quote! {
108        const _: &str = ::core::include_str!(#spec_path_str);
109
110        #[allow(
111            missing_docs,
112            missing_debug_implementations,
113            dead_code,
114            unused_imports,
115            clippy::all,
116            clippy::nursery,
117            clippy::pedantic,
118        )]
119        #mod_vis mod #mod_ident {
120            #body
121        }
122    };
123
124    openapi_trait_shared::debug::write_debug_output(mod_ident, &expanded);
125
126    expanded.into()
127}