oapi-codegen 1.0.1

Generate client and server boilerplate from OpenAPI 3 specifications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
//! Emitting the [`crate::ir`] as formatted Rust source.
//!
//! Items are built as a [`proc_macro2::TokenStream`] with `quote!`, parsed into
//! a [`syn::File`] (which guarantees the output is syntactically valid Rust),
//! and pretty-printed with `prettyplease`.

mod axum;
mod constraints;
mod models;
mod operation;
mod reqwest;
mod servers;
mod usage;

use std::collections::HashMap;

use proc_macro2::TokenStream;
use quote::quote;

use crate::emit::models::ModelDerives;
use crate::error::Error;
use crate::error::Result;
use crate::ir::Module;
use crate::ir::Multipart;
use crate::ir::NegotiatedBody;
use crate::ir::RustType;
use crate::ir::ServerUrls;
use crate::ir::Service;
use crate::naming::Case;
use crate::naming::to_ident;

/// Emits a server interface for a lowered [`Service`] as top-level token items.
///
/// One implementation per target framework. `AxumServer` is the only one today.
pub trait ServerEmitter {
    /// Emit the server-interface items (trait, response enums, router, handlers).
    fn emit(&self, service: &Service) -> Result<Vec<TokenStream>>;
}

/// Emits a client for a lowered [`Service`] as top-level token items.
///
/// One implementation per target HTTP library. `ReqwestClient` is the only one
/// today.
pub trait ClientEmitter {
    /// Emit the client items (error type, `Client` struct, per-operation methods,
    /// and the response types they return).
    fn emit(&self, service: &Service) -> Result<Vec<TokenStream>>;
}

/// Header prepended to every generated file: the do-not-edit marker, then a
/// blanket clippy allowance.
///
/// Clippy is the compiler, so a lint level belongs to the module tree and not to a
/// path. There is no exclude key and no blanket switch: `#![allow(clippy)]` is an
/// unknown lint and `#![allow(clippy::*)]` does not parse. Both measured. The four
/// groups below hold every clippy lint, including any a later release adds, so this
/// never tracks a lint list.
///
/// `dead_code` is a rustc lint, so the groups miss it. A generated file offers every
/// type the specification declares, and a consumer uses the ones it needs.
///
/// An inner attribute needs the file to be a module. `#[path = "..."] mod x;` reads
/// a generated file, and `include!` cannot, because rustc rejects an inner attribute
/// in a paste.
pub const HEADER: &str = "// Code generated by oapi-codegen-rust. DO NOT EDIT.
#![allow(
    dead_code,
    clippy::all,
    clippy::pedantic,
    clippy::nursery,
    clippy::restriction,
    reason = \"generated code, not first-party source\"
)]

";

/// Render a module of IR items into formatted Rust source.
///
/// Each top-level item is parsed and pretty-printed on its own so that a blank
/// line separates adjacent items — prettyplease otherwise emits them with no
/// separation, which is hard to read when many `pub` items follow each other.
pub fn emit_module(module: &Module, server_urls: Option<&ServerUrls>) -> Result<String> {
    // No service, so no direction to narrow the serde traits by, and every model
    // keeps both. The foreign narrowing does apply: a trait a foreign type lacks
    // is unsatisfiable whichever direction the data flows.
    let mut items = module_items(module, &usage::models_only_derives(module))?;
    items.extend(server_url_items(server_urls)?);
    return render(&items);
}

/// Which generator interfaces to emit alongside the shared per-operation types.
///
/// At least one field is set whenever [`emit_flat`] is called. The default, with
/// no field set, is models-only generation.
#[derive(Debug, Clone, Copy, Default)]
pub struct Targets {
    /// Emit the axum server interface.
    pub server: bool,
    /// Emit the blocking `reqwest` client.
    pub client: bool,
}

