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;
pub trait ServerEmitter {
fn emit(&self, service: &Service) -> Result<Vec<TokenStream>>;
}
pub trait ClientEmitter {
fn emit(&self, service: &Service) -> Result<Vec<TokenStream>>;
}
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\"
)]
";
pub fn emit_module(module: &Module, server_urls: Option<&ServerUrls>) -> Result<String> {
let mut items = module_items(module, &usage::models_only_derives(module))?;
items.extend(server_url_items(server_urls)?);
return render(&items);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Targets {
pub server: bool,
pub client: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct PreludeTypeName {
pub name: &'static str,
pub used_for: &'static str,
}
pub fn prelude_type_names(targets: Targets) -> Vec<PreludeTypeName> {
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;
}
#[derive(Debug, Clone, Copy)]
pub struct ReservedTypeName {
pub name: &'static str,
pub description: &'static str,
}
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;
}
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);
}
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()),
};
}
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);
}
fn render(items: &[TokenStream]) -> Result<String> {
let mut out = String::from(HEADER);
out.push_str(&render_body(items)?);
return Ok(out);
}
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);
}
pub(crate) fn doc_attr(doc: &Option<String>) -> TokenStream {
let tokens = match doc {
Some(text) => {
let spaced = format!(" {text}");
quote! { #[doc = #spaced] }
}
None => quote! {},
};
return tokens;
}
pub(crate) fn doc_lines(lines: &[String]) -> TokenStream {
let attrs = lines.iter().flat_map(|entry| return entry.split('\n')).map(|line| {
let trimmed = line.trim_end();
let spaced = if trimmed.is_empty() {
String::new()
} else {
format!(" {trimmed}")
};
return quote! { #[doc = #spaced] };
});
return quote! { #(#attrs)* };
}
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);
}
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)*
}
});
}
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),*
}
});
}