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}