Skip to main content

oapi_codegen/emit/
package.rs

1//! Emitting the [`crate::ir`] as a module tree.
2//!
3//! The root file at the configured output path declares each child module with
4//! an explicit `#[path]` and re-exports it, so every generated name keeps the
5//! position it had in the single-file layout. The explicit path matters: a root
6//! mounted with `#[path = "generated/restapi.rs"] mod restapi;` resolves a bare
7//! `mod models;` beside *itself* rather than under a `restapi/` directory, so
8//! only the written-out path places the children where they are. That path also
9//! resolves correctly when the root is a plain `mod restapi;` in `src/`, which
10//! lets one emitted tree serve both mounting styles.
11//!
12//! No `mod.rs` is written anywhere. A directory module beside a file module of
13//! the same name is the `E0761` ambiguity, and the explicit `#[path]` removes
14//! any need for one.
15
16use proc_macro2::Ident;
17use proc_macro2::TokenStream;
18use quote::format_ident;
19use quote::quote;
20
21use crate::emit::HEADER;
22use crate::emit::Targets;
23use crate::emit::axum;
24use crate::emit::operation;
25use crate::emit::render_body;
26use crate::emit::reqwest;
27use crate::emit::usage;
28use crate::error::Result;
29use crate::ir::Module;
30use crate::ir::ServerUrls;
31use crate::ir::Service;
32use crate::naming::RustIdent;
33use crate::naming::operations::axum_handler_name;
34use crate::package::GeneratedFile;
35use crate::package::GeneratedPackage;
36
37/// The module holding the component models.
38const MODELS: &str = "models";
39/// The module holding the server-URL constants and builders.
40const SERVER_URLS: &str = "server_urls";
41/// The module holding the per-operation input and response types.
42const OPERATIONS: &str = "operations";
43/// The module holding the axum server interface.
44const SERVER: &str = "server";
45/// The module holding the blocking `reqwest` client.
46const CLIENT: &str = "client";
47
48/// How many modules separate an operation's file from the package root.
49const OPERATION_DEPTH: usize = 2;
50/// How many modules separate a top-level module file from the package root.
51const MODULE_DEPTH: usize = 1;
52
53/// Emit the module tree for a run that lowered a service.
54///
55/// `stem` is the output file's stem, which names the companion directory the
56/// children live in.
57pub fn emit_package(
58    module: &Module,
59    service: &Service,
60    server_urls: Option<&ServerUrls>,
61    targets: Targets,
62    stem: &str,
63) -> Result<GeneratedPackage> {
64    let derives = usage::model_derives(module, service, targets);
65    let foreign = usage::foreign_resolver(module);
66    let modules: Vec<OperationModule> = service
67        .operations
68        .iter()
69        .map(|operation| return operation_module(&operation.name))
70        .collect();
71
72    let mut package = Builder::new(stem);
73
74    let models = super::module_items(module, &derives)?;
75    let has_models = !models.is_empty();
76    package.add(MODELS, models)?;
77    package.add(SERVER_URLS, super::server_url_items(server_urls)?)?;
78
79    let mut declarations = Vec::with_capacity(modules.len());
80    for (operation, module) in service.operations.iter().zip(&modules) {
81        let imports = imports(OPERATION_DEPTH, has_models, false, TokenStream::new());
82        let items = operation::emit_operation_types(operation, targets, &foreign)?;
83        package.add_child(OPERATIONS, &module.stem, imports, items)?;
84        declarations.push(reexport(OPERATIONS, module));
85    }
86    package.add(OPERATIONS, block(declarations))?;
87
88    if targets.server {
89        let items = server_items(service, &modules, has_models, &mut package)?;
90        package.add(SERVER, items)?;
91    }
92    if targets.client {
93        let items = client_items(service, &modules, has_models, &mut package)?;
94        package.add(CLIENT, items)?;
95    }
96
97    return package.finish();
98}
99
100/// Emit the server operation files and return the items `server.rs` holds.
101fn server_items(
102    service: &Service,
103    modules: &[OperationModule],
104    has_models: bool,
105    package: &mut Builder,
106) -> Result<Vec<TokenStream>> {
107    let items = axum::server_items(service, axum::HandlerVisibility::Parent)?;
108    let api = format_ident!("{}", axum::API_TRAIT_NAME);
109    let mut declarations = Vec::with_capacity(modules.len());
110    for ((entry, operation), module) in items.operations.into_iter().zip(&service.operations).zip(modules) {
111        // The handler names the `Api` trait in its bound, and the router in the
112        // parent module names the handler.
113        let imports = imports(OPERATION_DEPTH, has_models, true, quote! { use super::#api; });
114        let mut file = entry.extractors;
115        file.push(entry.into_response);
116        file.push(entry.handler);
117        package.add_child(SERVER, &module.stem, imports, file)?;
118        let declaration = declare(SERVER, module);
119        let ident = &module.ident;
120        let handler = axum_handler_name(&operation.name).to_token();
121        declarations.push(quote! {
122            #declaration
123            use #ident::#handler;
124        });
125    }
126    let mut file: Vec<TokenStream> = imports(MODULE_DEPTH, has_models, !modules.is_empty(), TokenStream::new())
127        .into_iter()
128        .collect();
129    file.extend(block(declarations));
130    file.push(items.api_trait);
131    file.push(items.router);
132    return Ok(file);
133}
134
135/// Emit the client operation files and return the items `client.rs` holds.
136fn client_items(
137    service: &Service,
138    modules: &[OperationModule],
139    has_models: bool,
140    package: &mut Builder,
141) -> Result<Vec<TokenStream>> {
142    let items = reqwest::client_items(service)?;
143    let client = format_ident!("{}", reqwest::CLIENT_STRUCT_NAME);
144    let error = format_ident!("{}", reqwest::CLIENT_ERROR_NAME);
145    let encode_set = format_ident!("{}", reqwest::ENCODE_SET_NAME);
146    let mut declarations = Vec::with_capacity(modules.len());
147    for ((method, operation), module) in items
148        .operation_methods
149        .into_iter()
150        .zip(&service.operations)
151        .zip(modules)
152    {
153        // An inherent `impl` applies crate-wide, so the method reaches callers
154        // from its own module. Only the names it mentions have to be imported.
155        let mut extra = quote! { use super::{#client, #error}; };
156        if !operation.path_params.is_empty() {
157            extra.extend(quote! { use super::#encode_set; });
158        }
159        let imports = imports(OPERATION_DEPTH, has_models, true, extra);
160        let body = vec![quote! {
161            impl #client {
162                #method
163            }
164        }];
165        package.add_child(CLIENT, &module.stem, imports, body)?;
166        declarations.push(declare(CLIENT, module));
167    }
168    let shared = items.shared_methods;
169    let mut file = block(declarations);
170    file.push(items.error);
171    file.extend(items.encode_set);
172    file.push(items.client_struct);
173    file.push(quote! {
174        impl #client {
175            #(#shared)*
176        }
177    });
178    return Ok(file);
179}
180
181/// Collects the files of a package and the modules its root mounts.
182struct Builder {
183    /// The output file's stem, which names the companion directory.
184    stem: String,
185    /// The finished child files.
186    files: Vec<GeneratedFile>,
187    /// The top-level modules the root mounts, in emission order.
188    mounted: Vec<&'static str>,
189}
190
191impl Builder {
192    /// Start an empty package for the given output stem.
193    fn new(stem: &str) -> Self {
194        return Self {
195            stem: stem.to_owned(),
196            files: Vec::new(),
197            mounted: Vec::new(),
198        };
199    }
200
201    /// Add a top-level module, skipping it when it holds no item.
202    ///
203    /// An empty module would leave the root re-exporting a module with nothing
204    /// public in it, which rustc rejects, so an absent module is the right
205    /// answer for a run that produces none of its items.
206    fn add(&mut self, name: &'static str, items: Vec<TokenStream>) -> Result<()> {
207        if items.is_empty() {
208            return Ok(());
209        }
210        let path = format!("{}/{name}.rs", self.stem);
211        self.files.push(GeneratedFile::new(path, render(&items)?));
212        self.mounted.push(name);
213        return Ok(());
214    }
215
216    /// Add one operation's file under a top-level module.
217    fn add_child(
218        &mut self,
219        parent: &'static str,
220        stem: &str,
221        imports: Option<TokenStream>,
222        items: Vec<TokenStream>,
223    ) -> Result<()> {
224        let file: Vec<TokenStream> = imports.into_iter().chain(items).collect();
225        let path = format!("{}/{parent}/{stem}.rs", self.stem);
226        self.files.push(GeneratedFile::new(path, render(&file)?));
227        return Ok(());
228    }
229
230    /// Render the root facade and hand back the finished package.
231    fn finish(mut self) -> Result<GeneratedPackage> {
232        let mounts = self.mounted.iter().map(|name| {
233            let ident = format_ident!("{name}");
234            let path = format!("{}/{name}.rs", self.stem);
235            return quote! {
236                #[path = #path]
237                mod #ident;
238                pub use #ident::*;
239            };
240        });
241        let root = render(&[quote! { #(#mounts)* }])?;
242        self.files.sort_by(|left, right| return left.path().cmp(right.path()));
243        return Ok(GeneratedPackage::new(root, self.files));
244    }
245}
246
247/// Join related declarations into one rendered item, or nothing when there are
248/// none.
249///
250/// Items are rendered a blank line apart, which reads well between types and
251/// badly between a run of one-line `mod` declarations.
252fn block(declarations: Vec<TokenStream>) -> Vec<TokenStream> {
253    if declarations.is_empty() {
254        return Vec::new();
255    }
256    return vec![quote! { #(#declarations)* }];
257}
258
259/// The file and module an operation gets.
260///
261/// The two are tracked together because they can differ. An operation named
262/// `mod` needs the file stem `mod_`, since `mod.rs` beside `operations.rs` is
263/// the `E0761` ambiguity, while its module item stays `r#mod`. The explicit
264/// `#[path]` ties the two back together.
265struct OperationModule {
266    /// The file stem, without the `.rs`.
267    stem: String,
268    /// The identifier of the module item, raw when the name is a keyword.
269    ident: Ident,
270}
271
272/// The file and module for the operation named `name`.
273///
274/// Operation names are unique `snake_case` identifiers, so the stems collide
275/// with nothing on any filesystem, including a case-insensitive one.
276fn operation_module(name: &RustIdent) -> OperationModule {
277    let text = name.logical();
278    let stem = if text == "mod" {
279        format!("{text}_")
280    } else {
281        text.to_owned()
282    };
283    return OperationModule {
284        stem,
285        ident: name.to_token(),
286    };
287}
288
289/// The declaration and re-export a parent module writes for one child.
290fn reexport(parent: &'static str, module: &OperationModule) -> TokenStream {
291    let declaration = declare(parent, module);
292    let ident = &module.ident;
293    return quote! {
294        #declaration
295        pub use #ident::*;
296    };
297}
298
299/// The `#[path]`-carrying `mod` item a parent module writes for one child.
300fn declare(parent: &'static str, module: &OperationModule) -> TokenStream {
301    let path = format!("{parent}/{}.rs", module.stem);
302    let ident = &module.ident;
303    return quote! {
304        #[path = #path]
305        mod #ident;
306    };
307}
308
309/// The `use` items a generated module needs, or nothing when it needs none.
310///
311/// `depth` is how many modules lie between the file and the package root. A file
312/// imports every name it could refer to, whether or not it does: an operation
313/// that names only primitives references no model, and the header's
314/// `unused_imports` allowance covers that.
315fn imports(depth: usize, models: bool, operations: bool, extra: TokenStream) -> Option<TokenStream> {
316    if !models && !operations && extra.is_empty() {
317        return None;
318    }
319    let root = root_path(depth);
320    let models = models.then(|| {
321        let ident = format_ident!("{MODELS}");
322        return quote! { use #root #ident::*; };
323    });
324    let operations = operations.then(|| {
325        let ident = format_ident!("{OPERATIONS}");
326        return quote! { use #root #ident::*; };
327    });
328    return Some(quote! {
329        #models
330        #operations
331        #extra
332    });
333}
334
335/// The `super::` chain that reaches the package root from `depth` modules down.
336fn root_path(depth: usize) -> TokenStream {
337    let hops = std::iter::repeat_n(quote! { super:: }, depth);
338    return quote! { #(#hops)* };
339}
340
341/// Render a file: the generated header, then the items one blank line apart.
342fn render(items: &[TokenStream]) -> Result<String> {
343    let mut out = String::from(HEADER);
344    out.push_str(&render_body(items)?);
345    return Ok(out);
346}