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