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 package;
12mod reqwest;
13mod servers;
14mod union;
15pub(crate) mod usage;
16
17use std::collections::HashMap;
18
19use proc_macro2::TokenStream;
20use quote::quote;
21
22use crate::emit::models::ModelDerives;
23pub use crate::emit::package::emit_package;
24use crate::error::Error;
25use crate::error::Result;
26use crate::ir::Module;
27use crate::ir::Multipart;
28use crate::ir::NegotiatedBody;
29use crate::ir::RustType;
30use crate::ir::ServerUrls;
31use crate::ir::Service;
32use crate::naming::Case;
33use crate::naming::to_ident;
34
35/// Emits a server interface for a lowered [`Service`] as top-level token items.
36///
37/// One implementation per target framework. `AxumServer` is the only one today.
38pub trait ServerEmitter {
39    /// Emit the server-interface items (trait, response enums, router, handlers).
40    fn emit(&self, service: &Service) -> Result<Vec<TokenStream>>;
41}
42
43/// Emits a client for a lowered [`Service`] as top-level token items.
44///
45/// One implementation per target HTTP library. `ReqwestClient` is the only one
46/// today.
47pub trait ClientEmitter {
48    /// Emit the client items (error type, `Client` struct, per-operation methods,
49    /// and the response types they return).
50    fn emit(&self, service: &Service) -> Result<Vec<TokenStream>>;
51}
52
53/// The marker literal, so [`GENERATED_MARKER`] and [`HEADER`] cannot drift
54/// apart. A `const` cannot be concatenated into another, and `concat!` takes
55/// literals only, so the one spelling has to live in a macro.
56macro_rules! generated_marker {
57    () => {
58        "// Code generated by oapi-codegen-rust. DO NOT EDIT."
59    };
60}
61
62/// The first line of every generated file: the marker that says the generator
63/// owns it.
64///
65/// A package writes several files into a directory it manages, and reconciling
66/// that directory means deleting the files a previous run left behind. This
67/// marker is what distinguishes them from a file somebody wrote by hand, which
68/// the generator refuses to touch.
69pub const GENERATED_MARKER: &str = generated_marker!();
70
71/// Header prepended to every generated file: the do-not-edit marker, then a
72/// blanket clippy allowance.
73///
74/// Clippy is the compiler, so a lint level belongs to the module tree and not to a
75/// path. There is no exclude key and no blanket switch: `#![allow(clippy)]` is an
76/// unknown lint and `#![allow(clippy::*)]` does not parse. Both measured. The four
77/// groups below hold every clippy lint, including any a later release adds, so this
78/// never tracks a lint list.
79///
80/// `dead_code` is a rustc lint, so the groups miss it. A generated file offers every
81/// type the specification declares, and a consumer uses the ones it needs.
82///
83/// `unused_imports` is a rustc lint too, and the split output needs it for the same
84/// reason. The root file re-exports every module so each name stays where it was, and
85/// a module imports every name its file could refer to. Neither depends on the
86/// consumer reaching for all of them.
87///
88/// An inner attribute needs the file to be a module. `#[path = "..."] mod x;` reads
89/// a generated file, and `include!` cannot, because rustc rejects an inner attribute
90/// in a paste.
91pub const HEADER: &str = concat!(
92    generated_marker!(),
93    "
94#![allow(
95    dead_code,
96    unused_imports,
97    clippy::all,
98    clippy::pedantic,
99    clippy::nursery,
100    clippy::restriction,
101    reason = \"generated code, not first-party source\"
102)]
103
104"
105);
106
107/// Render a module of IR items into formatted Rust source.
108///
109/// Each top-level item is parsed and pretty-printed on its own so that a blank
110/// line separates adjacent items — prettyplease otherwise emits them with no
111/// separation, which is hard to read when many `pub` items follow each other.
112pub fn emit_module(module: &Module, server_urls: Option<&ServerUrls>) -> Result<String> {
113    // No service, so no direction to narrow the serde traits by, and every model
114    // keeps both. The foreign narrowing does apply: a trait a foreign type lacks
115    // is unsatisfiable whichever direction the data flows.
116    let mut items = module_items(module, &usage::models_only_derives(module), false)?;
117    items.extend(server_url_items(server_urls)?);
118    return render(&items);
119}
120
121/// Which generator interfaces to emit alongside the shared per-operation types.
122///
123/// At least one field is set whenever [`emit_flat`] is called. The default, with
124/// no field set, is models-only generation.
125#[derive(Debug, Clone, Copy, Default)]
126pub struct Targets {
127    /// Emit the axum server interface.
128    pub server: bool,
129    /// Emit the blocking `reqwest` client.
130    pub client: bool,
131}
132
133/// A Rust prelude type that the emitted file names without a path, and what
134/// needs it.
135#[derive(Debug, Clone, Copy)]
136pub struct PreludeTypeName {
137    /// The prelude identifier, for example `Option`.
138    pub name: &'static str,
139    /// What generated code can name it for, for example `every optional field`,
140    /// used in the shadowing error.
141    pub used_for: &'static str,
142}
143
144/// The prelude types the requested `targets` name without a path.
145///
146/// A generated type of one of these names does not duplicate any item, so no
147/// collision check sees it. It shadows the prelude inside the file, and every
148/// use of the shadowed type stops compiling, so
149/// [`crate::lower::check_prelude_shadowing`] rejects it up front.
150///
151/// `Ok`, `Err`, `Some`, and `None` are absent, and belong in no list, but the
152/// reason is narrow. Those name values, and a *braced* `struct`, an `enum`, and
153/// an alias each take a type name only. A tuple or unit `struct` would take the
154/// value name too, and a model named `Ok` would then hide the prelude variant.
155/// The emitter writes `pub struct #name {..}` at every site, and
156/// `every_generated_struct_is_braced` holds it there. Fixture
157/// `combined_prelude_value_names` compiles the adversarial case: an operation
158/// references each of the four names, so none is pruned, and a server and a
159/// client then write `Ok(..)`, `Err(..)`, `Some(..)`, and `None` without a path
160/// beside models of those names.
161pub fn prelude_type_names(targets: Targets) -> Vec<PreludeTypeName> {
162    // Models carry the first four whichever target asks for them.
163    let mut names = vec![
164        PreludeTypeName {
165            name: "Option",
166            used_for: "every optional field",
167        },
168        PreludeTypeName {
169            name: "String",
170            used_for: "every string field",
171        },
172        PreludeTypeName {
173            name: "Vec",
174            used_for: "every array field",
175        },
176        PreludeTypeName {
177            name: "Box",
178            used_for: "the indirection a recursive schema takes",
179        },
180    ];
181    if targets.server || targets.client {
182        names.push(PreludeTypeName {
183            name: "Result",
184            used_for: "every generated method signature",
185        });
186    }
187    return names;
188}
189
190/// A fixed type name the generator emits at the crate root for a given target,
191/// which a component-schema model must not collide with.
192#[derive(Debug, Clone, Copy)]
193pub struct ReservedTypeName {
194    /// The reserved Rust identifier (for example `Api`).
195    pub name: &'static str,
196    /// Human-readable description of what emits it (for example `server interface
197    /// trait`), used in the collision error.
198    pub description: &'static str,
199}
200
201/// The crate-root type names the requested `targets` emit. A component schema
202/// whose generated name matches one of these will produce a duplicate item, so
203/// [`crate::lower::check_type_name_collisions`] rejects it up front.
204pub fn reserved_type_names(targets: Targets) -> Vec<ReservedTypeName> {
205    let mut names = vec![ReservedTypeName {
206        name: "Nullable",
207        description: "nullable JSON value",
208    }];
209    if targets.server {
210        names.push(ReservedTypeName {
211            name: axum::API_TRAIT_NAME,
212            description: "server interface trait",
213        });
214    }
215    if targets.client {
216        names.push(ReservedTypeName {
217            name: reqwest::CLIENT_STRUCT_NAME,
218            description: "client struct",
219        });
220        names.push(ReservedTypeName {
221            name: reqwest::CLIENT_ERROR_NAME,
222            description: "client error enum",
223        });
224    }
225    return names;
226}
227
228/// Render a module as a flat file: the shared component models and per-operation
229/// types at the crate root, followed by the requested generator interfaces.
230///
231/// The query/header/cookie inputs, request and response bodies, and response
232/// enum an operation contributes are the same types whichever generator uses
233/// them, so `operation::emit_operation_types` emits them once. The axum server
234/// then adds its extractor and `IntoResponse` impls, and the `reqwest` client
235/// its request-building methods, both naming those root types directly. Server
236/// and client can therefore share a single file without a name clash.
237pub fn emit_flat(
238    module: &Module,
239    service: &Service,
240    server_urls: Option<&ServerUrls>,
241    targets: Targets,
242) -> Result<String> {
243    let derives = usage::model_derives(module, service, targets);
244    let foreign = usage::foreign_resolver(module);
245    let mut items = module_items(module, &derives, uses_nullable(module, Some(service)))?;
246    items.extend(server_url_items(server_urls)?);
247    for operation in &service.operations {
248        items.extend(operation::emit_operation_types(operation, targets, &foreign)?);
249    }
250    if targets.server {
251        items.extend(axum::AxumServer.emit(service)?);
252    }
253    if targets.client {
254        items.extend(reqwest::ReqwestClient.emit(service)?);
255    }
256    return render(&items);
257}
258
259/// Emit the server-URL items, or nothing when the feature is disabled or the
260/// spec declares no servers.
261fn server_url_items(server_urls: Option<&ServerUrls>) -> Result<Vec<TokenStream>> {
262    return match server_urls {
263        Some(server_urls) => servers::emit_server_urls(server_urls),
264        None => Ok(Vec::new()),
265    };
266}
267
268/// Lower every IR item in a module into its token stream, applying each item's
269/// derive set from `derives` (defaulting to every trait when absent).
270fn module_items(
271    module: &Module,
272    derives: &HashMap<String, ModelDerives>,
273    service_nullable: bool,
274) -> Result<Vec<TokenStream>> {
275    let mut items = Vec::with_capacity(module.items.len());
276    if service_nullable || module.items.iter().any(item_uses_nullable) {
277        items.push(quote! {
278        /// A present JSON value, including explicit null.
279        #[derive(Debug, Clone, Default, PartialEq, Eq, Hash, serde::Serialize)]
280        #[serde(untagged)]
281        pub enum Nullable<T> {
282            /// Explicit JSON null.
283            #[default]
284            Null,
285            /// A non-null value.
286            Value(T),
287        }
288
289        impl<T> Nullable<T> {
290            /// Borrow the non-null value.
291            pub fn as_ref(&self) -> Option<&T> {
292                return match self {
293                    Self::Null => None,
294                    Self::Value(value) => Some(value),
295                };
296            }
297        }
298
299        impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Nullable<T> {
300            fn deserialize<D>(deserializer: D) -> ::core::result::Result<Self, D::Error>
301            where
302                D: serde::Deserializer<'de>,
303            {
304                struct NullableVisitor<T>(::core::marker::PhantomData<T>);
305
306                impl<'de, T: serde::Deserialize<'de>> serde::de::Visitor<'de> for NullableVisitor<T> {
307                    type Value = Nullable<T>;
308
309                    fn expecting(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
310                        return formatter.write_str("a present value or null");
311                    }
312
313                    fn visit_newtype_struct<D>(self, deserializer: D) -> ::core::result::Result<Self::Value, D::Error>
314                    where
315                        D: serde::Deserializer<'de>,
316                    {
317                        return <Option<T> as serde::Deserialize>::deserialize(deserializer).map(|value| {
318                            return match value {
319                                Some(value) => Nullable::Value(value),
320                                None => Nullable::Null,
321                            };
322                        });
323                    }
324
325                    fn visit_map<M>(self, map: M) -> ::core::result::Result<Self::Value, M::Error>
326                    where
327                        M: serde::de::MapAccess<'de>,
328                    {
329                        return T::deserialize(serde::de::value::MapAccessDeserializer::new(map))
330                            .map(Nullable::Value);
331                    }
332                }
333
334                // The newtype boundary rejects absent fields before reading an Option.
335                return deserializer.deserialize_newtype_struct(
336                    "Nullable",
337                    NullableVisitor(::core::marker::PhantomData),
338                );
339            }
340        }
341        });
342    }
343    for item in &module.items {
344        let set = derives.get(item.name()).copied().unwrap_or_else(ModelDerives::both);
345        items.push(models::emit_item(item, set)?);
346    }
347    return Ok(items);
348}
349
350pub(crate) fn uses_nullable(module: &Module, service: Option<&Service>) -> bool {
351    use crate::ir::RequestPayload;
352    use crate::ir::ResponseBody;
353
354    return module.items.iter().any(item_uses_nullable)
355        || service.is_some_and(|service| {
356            return service.operations.iter().any(|operation| {
357                let request = match &operation.request {
358                    Some(RequestPayload::Single(body)) => type_uses_nullable(&body.ty),
359                    Some(RequestPayload::Negotiated(body)) => body
360                        .variants
361                        .iter()
362                        .any(|variant| return type_uses_nullable(&variant.body.ty)),
363                    _ => false,
364                };
365                return request
366                    || operation.responses.iter().any(|response| {
367                        return match &response.body {
368                            Some(ResponseBody::Single(body)) => type_uses_nullable(&body.ty),
369                            Some(ResponseBody::Negotiated(body)) => body
370                                .variants
371                                .iter()
372                                .any(|variant| return type_uses_nullable(&variant.body.ty)),
373                            None => false,
374                        };
375                    });
376            });
377        });
378}
379
380fn item_uses_nullable(item: &crate::ir::Item) -> bool {
381    return match item {
382        crate::ir::Item::Struct(value) => {
383            value.fields.iter().any(|field| return type_uses_nullable(&field.ty))
384                || value.additional_properties.as_ref().is_some_and(type_uses_nullable)
385        }
386        crate::ir::Item::Alias(value) => type_uses_nullable(&value.ty),
387        crate::ir::Item::Enum(value) => match &value.kind {
388            crate::ir::EnumKind::Union(variants) => {
389                variants.iter().any(|variant| return type_uses_nullable(&variant.ty))
390            }
391            _ => false,
392        },
393    };
394}
395
396fn type_uses_nullable(ty: &RustType) -> bool {
397    return match ty {
398        RustType::Nullable(_) => true,
399        RustType::Vec(inner) | RustType::Map(inner) | RustType::Option(inner) | RustType::Boxed(inner) => {
400            type_uses_nullable(inner)
401        }
402        _ => false,
403    };
404}
405
406/// Pretty-print a sequence of top-level items, one blank line apart, prefixed
407/// with the generated-file header.
408fn render(items: &[TokenStream]) -> Result<String> {
409    let mut out = String::from(HEADER);
410    out.push_str(&render_body(items)?);
411    return Ok(out);
412}
413
414/// Pretty-print a sequence of items, one blank line apart, without the header.
415///
416/// Each item is parsed and pretty-printed on its own so that a blank line
417/// separates adjacent items — prettyplease otherwise emits them with no
418/// separation, which is hard to read when many `pub` items follow each other.
419fn render_body(items: &[TokenStream]) -> Result<String> {
420    let mut out = String::new();
421    for (index, tokens) in items.iter().enumerate() {
422        let file = syn::parse2::<syn::File>(tokens.clone()).map_err(|source| {
423            return Error::InvalidGeneratedCode { source };
424        })?;
425        if index > 0 {
426            out.push('\n');
427        }
428        out.push_str(&prettyplease::unparse(&file));
429    }
430    return Ok(out);
431}
432
433/// Render a doc attribute, or nothing when there is no documentation.
434///
435/// This splits the text on its line breaks, so a multi-line `description` prints
436/// as a run of `///` lines and not one `/** */` block.
437pub(crate) fn doc_attr(doc: &Option<String>) -> TokenStream {
438    let tokens = match doc {
439        Some(text) => doc_lines(std::slice::from_ref(text)),
440        None => quote! {},
441    };
442    return tokens;
443}
444
445/// Render one doc attribute per line, which rustdoc reads as one comment.
446///
447/// A blank line stays blank, so a caller can separate paragraphs with one. An
448/// entry that already holds line breaks, such as a multi-line `description` from
449/// the document, is split on them: a `#[doc]` carrying a `\n` prints as a
450/// `/** */` block, which would sit unevenly among its `///` siblings.
451pub(crate) fn doc_lines(lines: &[String]) -> TokenStream {
452    let attrs = lines.iter().flat_map(|entry| return entry.split('\n')).map(|line| {
453        // Leading space matches the `/// text` desugaring rustfmt produces. A
454        // blank line takes none, so no trailing space reaches the output.
455        let trimmed = line.trim_end();
456        let spaced = if trimmed.is_empty() {
457            String::new()
458        } else {
459            format!(" {trimmed}")
460        };
461        return quote! { #[doc = #spaced] };
462    });
463    return quote! { #(#attrs)* };
464}
465
466/// Render a Rust type expression.
467pub(crate) fn emit_type(ty: &RustType) -> Result<TokenStream> {
468    let tokens = match ty {
469        RustType::Bool => quote! { bool },
470        RustType::I32 => quote! { i32 },
471        RustType::I64 => quote! { i64 },
472        RustType::U32 => quote! { u32 },
473        RustType::U64 => quote! { u64 },
474        RustType::F64 => quote! { f64 },
475        RustType::String => quote! { String },
476        RustType::Value => quote! { serde_json::Value },
477        RustType::Date => quote! { chrono::NaiveDate },
478        RustType::DateTime => quote! { chrono::DateTime<chrono::Utc> },
479        RustType::Uuid => quote! { uuid::Uuid },
480        RustType::Bytes => quote! { Vec<u8> },
481        RustType::Vec(inner) => {
482            let inner = emit_type(inner)?;
483            quote! { Vec<#inner> }
484        }
485        RustType::Map(inner) => {
486            let inner = emit_type(inner)?;
487            quote! { std::collections::HashMap<String, #inner> }
488        }
489        RustType::Option(inner) => {
490            let inner = emit_type(inner)?;
491            quote! { Option<#inner> }
492        }
493        RustType::Nullable(inner) => {
494            let inner = emit_type(inner)?;
495            quote! { Nullable<#inner> }
496        }
497        RustType::Boxed(inner) => {
498            let inner = emit_type(inner)?;
499            quote! { Box<#inner> }
500        }
501        RustType::Named(name) => {
502            let ident = to_ident(name, Case::Pascal).to_token();
503            quote! { #ident }
504        }
505        RustType::External { module, name } => {
506            let path: syn::Path = syn::parse_str(module).map_err(|err| {
507                return Error::UnsupportedSchema {
508                    path: "import-mapping".to_owned(),
509                    reason: format!("module path `{module}` is not a valid Rust path expression: {err}"),
510                };
511            })?;
512            let ident = to_ident(name, Case::Pascal).to_token();
513            quote! { #path::#ident }
514        }
515        RustType::Verbatim { text, .. } => {
516            let parsed: TokenStream = text.parse().map_err(|err: proc_macro2::LexError| {
517                return Error::UnsupportedSchema {
518                    path: "x-rust-type".to_owned(),
519                    reason: format!("value `{text}` is not a valid Rust type expression: {err}"),
520                };
521            })?;
522            parsed
523        }
524    };
525    return Ok(tokens);
526}
527
528/// Emit the plain per-operation multipart struct (`<Op>Multipart`) shared by the
529/// server extractor and the client request builder: one public field per part,
530/// wrapped in `Option` when the part is optional. The server augments this with a
531/// `FromRequest` impl. The client reads the fields to build a `reqwest` form.
532pub(crate) fn emit_multipart_struct(multipart: &Multipart, foreign: &usage::ForeignResolver) -> Result<TokenStream> {
533    let name = multipart.name.to_token();
534    let field_types = multipart.fields.iter().map(|field| return &field.ty);
535    let derive_attr = models::plain_derive_attr(models::DEBUG_AND_CLONE, foreign.of_types(field_types));
536    let mut field_defs = Vec::with_capacity(multipart.fields.len());
537    for field in &multipart.fields {
538        let ident = field.rust_name.to_token();
539        let ty = emit_type(&field.ty)?;
540        let field_ty = if field.optional {
541            quote! { Option<#ty> }
542        } else {
543            quote! { #ty }
544        };
545        field_defs.push(quote! { pub #ident: #field_ty, });
546    }
547    return Ok(quote! {
548        #derive_attr
549        pub struct #name {
550            #(#field_defs)*
551        }
552    });
553}
554
555/// Emit the plain enum backing a negotiated (multi-content-type) body: one
556/// variant per content representation, carrying that representation's decoded
557/// type. Shared by the server (which augments the request enum with a
558/// `FromRequest` impl and renders the response enum with `IntoResponse`) and the
559/// client (which matches on it to build a request or decode a response).
560pub(crate) fn emit_negotiated_body_enum(
561    body: &NegotiatedBody,
562    foreign: &usage::ForeignResolver,
563) -> Result<TokenStream> {
564    let name = body.name.to_token();
565    let variant_types = body.variants.iter().map(|variant| return &variant.body.ty);
566    let derive_attr = models::plain_derive_attr(models::DEBUG_CLONE_AND_EQ, foreign.of_types(variant_types));
567    let mut variants = Vec::with_capacity(body.variants.len());
568    for variant in &body.variants {
569        let ident = variant.variant.to_token();
570        let ty = emit_type(&variant.body.ty)?;
571        variants.push(quote! { #ident(#ty) });
572    }
573    return Ok(quote! {
574        #derive_attr
575        pub enum #name {
576            #(#variants),*
577        }
578    });
579}