/// A Rust prelude type that the emitted file names without a path, and what
/// needs it.
#[derive(Debug, Clone, Copy)]
pub struct PreludeTypeName {
    /// The prelude identifier, for example `Option`.
    pub name: &'static str,
    /// What generated code can name it for, for example `every optional field`,
    /// used in the shadowing error.
    pub used_for: &'static str,
}

/// The prelude types the requested `targets` name without a path.
///
/// A generated type of one of these names does not duplicate any item, so no
/// collision check sees it. It shadows the prelude inside the file, and every
/// use of the shadowed type stops compiling, so
/// [`crate::lower::check_prelude_shadowing`] rejects it up front.
///
/// `Ok`, `Err`, `Some`, and `None` are absent, and belong in no list, but the
/// reason is narrow. Those name values, and a *braced* `struct`, an `enum`, and
/// an alias each take a type name only. A tuple or unit `struct` would take the
/// value name too, and a model named `Ok` would then hide the prelude variant.
/// The emitter writes `pub struct #name {..}` at every site, and
/// `every_generated_struct_is_braced` holds it there. Fixture
/// `combined_prelude_value_names` compiles the adversarial case: an operation
/// references each of the four names, so none is pruned, and a server and a
/// client then write `Ok(..)`, `Err(..)`, `Some(..)`, and `None` without a path
/// beside models of those names.
pub fn prelude_type_names(targets: Targets) -> Vec<PreludeTypeName> {
    // Models carry the first four whichever target asks for them.
    let mut names = vec![
        PreludeTypeName {
            name: "Option",
            used_for: "every optional field",
        },
        PreludeTypeName {
            name: "String",
            used_for: "every string field",
        },
        PreludeTypeName {
            name: "Vec",
            used_for: "every array field",
        },
        PreludeTypeName {
            name: "Box",
            used_for: "the indirection a recursive schema takes",
        },
    ];
    if targets.server || targets.client {
        names.push(PreludeTypeName {
            name: "Result",
            used_for: "every generated method signature",
        });
    }
    return names;
}

/// A fixed type name the generator emits at the crate root for a given target,
/// which a component-schema model must not collide with.
#[derive(Debug, Clone, Copy)]
pub struct ReservedTypeName {
    /// The reserved Rust identifier (for example `Api`).
    pub name: &'static str,
    /// Human-readable description of what emits it (for example `server interface
    /// trait`), used in the collision error.
    pub description: &'static str,
}

/// The crate-root type names the requested `targets` emit. A component schema
/// whose generated name matches one of these will produce a duplicate item, so
/// [`crate::lower::check_type_name_collisions`] rejects it up front.
pub fn reserved_type_names(targets: Targets) -> Vec<ReservedTypeName> {
    let mut names = Vec::new();
    if targets.server {
        names.push(ReservedTypeName {
            name: axum::API_TRAIT_NAME,
            description: "server interface trait",
        });
    }
    if targets.client {
        names.push(ReservedTypeName {
            name: reqwest::CLIENT_STRUCT_NAME,
            description: "client struct",
        });
        names.push(ReservedTypeName {
            name: reqwest::CLIENT_ERROR_NAME,
            description: "client error enum",
        });
    }
    return names;
}

/// Render a module as a flat file: the shared component models and per-operation
/// types at the crate root, followed by the requested generator interfaces.
///
/// The query/header/cookie inputs, request and response bodies, and response
/// enum an operation contributes are the same types whichever generator uses
/// them, so `operation::emit_operation_types` emits them once. The axum server
/// then adds its extractor and `IntoResponse` impls, and the `reqwest` client
/// its request-building methods, both naming those root types directly. Server
/// and client can therefore share a single file without a name clash.
pub fn emit_flat(
    module: &Module,
    service: &Service,
    server_urls: Option<&ServerUrls>,
    targets: Targets,
) -> Result<String> {
    let derives = usage::model_derives(module, service, targets);
    let foreign = usage::foreign_resolver(module);
    let mut items = module_items(module, &derives)?;
    items.extend(server_url_items(server_urls)?);
    for operation in &service.operations {
        items.extend(operation::emit_operation_types(operation, targets, &foreign)?);
    }
    if targets.server {
        items.extend(axum::AxumServer.emit(service)?);
    }
    if targets.client {
        items.extend(reqwest::ReqwestClient.emit(service)?);
    }
    return render(&items);
}

