Skip to main content

oapi_codegen/emit/
mod.rs

1//! Emitting the [`crate::ir`] as formatted Rust source.
2//!
3//! Items are built as a [`proc_macro2::TokenStream`] with `quote!`, parsed into
4//! a [`syn::File`] (which guarantees the output is syntactically valid Rust),
5//! and pretty-printed with `prettyplease`.
6
7mod axum;
8mod constraints;
9mod models;
10mod operation;
11mod reqwest;
12mod servers;
13mod usage;
14
15use std::collections::HashMap;
16
17use proc_macro2::TokenStream;
18use quote::quote;
19
20use crate::emit::models::ModelDerives;
21use crate::error::Error;
22use crate::error::Result;
23use crate::ir::Module;
24use crate::ir::Multipart;
25use crate::ir::NegotiatedBody;
26use crate::ir::RustType;
27use crate::ir::ServerUrls;
28use crate::ir::Service;
29use crate::naming::Case;
30use crate::naming::to_ident;
31
32/// Emits a server interface for a lowered [`Service`] as top-level token items.
33///
34/// One implementation per target framework. `AxumServer` is the only one today.
35pub trait ServerEmitter {
36    /// Emit the server-interface items (trait, response enums, router, handlers).
37    fn emit(&self, service: &Service) -> Result<Vec<TokenStream>>;
38}
39
40/// Emits a client for a lowered [`Service`] as top-level token items.
41///
42/// One implementation per target HTTP library. `ReqwestClient` is the only one
43/// today.
44pub trait ClientEmitter {
45    /// Emit the client items (error type, `Client` struct, per-operation methods,
46    /// and the response types they return).
47    fn emit(&self, service: &Service) -> Result<Vec<TokenStream>>;
48}
49
50/// Header prepended to every generated file: the do-not-edit marker, then a
51/// blanket clippy allowance.
52///
53/// Clippy is the compiler, so a lint level belongs to the module tree and not to a
54/// path. There is no exclude key and no blanket switch: `#![allow(clippy)]` is an
55/// unknown lint and `#![allow(clippy::*)]` does not parse. Both measured. The four
56/// groups below hold every clippy lint, including any a later release adds, so this
57/// never tracks a lint list.
58///
59/// `dead_code` is a rustc lint, so the groups miss it. A generated file offers every
60/// type the specification declares, and a consumer uses the ones it needs.
61///
62/// An inner attribute needs the file to be a module. `#[path = "..."] mod x;` reads
63/// a generated file, and `include!` cannot, because rustc rejects an inner attribute
64/// in a paste.
65pub const HEADER: &str = "// Code generated by oapi-codegen-rust. DO NOT EDIT.
66#![allow(
67    dead_code,
68    clippy::all,
69    clippy::pedantic,
70    clippy::nursery,
71    clippy::restriction,
72    reason = \"generated code, not first-party source\"
73)]
74
75";
76
77/// Render a module of IR items into formatted Rust source.
78///
79/// Each top-level item is parsed and pretty-printed on its own so that a blank
80/// line separates adjacent items — prettyplease otherwise emits them with no
81/// separation, which is hard to read when many `pub` items follow each other.
82pub fn emit_module(module: &Module, server_urls: Option<&ServerUrls>) -> Result<String> {
83    // No service, so no direction to narrow the serde traits by, and every model
84    // keeps both. The foreign narrowing does apply: a trait a foreign type lacks
85    // is unsatisfiable whichever direction the data flows.
86    let mut items = module_items(module, &usage::models_only_derives(module))?;
87    items.extend(server_url_items(server_urls)?);
88    return render(&items);
89}
90
91/// Which generator interfaces to emit alongside the shared per-operation types.
92///
93/// At least one field is set whenever [`emit_flat`] is called. The default, with
94/// no field set, is models-only generation.
95#[derive(Debug, Clone, Copy, Default)]
96pub struct Targets {
97    /// Emit the axum server interface.
98    pub server: bool,
99    /// Emit the blocking `reqwest` client.
100    pub client: bool,
101}
102
103/// A Rust prelude type that the emitted file names without a path, and what
104/// needs it.
105#[derive(Debug, Clone, Copy)]
106pub struct PreludeTypeName {
107    /// The prelude identifier, for example `Option`.
108    pub name: &'static str,
109    /// What generated code can name it for, for example `every optional field`,
110    /// used in the shadowing error.
111    pub used_for: &'static str,
112}
113
114/// The prelude types the requested `targets` name without a path.
115///
116/// A generated type of one of these names does not duplicate any item, so no
117/// collision check sees it. It shadows the prelude inside the file, and every
118/// use of the shadowed type stops compiling, so
119/// [`crate::lower::check_prelude_shadowing`] rejects it up front.
120///
121/// `Ok`, `Err`, `Some`, and `None` are absent, and belong in no list, but the
122/// reason is narrow. Those name values, and a *braced* `struct`, an `enum`, and
123/// an alias each take a type name only. A tuple or unit `struct` would take the
124/// value name too, and a model named `Ok` would then hide the prelude variant.
125/// The emitter writes `pub struct #name {..}` at every site, and
126/// `every_generated_struct_is_braced` holds it there. Fixture
127/// `combined_prelude_value_names` compiles the adversarial case: an operation
128/// references each of the four names, so none is pruned, and a server and a
129/// client then write `Ok(..)`, `Err(..)`, `Some(..)`, and `None` without a path
130/// beside models of those names.
131pub fn prelude_type_names(targets: Targets) -> Vec<PreludeTypeName> {
132    // Models carry the first four whichever target asks for them.
133    let mut names = vec![
134        PreludeTypeName {
135            name: "Option",
136            used_for: "every optional field",
137        },
138        PreludeTypeName {
139            name: "String",
140            used_for: "every string field",
141        },
142        PreludeTypeName {
143            name: "Vec",
144            used_for: "every array field",
145        },
146        PreludeTypeName {
147            name: "Box",
148            used_for: "the indirection a recursive schema takes",
149        },
150    ];
151    if targets.server || targets.client {
152        names.push(PreludeTypeName {
153            name: "Result",
154            used_for: "every generated method signature",
155        });
156    }
157    return names;
158}
159
160/// A fixed type name the generator emits at the crate root for a given target,
161/// which a component-schema model must not collide with.
162#[derive(Debug, Clone, Copy)]
163pub struct ReservedTypeName {
164    /// The reserved Rust identifier (for example `Api`).
165    pub name: &'static str,
166    /// Human-readable description of what emits it (for example `server interface
167    /// trait`), used in the collision error.
168    pub description: &'static str,
169}
170
171/// The crate-root type names the requested `targets` emit. A component schema
172/// whose generated name matches one of these will produce a duplicate item, so
173/// [`crate::lower::check_type_name_collisions`] rejects it up front.
174pub fn reserved_type_names(targets: Targets) -> Vec<ReservedTypeName> {
175    let mut names = Vec::new();
176    if targets.server {
177        names.push(ReservedTypeName {
178            name: axum::API_TRAIT_NAME,
179            description: "server interface trait",
180        });
181    }
182    if targets.client {
183        names.push(ReservedTypeName {
184            name: reqwest::CLIENT_STRUCT_NAME,
185            description: "client struct",
186        });
187        names.push(ReservedTypeName {
188            name: reqwest::CLIENT_ERROR_NAME,
189            description: "client error enum",
190        });
191    }
192    return names;
193}
194
195/// Render a module as a flat file: the shared component models and per-operation
196/// types at the crate root, followed by the requested generator interfaces.
197///
198/// The query/header/cookie inputs, request and response bodies, and response
199/// enum an operation contributes are the same types whichever generator uses
200/// them, so `operation::emit_operation_types` emits them once. The axum server
201/// then adds its extractor and `IntoResponse` impls, and the `reqwest` client
202/// its request-building methods, both naming those root types directly. Server
203/// and client can therefore share a single file without a name clash.
204pub fn emit_flat(
205    module: &Module,
206    service: &Service,
207    server_urls: Option<&ServerUrls>,
208    targets: Targets,
209) -> Result<String> {
210    let derives = usage::model_derives(module, service, targets);
211    let foreign = usage::foreign_resolver(module);
212    let mut items = module_items(module, &derives)?;
213    items.extend(server_url_items(server_urls)?);
214    for operation in &service.operations {
215        items.extend(operation::emit_operation_types(operation, targets, &foreign)?);
216    }
217    if targets.server {
218        items.extend(axum::AxumServer.emit(service)?);
219    }
220    if targets.client {
221        items.extend(reqwest::ReqwestClient.emit(service)?);
222    }
223    return render(&items);
224}
225
226/// Emit the server-URL items, or nothing when the feature is disabled or the
227/// spec declares no servers.
228fn server_url_items(server_urls: Option<&ServerUrls>) -> Result<Vec<TokenStream>> {
229    return match server_urls {
230        Some(server_urls) => servers::emit_server_urls(server_urls),
231        None => Ok(Vec::new()),
232    };
233}
234
235/// Lower every IR item in a module into its token stream, applying each item's
236/// derive set from `derives` (defaulting to every trait when absent).
237fn module_items(module: &Module, derives: &HashMap<String, ModelDerives>) -> Result<Vec<TokenStream>> {
238    let mut items = Vec::with_capacity(module.items.len());
239    for item in &module.items {
240        let set = derives.get(item.name()).copied().unwrap_or_else(ModelDerives::both);
241        items.push(models::emit_item(item, set)?);
242    }
243    return Ok(items);
244}
245
246/// Pretty-print a sequence of top-level items, one blank line apart, prefixed
247/// with the generated-file header.
248fn render(items: &[TokenStream]) -> Result<String> {
249    let mut out = String::from(HEADER);
250    out.push_str(&render_body(items)?);
251    return Ok(out);
252}
253
254/// Pretty-print a sequence of items, one blank line apart, without the header.
255///
256/// Each item is parsed and pretty-printed on its own so that a blank line
257/// separates adjacent items — prettyplease otherwise emits them with no
258/// separation, which is hard to read when many `pub` items follow each other.
259fn render_body(items: &[TokenStream]) -> Result<String> {
260    let mut out = String::new();
261    for (index, tokens) in items.iter().enumerate() {
262        let file = syn::parse2::<syn::File>(tokens.clone()).map_err(|source| {
263            return Error::InvalidGeneratedCode { source };
264        })?;
265        if index > 0 {
266            out.push('\n');
267        }
268        out.push_str(&prettyplease::unparse(&file));
269    }
270    return Ok(out);
271}
272
273/// Render a doc attribute, or nothing when there is no documentation.
274pub(crate) fn doc_attr(doc: &Option<String>) -> TokenStream {
275    let tokens = match doc {
276        Some(text) => {
277            // Leading space matches the `/// text` desugaring rustfmt produces.
278            let spaced = format!(" {text}");
279            quote! { #[doc = #spaced] }
280        }
281        None => quote! {},
282    };
283    return tokens;
284}
285
286/// Render one doc attribute per line, which rustdoc reads as one comment.
287///
288/// A blank line stays blank, so a caller can separate paragraphs with one. An
289/// entry that already holds line breaks, such as a multi-line `description` from
290/// the document, is split on them: a `#[doc]` carrying a `\n` prints as a
291/// `/** */` block, which would sit unevenly among its `///` siblings.
292pub(crate) fn doc_lines(lines: &[String]) -> TokenStream {
293    let attrs = lines.iter().flat_map(|entry| return entry.split('\n')).map(|line| {
294        // Leading space matches the `/// text` desugaring rustfmt produces. A
295        // blank line takes none, so no trailing space reaches the output.
296        let trimmed = line.trim_end();
297        let spaced = if trimmed.is_empty() {
298            String::new()
299        } else {
300            format!(" {trimmed}")
301        };
302        return quote! { #[doc = #spaced] };
303    });
304    return quote! { #(#attrs)* };
305}
306
307/// Render a Rust type expression.
308pub(crate) fn emit_type(ty: &RustType) -> Result<TokenStream> {
309    let tokens = match ty {
310        RustType::Bool => quote! { bool },
311        RustType::I32 => quote! { i32 },
312        RustType::I64 => quote! { i64 },
313        RustType::U32 => quote! { u32 },
314        RustType::U64 => quote! { u64 },
315        RustType::F64 => quote! { f64 },
316        RustType::String => quote! { String },
317        RustType::Value => quote! { serde_json::Value },
318        RustType::Date => quote! { chrono::NaiveDate },
319        RustType::DateTime => quote! { chrono::DateTime<chrono::Utc> },
320        RustType::Uuid => quote! { uuid::Uuid },
321        RustType::Bytes => quote! { Vec<u8> },
322        RustType::Vec(inner) => {
323            let inner = emit_type(inner)?;
324            quote! { Vec<#inner> }
325        }
326        RustType::Map(inner) => {
327            let inner = emit_type(inner)?;
328            quote! { std::collections::HashMap<String, #inner> }
329        }
330        RustType::Option(inner) => {
331            let inner = emit_type(inner)?;
332            quote! { Option<#inner> }
333        }
334        RustType::Boxed(inner) => {
335            let inner = emit_type(inner)?;
336            quote! { Box<#inner> }
337        }
338        RustType::Named(name) => {
339            let ident = to_ident(name, Case::Pascal).to_token();
340            quote! { #ident }
341        }
342        RustType::External { module, name } => {
343            let path: syn::Path = syn::parse_str(module).map_err(|err| {
344                return Error::UnsupportedSchema {
345                    path: "import-mapping".to_owned(),
346                    reason: format!("module path `{module}` is not a valid Rust path expression: {err}"),
347                };
348            })?;
349            let ident = to_ident(name, Case::Pascal).to_token();
350            quote! { #path::#ident }
351        }
352        RustType::Verbatim { text, .. } => {
353            let parsed: TokenStream = text.parse().map_err(|err: proc_macro2::LexError| {
354                return Error::UnsupportedSchema {
355                    path: "x-rust-type".to_owned(),
356                    reason: format!("value `{text}` is not a valid Rust type expression: {err}"),
357                };
358            })?;
359            parsed
360        }
361    };
362    return Ok(tokens);
363}
364
365/// Emit the plain per-operation multipart struct (`<Op>Multipart`) shared by the
366/// server extractor and the client request builder: one public field per part,
367/// wrapped in `Option` when the part is optional. The server augments this with a
368/// `FromRequest` impl. The client reads the fields to build a `reqwest` form.
369pub(crate) fn emit_multipart_struct(multipart: &Multipart, foreign: &usage::ForeignResolver) -> Result<TokenStream> {
370    let name = multipart.name.to_token();
371    let field_types = multipart.fields.iter().map(|field| return &field.ty);
372    let derive_attr = models::plain_derive_attr(models::DEBUG_AND_CLONE, foreign.of_types(field_types));
373    let mut field_defs = Vec::with_capacity(multipart.fields.len());
374    for field in &multipart.fields {
375        let ident = field.rust_name.to_token();
376        let ty = emit_type(&field.ty)?;
377        let field_ty = if field.optional {
378            quote! { Option<#ty> }
379        } else {
380            quote! { #ty }
381        };
382        field_defs.push(quote! { pub #ident: #field_ty, });
383    }
384    return Ok(quote! {
385        #derive_attr
386        pub struct #name {
387            #(#field_defs)*
388        }
389    });
390}
391
392/// Emit the plain enum backing a negotiated (multi-content-type) body: one
393/// variant per content representation, carrying that representation's decoded
394/// type. Shared by the server (which augments the request enum with a
395/// `FromRequest` impl and renders the response enum with `IntoResponse`) and the
396/// client (which matches on it to build a request or decode a response).
397pub(crate) fn emit_negotiated_body_enum(
398    body: &NegotiatedBody,
399    foreign: &usage::ForeignResolver,
400) -> Result<TokenStream> {
401    let name = body.name.to_token();
402    let variant_types = body.variants.iter().map(|variant| return &variant.body.ty);
403    let derive_attr = models::plain_derive_attr(models::DEBUG_CLONE_AND_EQ, foreign.of_types(variant_types));
404    let mut variants = Vec::with_capacity(body.variants.len());
405    for variant in &body.variants {
406        let ident = variant.variant.to_token();
407        let ty = emit_type(&variant.body.ty)?;
408        variants.push(quote! { #ident(#ty) });
409    }
410    return Ok(quote! {
411        #derive_attr
412        pub enum #name {
413            #(#variants),*
414        }
415    });
416}