mod axum;
mod constraints;
mod models;
mod operation;
mod package;
mod reqwest;
mod servers;
mod union;
pub(crate) mod usage;
use std::collections::HashMap;
use proc_macro2::TokenStream;
use quote::quote;
use crate::emit::models::ModelDerives;
pub use crate::emit::package::emit_package;
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>>;
}
macro_rules! generated_marker {
() => {
"// Code generated by oapi-codegen-rust. DO NOT EDIT."
};
}
pub const GENERATED_MARKER: &str = generated_marker!();
pub const HEADER: &str = concat!(
generated_marker!(),
"
#![allow(
dead_code,
unused_imports,
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), false)?;
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![ReservedTypeName {
name: "Nullable",
description: "nullable JSON value",
}];
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, uses_nullable(module, Some(service)))?;
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>,
service_nullable: bool,
) -> Result<Vec<TokenStream>> {
let mut items = Vec::with_capacity(module.items.len());
if service_nullable || module.items.iter().any(item_uses_nullable) {
items.push(quote! {
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, serde::Serialize)]
#[serde(untagged)]
pub enum Nullable<T> {
#[default]
Null,
Value(T),
}
impl<T> Nullable<T> {
pub fn as_ref(&self) -> Option<&T> {
return match self {
Self::Null => None,
Self::Value(value) => Some(value),
};
}
}
impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Nullable<T> {
fn deserialize<D>(deserializer: D) -> ::core::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct NullableVisitor<T>(::core::marker::PhantomData<T>);
impl<'de, T: serde::Deserialize<'de>> serde::de::Visitor<'de> for NullableVisitor<T> {
type Value = Nullable<T>;
fn expecting(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
return formatter.write_str("a present value or null");
}
fn visit_newtype_struct<D>(self, deserializer: D) -> ::core::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
return <Option<T> as serde::Deserialize>::deserialize(deserializer).map(|value| {
return match value {
Some(value) => Nullable::Value(value),
None => Nullable::Null,
};
});
}
fn visit_map<M>(self, map: M) -> ::core::result::Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
return T::deserialize(serde::de::value::MapAccessDeserializer::new(map))
.map(Nullable::Value);
}
}
return deserializer.deserialize_newtype_struct(
"Nullable",
NullableVisitor(::core::marker::PhantomData),
);
}
}
});
}
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);
}
pub(crate) fn uses_nullable(module: &Module, service: Option<&Service>) -> bool {
use crate::ir::RequestPayload;
use crate::ir::ResponseBody;
return module.items.iter().any(item_uses_nullable)
|| service.is_some_and(|service| {
return service.operations.iter().any(|operation| {
let request = match &operation.request {
Some(RequestPayload::Single(body)) => type_uses_nullable(&body.ty),
Some(RequestPayload::Negotiated(body)) => body
.variants
.iter()
.any(|variant| return type_uses_nullable(&variant.body.ty)),
_ => false,
};
return request
|| operation.responses.iter().any(|response| {
return match &response.body {
Some(ResponseBody::Single(body)) => type_uses_nullable(&body.ty),
Some(ResponseBody::Negotiated(body)) => body
.variants
.iter()
.any(|variant| return type_uses_nullable(&variant.body.ty)),
None => false,
};
});
});
});
}
fn item_uses_nullable(item: &crate::ir::Item) -> bool {
return match item {
crate::ir::Item::Struct(value) => {
value.fields.iter().any(|field| return type_uses_nullable(&field.ty))
|| value.additional_properties.as_ref().is_some_and(type_uses_nullable)
}
crate::ir::Item::Alias(value) => type_uses_nullable(&value.ty),
crate::ir::Item::Enum(value) => match &value.kind {
crate::ir::EnumKind::Union(variants) => {
variants.iter().any(|variant| return type_uses_nullable(&variant.ty))
}
_ => false,
},
};
}
fn type_uses_nullable(ty: &RustType) -> bool {
return match ty {
RustType::Nullable(_) => true,
RustType::Vec(inner) | RustType::Map(inner) | RustType::Option(inner) | RustType::Boxed(inner) => {
type_uses_nullable(inner)
}
_ => false,
};
}
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) => doc_lines(std::slice::from_ref(text)),
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::Nullable(inner) => {
let inner = emit_type(inner)?;
quote! { Nullable<#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),*
}
});
}