/// Emit the server-URL items, or nothing when the feature is disabled or the
/// spec declares no servers.
fn server_url_items(server_urls: Option<&ServerUrls>) -> Result<Vec<TokenStream>> {
    return match server_urls {
        Some(server_urls) => servers::emit_server_urls(server_urls),
        None => Ok(Vec::new()),
    };
}

/// Lower every IR item in a module into its token stream, applying each item's
/// derive set from `derives` (defaulting to every trait when absent).
fn module_items(module: &Module, derives: &HashMap<String, ModelDerives>) -> Result<Vec<TokenStream>> {
    let mut items = Vec::with_capacity(module.items.len());
    for item in &module.items {
        let set = derives.get(item.name()).copied().unwrap_or_else(ModelDerives::both);
        items.push(models::emit_item(item, set)?);
    }
    return Ok(items);
}

/// Pretty-print a sequence of top-level items, one blank line apart, prefixed
/// with the generated-file header.
fn render(items: &[TokenStream]) -> Result<String> {
    let mut out = String::from(HEADER);
    out.push_str(&render_body(items)?);
    return Ok(out);
}

/// Pretty-print a sequence of items, one blank line apart, without the header.
///
/// Each item is parsed and pretty-printed on its own so that a blank line
/// separates adjacent items — prettyplease otherwise emits them with no
/// separation, which is hard to read when many `pub` items follow each other.
fn render_body(items: &[TokenStream]) -> Result<String> {
    let mut out = String::new();
    for (index, tokens) in items.iter().enumerate() {
        let file = syn::parse2::<syn::File>(tokens.clone()).map_err(|source| {
            return Error::InvalidGeneratedCode { source };
        })?;
        if index > 0 {
            out.push('\n');
        }
        out.push_str(&prettyplease::unparse(&file));
    }
    return Ok(out);
}

