use proc_macro2::Ident;
use proc_macro2::TokenStream;
use quote::format_ident;
use quote::quote;
use crate::emit::HEADER;
use crate::emit::Targets;
use crate::emit::axum;
use crate::emit::operation;
use crate::emit::render_body;
use crate::emit::reqwest;
use crate::emit::usage;
use crate::error::Result;
use crate::ir::Module;
use crate::ir::ServerUrls;
use crate::ir::Service;
use crate::naming::RustIdent;
use crate::naming::operations::axum_handler_name;
use crate::package::GeneratedFile;
use crate::package::GeneratedPackage;
const MODELS: &str = "models";
const SERVER_URLS: &str = "server_urls";
const OPERATIONS: &str = "operations";
const SERVER: &str = "server";
const CLIENT: &str = "client";
const OPERATION_DEPTH: usize = 2;
const MODULE_DEPTH: usize = 1;
pub fn emit_package(
module: &Module,
service: &Service,
server_urls: Option<&ServerUrls>,
targets: Targets,
stem: &str,
) -> Result<GeneratedPackage> {
let derives = usage::model_derives(module, service, targets);
let foreign = usage::foreign_resolver(module);
let modules: Vec<OperationModule> = service
.operations
.iter()
.map(|operation| return operation_module(&operation.name))
.collect();
let mut package = Builder::new(stem);
let models = super::module_items(module, &derives)?;
let has_models = !models.is_empty();
package.add(MODELS, models)?;
package.add(SERVER_URLS, super::server_url_items(server_urls)?)?;
let mut declarations = Vec::with_capacity(modules.len());
for (operation, module) in service.operations.iter().zip(&modules) {
let imports = imports(OPERATION_DEPTH, has_models, false, TokenStream::new());
let items = operation::emit_operation_types(operation, targets, &foreign)?;
package.add_child(OPERATIONS, &module.stem, imports, items)?;
declarations.push(reexport(OPERATIONS, module));
}
package.add(OPERATIONS, block(declarations))?;
if targets.server {
let items = server_items(service, &modules, has_models, &mut package)?;
package.add(SERVER, items)?;
}
if targets.client {
let items = client_items(service, &modules, has_models, &mut package)?;
package.add(CLIENT, items)?;
}
return package.finish();
}
fn server_items(
service: &Service,
modules: &[OperationModule],
has_models: bool,
package: &mut Builder,
) -> Result<Vec<TokenStream>> {
let items = axum::server_items(service, axum::HandlerVisibility::Parent)?;
let api = format_ident!("{}", axum::API_TRAIT_NAME);
let mut declarations = Vec::with_capacity(modules.len());
for ((entry, operation), module) in items.operations.into_iter().zip(&service.operations).zip(modules) {
let imports = imports(OPERATION_DEPTH, has_models, true, quote! { use super::#api; });
let mut file = entry.extractors;
file.push(entry.into_response);
file.push(entry.handler);
package.add_child(SERVER, &module.stem, imports, file)?;
let declaration = declare(SERVER, module);
let ident = &module.ident;
let handler = axum_handler_name(&operation.name).to_token();
declarations.push(quote! {
#declaration
use #ident::#handler;
});
}
let mut file: Vec<TokenStream> = imports(MODULE_DEPTH, has_models, !modules.is_empty(), TokenStream::new())
.into_iter()
.collect();
file.extend(block(declarations));
file.push(items.api_trait);
file.push(items.router);
return Ok(file);
}
fn client_items(
service: &Service,
modules: &[OperationModule],
has_models: bool,
package: &mut Builder,
) -> Result<Vec<TokenStream>> {
let items = reqwest::client_items(service)?;
let client = format_ident!("{}", reqwest::CLIENT_STRUCT_NAME);
let error = format_ident!("{}", reqwest::CLIENT_ERROR_NAME);
let encode_set = format_ident!("{}", reqwest::ENCODE_SET_NAME);
let mut declarations = Vec::with_capacity(modules.len());
for ((method, operation), module) in items
.operation_methods
.into_iter()
.zip(&service.operations)
.zip(modules)
{
let mut extra = quote! { use super::{#client, #error}; };
if !operation.path_params.is_empty() {
extra.extend(quote! { use super::#encode_set; });
}
let imports = imports(OPERATION_DEPTH, has_models, true, extra);
let body = vec![quote! {
impl #client {
#method
}
}];
package.add_child(CLIENT, &module.stem, imports, body)?;
declarations.push(declare(CLIENT, module));
}
let shared = items.shared_methods;
let mut file = block(declarations);
file.push(items.error);
file.extend(items.encode_set);
file.push(items.client_struct);
file.push(quote! {
impl #client {
#(#shared)*
}
});
return Ok(file);
}
struct Builder {
stem: String,
files: Vec<GeneratedFile>,
mounted: Vec<&'static str>,
}
impl Builder {
fn new(stem: &str) -> Self {
return Self {
stem: stem.to_owned(),
files: Vec::new(),
mounted: Vec::new(),
};
}
fn add(&mut self, name: &'static str, items: Vec<TokenStream>) -> Result<()> {
if items.is_empty() {
return Ok(());
}
let path = format!("{}/{name}.rs", self.stem);
self.files.push(GeneratedFile::new(path, render(&items)?));
self.mounted.push(name);
return Ok(());
}
fn add_child(
&mut self,
parent: &'static str,
stem: &str,
imports: Option<TokenStream>,
items: Vec<TokenStream>,
) -> Result<()> {
let file: Vec<TokenStream> = imports.into_iter().chain(items).collect();
let path = format!("{}/{parent}/{stem}.rs", self.stem);
self.files.push(GeneratedFile::new(path, render(&file)?));
return Ok(());
}
fn finish(mut self) -> Result<GeneratedPackage> {
let mounts = self.mounted.iter().map(|name| {
let ident = format_ident!("{name}");
let path = format!("{}/{name}.rs", self.stem);
return quote! {
#[path = #path]
mod #ident;
pub use #ident::*;
};
});
let root = render(&[quote! { #(#mounts)* }])?;
self.files.sort_by(|left, right| return left.path().cmp(right.path()));
return Ok(GeneratedPackage::new(root, self.files));
}
}
fn block(declarations: Vec<TokenStream>) -> Vec<TokenStream> {
if declarations.is_empty() {
return Vec::new();
}
return vec![quote! { #(#declarations)* }];
}
struct OperationModule {
stem: String,
ident: Ident,
}
fn operation_module(name: &RustIdent) -> OperationModule {
let text = name.logical();
let stem = if text == "mod" {
format!("{text}_")
} else {
text.to_owned()
};
return OperationModule {
stem,
ident: name.to_token(),
};
}
fn reexport(parent: &'static str, module: &OperationModule) -> TokenStream {
let declaration = declare(parent, module);
let ident = &module.ident;
return quote! {
#declaration
pub use #ident::*;
};
}
fn declare(parent: &'static str, module: &OperationModule) -> TokenStream {
let path = format!("{parent}/{}.rs", module.stem);
let ident = &module.ident;
return quote! {
#[path = #path]
mod #ident;
};
}
fn imports(depth: usize, models: bool, operations: bool, extra: TokenStream) -> Option<TokenStream> {
if !models && !operations && extra.is_empty() {
return None;
}
let root = root_path(depth);
let models = models.then(|| {
let ident = format_ident!("{MODELS}");
return quote! { use #root #ident::*; };
});
let operations = operations.then(|| {
let ident = format_ident!("{OPERATIONS}");
return quote! { use #root #ident::*; };
});
return Some(quote! {
#models
#operations
#extra
});
}
fn root_path(depth: usize) -> TokenStream {
let hops = std::iter::repeat_n(quote! { super:: }, depth);
return quote! { #(#hops)* };
}
fn render(items: &[TokenStream]) -> Result<String> {
let mut out = String::from(HEADER);
out.push_str(&render_body(items)?);
return Ok(out);
}