/// Render a doc attribute, or nothing when there is no documentation.
pub(crate) fn doc_attr(doc: &Option<String>) -> TokenStream {
    let tokens = match doc {
        Some(text) => {
            // Leading space matches the `/// text` desugaring rustfmt produces.
            let spaced = format!(" {text}");
            quote! { #[doc = #spaced] }
        }
        None => quote! {},
    };
    return tokens;
}

/// Render one doc attribute per line, which rustdoc reads as one comment.
///
/// A blank line stays blank, so a caller can separate paragraphs with one. An
/// entry that already holds line breaks, such as a multi-line `description` from
/// the document, is split on them: a `#[doc]` carrying a `\n` prints as a
/// `/** */` block, which would sit unevenly among its `///` siblings.
pub(crate) fn doc_lines(lines: &[String]) -> TokenStream {
    let attrs = lines.iter().flat_map(|entry| return entry.split('\n')).map(|line| {
        // Leading space matches the `/// text` desugaring rustfmt produces. A
        // blank line takes none, so no trailing space reaches the output.
        let trimmed = line.trim_end();
        let spaced = if trimmed.is_empty() {
            String::new()
        } else {
            format!(" {trimmed}")
        };
        return quote! { #[doc = #spaced] };
    });
    return quote! { #(#attrs)* };
}

/// Render a Rust type expression.
pub(crate) fn emit_type(ty: &RustType) -> Result<TokenStream> {
    let tokens = match ty {
        RustType::Bool => quote! { bool },
        RustType::I32 => quote! { i32 },
        RustType::I64 => quote! { i64 },
        RustType::U32 => quote! { u32 },
        RustType::U64 => quote! { u64 },
        RustType::F64 => quote! { f64 },
        RustType::String => quote! { String },
        RustType::Value => quote! { serde_json::Value },
        RustType::Date => quote! { chrono::NaiveDate },
        RustType::DateTime => quote! { chrono::DateTime<chrono::Utc> },
        RustType::Uuid => quote! { uuid::Uuid },
        RustType::Bytes => quote! { Vec<u8> },
        RustType::Vec(inner) => {
            let inner = emit_type(inner)?;
            quote! { Vec<#inner> }
        }
        RustType::Map(inner) => {
            let inner = emit_type(inner)?;
            quote! { std::collections::HashMap<String, #inner> }
        }
        RustType::Option(inner) => {
            let inner = emit_type(inner)?;
            quote! { Option<#inner> }
        }
        RustType::Boxed(inner) => {
            let inner = emit_type(inner)?;
            quote! { Box<#inner> }
        }
        RustType::Named(name) => {
            let ident = to_ident(name, Case::Pascal).to_token();
            quote! { #ident }
        }
        RustType::External { module, name } => {
            let path: syn::Path = syn::parse_str(module).map_err(|err| {
                return Error::UnsupportedSchema {
                    path: "import-mapping".to_owned(),
                    reason: format!("module path `{module}` is not a valid Rust path expression: {err}"),
                };
            })?;
            let ident = to_ident(name, Case::Pascal).to_token();
            quote! { #path::#ident }
        }
        RustType::Verbatim { text, .. } => {
            let parsed: TokenStream = text.parse().map_err(|err: proc_macro2::LexError| {
                return Error::UnsupportedSchema {
                    path: "x-rust-type".to_owned(),
                    reason: format!("value `{text}` is not a valid Rust type expression: {err}"),
                };
            })?;
            parsed
        }
    };
    return Ok(tokens);
}

/// Emit the plain per-operation multipart struct (`<Op>Multipart`) shared by the
/// server extractor and the client request builder: one public field per part,
/// wrapped in `Option` when the part is optional. The server augments this with a
/// `FromRequest` impl. The client reads the fields to build a `reqwest` form.
pub(crate) fn emit_multipart_struct(multipart: &Multipart, foreign: &usage::ForeignResolver) -> Result<TokenStream> {
    let name = multipart.name.to_token();
    let field_types = multipart.fields.iter().map(|field| return &field.ty);
    let derive_attr = models::plain_derive_attr(models::DEBUG_AND_CLONE, foreign.of_types(field_types));
    let mut field_defs = Vec::with_capacity(multipart.fields.len());
    for field in &multipart.fields {
        let ident = field.rust_name.to_token();
        let ty = emit_type(&field.ty)?;
        let field_ty = if field.optional {
            quote! { Option<#ty> }
        } else {
            quote! { #ty }
        };
        field_defs.push(quote! { pub #ident: #field_ty, });
    }
    return Ok(quote! {
        #derive_attr
        pub struct #name {
            #(#field_defs)*
        }
    });
}

/// Emit the plain enum backing a negotiated (multi-content-type) body: one
/// variant per content representation, carrying that representation's decoded
/// type. Shared by the server (which augments the request enum with a
/// `FromRequest` impl and renders the response enum with `IntoResponse`) and the
/// client (which matches on it to build a request or decode a response).
pub(crate) fn emit_negotiated_body_enum(
    body: &NegotiatedBody,
    foreign: &usage::ForeignResolver,
) -> Result<TokenStream> {
    let name = body.name.to_token();
    let variant_types = body.variants.iter().map(|variant| return &variant.body.ty);
    let derive_attr = models::plain_derive_attr(models::DEBUG_CLONE_AND_EQ, foreign.of_types(variant_types));
    let mut variants = Vec::with_capacity(body.variants.len());
    for variant in &body.variants {
        let ident = variant.variant.to_token();
        let ty = emit_type(&variant.body.ty)?;
        variants.push(quote! { #ident(#ty) });
    }
    return Ok(quote! {
        #derive_attr
        pub enum #name {
            #(#variants),*
        }
    });
}