use http::Method;
use indexmap::IndexMap;
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use super::{Visibility, enums::ResponseEnumFragment};
use crate::generator::{
ast::{ContentCategory, HandlerBodyInfo, ResponseEnumDef, ResponseVariant, ServerRequestTraitDef, ServerTraitMethod},
codegen::http::HttpStatusCode,
};
pub struct ServerGenerator {
server_trait: Option<ServerRequestTraitDef>,
visibility: Visibility,
with_types_import: bool,
}
impl ServerGenerator {
pub fn new(server_trait: Option<ServerRequestTraitDef>, visibility: Visibility) -> Self {
Self {
server_trait,
visibility,
with_types_import: false,
}
}
pub fn with_types_import(mut self) -> Self {
self.with_types_import = true;
self
}
}
impl ToTokens for ServerGenerator {
fn to_tokens(&self, tokens: &mut TokenStream) {
let types_import = self.with_types_import.then(|| quote! { use super::types::*; });
let Some(def) = self.server_trait.as_ref() else {
return;
};
let trait_fragment = ServerTraitFragment::new(def.clone(), self.visibility);
let handlers = def
.methods
.iter()
.map(|m| HandlerFunctionFragment::new(m.clone(), self.visibility))
.collect::<Vec<_>>();
let router = RouterFragment::new(def.methods.clone(), self.visibility);
tokens.extend(quote! {
use axum::{
Router,
extract::{Path, Query, State},
http::HeaderMap,
response::IntoResponse,
routing::{delete, get, head, options, patch, post, put, trace},
};
#types_import
#trait_fragment
#(#handlers)*
#router
});
}
}
#[derive(Clone, Debug)]
struct ServerTraitFragment {
def: ServerRequestTraitDef,
vis: Visibility,
}
impl ServerTraitFragment {
fn new(def: ServerRequestTraitDef, vis: Visibility) -> Self {
Self { def, vis }
}
}
impl ToTokens for ServerTraitFragment {
fn to_tokens(&self, tokens: &mut TokenStream) {
let vis = self.vis.to_tokens();
let name = &self.def.name;
let methods = self.def.methods.iter().cloned().map(ServerTraitMethodFragment);
tokens.extend(quote! {
#vis trait #name: Send + Sync {
#(#methods)*
}
});
}
}
#[derive(Clone, Debug)]
struct ServerTraitMethodFragment(ServerTraitMethod);
impl ToTokens for ServerTraitMethodFragment {
fn to_tokens(&self, tokens: &mut TokenStream) {
let name = &self.0.name;
let docs = &self.0.docs;
let (request_param, return_type) = match (&self.0.request_type, &self.0.response_type) {
(Some(req), Some(resp)) => (quote! { request: #req }, quote! { #resp }),
(Some(req), None) => (quote! { request: #req }, quote! { () }),
(None, Some(resp)) => (quote! {}, quote! { #resp }),
(None, None) => (quote! {}, quote! { () }),
};
tokens.extend(quote! {
#docs
fn #name(&self, #request_param) -> impl std::future::Future<Output = anyhow::Result<#return_type>> + Send;
});
}
}
#[derive(Clone, Debug)]
pub(crate) struct AxumIntoResponse(ResponseEnumDef);
impl AxumIntoResponse {
pub(crate) fn new(def: ResponseEnumDef) -> Self {
Self(def)
}
}
impl ToTokens for AxumIntoResponse {
fn to_tokens(&self, tokens: &mut TokenStream) {
let name = &self.0.name;
let variants = self
.0
.variants
.iter()
.map(|v| AxumIntoResponseVariant::new(v.clone()))
.collect::<Vec<_>>();
let ts = quote! {
impl IntoResponse for #name {
fn into_response(self) -> axum::response::Response {
match self {
#(#variants),*
}
}
}
};
tokens.extend(ts);
}
}
#[derive(Clone, Debug)]
pub(crate) struct AxumIntoResponseVariant(ResponseVariant);
impl AxumIntoResponseVariant {
pub(crate) fn new(variant: ResponseVariant) -> Self {
Self(variant)
}
}
impl ToTokens for AxumIntoResponseVariant {
fn to_tokens(&self, tokens: &mut TokenStream) {
let variant = &self.0.variant_name;
let status_code = HttpStatusCode::new(self.0.status_code);
let ts = if self.0.schema_type.is_some() {
quote! {
Self::#variant(data) => (#status_code, axum::Json(data)).into_response()
}
} else {
quote! {
Self::#variant => #status_code.into_response()
}
};
tokens.extend(ts);
}
}
#[derive(Clone, Debug)]
pub(crate) struct AxumResponseEnumFragment {
vis: Visibility,
def: ResponseEnumDef,
}
impl AxumResponseEnumFragment {
pub(crate) fn new(vis: Visibility, def: ResponseEnumDef) -> Self {
Self { vis, def }
}
}
impl ToTokens for AxumResponseEnumFragment {
fn to_tokens(&self, tokens: &mut TokenStream) {
let response = ResponseEnumFragment::new(self.vis, self.def.clone());
let into_response = AxumIntoResponse::new(self.def.clone());
let ts = quote! {
#response
#into_response
};
tokens.extend(ts);
}
}
#[derive(Clone, Debug)]
struct HandlerFunctionFragment {
method: ServerTraitMethod,
vis: Visibility,
}
impl HandlerFunctionFragment {
fn new(method: ServerTraitMethod, vis: Visibility) -> Self {
Self { method, vis }
}
}
impl ToTokens for HandlerFunctionFragment {
fn to_tokens(&self, tokens: &mut TokenStream) {
let vis = self.vis.to_tokens();
let fn_name = &self.method.name;
let trait_name = format_ident!("ApiServer");
let extractors = ExtractorsFragment::new(self.method.clone());
let request_construction = RequestConstructionFragment::new(self.method.clone());
let return_type = self
.method
.response_type
.as_ref()
.map_or_else(|| quote! { impl IntoResponse }, |resp| quote! { #resp });
let service_call = if self.method.request_type.is_some() {
quote! { service.#fn_name(request).await }
} else {
quote! { service.#fn_name().await }
};
let error_handling = quote! {
match result {
Ok(response) => response.into_response(),
Err(e) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
format!("Internal error: {e}")
).into_response(),
}
};
tokens.extend(quote! {
#vis async fn #fn_name<S>(
#extractors
) -> impl IntoResponse
where
S: #trait_name + Clone + Send + Sync + 'static,
{
#request_construction
let result: anyhow::Result<#return_type> = #service_call;
#error_handling
}
});
}
}
#[derive(Clone, Debug)]
struct ExtractorsFragment {
method: ServerTraitMethod,
}
impl ExtractorsFragment {
fn new(method: ServerTraitMethod) -> Self {
Self { method }
}
}
impl ToTokens for ExtractorsFragment {
fn to_tokens(&self, tokens: &mut TokenStream) {
let mut parts = vec![quote! { State(service): State<S> }];
if let Some(path_type) = &self.method.path_params_type {
parts.push(quote! { Path(path): Path<#path_type> });
}
if let Some(query_type) = &self.method.query_params_type {
parts.push(quote! { Query(query): Query<#query_type> });
}
if self.method.header_params_type.is_some() {
parts.push(quote! { headers: HeaderMap });
}
if let Some(body_info) = &self.method.body_info {
let body_extractor = BodyExtractorFragment::new(body_info.clone());
parts.push(body_extractor.into_token_stream());
}
let ts = quote! { #(#parts),* };
tokens.extend(ts);
}
}
#[derive(Clone, Debug)]
struct BodyExtractorFragment {
body_info: HandlerBodyInfo,
}
impl BodyExtractorFragment {
fn new(body_info: HandlerBodyInfo) -> Self {
Self { body_info }
}
}
impl ToTokens for BodyExtractorFragment {
fn to_tokens(&self, tokens: &mut TokenStream) {
let body_type = &self.body_info.body_type;
let ts = match self.body_info.content_category {
ContentCategory::Json | ContentCategory::Multipart => {
if self.body_info.optional {
quote! { body: Option<axum::Json<#body_type>> }
} else {
quote! { axum::Json(body): axum::Json<#body_type> }
}
}
ContentCategory::FormUrlEncoded => {
if self.body_info.optional {
quote! { body: Option<axum::extract::Form<#body_type>> }
} else {
quote! { axum::extract::Form(body): axum::extract::Form<#body_type> }
}
}
ContentCategory::Text | ContentCategory::EventStream | ContentCategory::Xml => {
if self.body_info.optional {
quote! { body: Option<String> }
} else {
quote! { body: String }
}
}
ContentCategory::Binary => {
if self.body_info.optional {
quote! { body: Option<axum::body::Bytes> }
} else {
quote! { body: axum::body::Bytes }
}
}
};
tokens.extend(ts);
}
}
#[derive(Clone, Debug)]
struct RequestConstructionFragment {
method: ServerTraitMethod,
}
impl RequestConstructionFragment {
fn new(method: ServerTraitMethod) -> Self {
Self { method }
}
}
impl ToTokens for RequestConstructionFragment {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Some(request_type) = &self.method.request_type else {
return;
};
let mut field_assignments = vec![];
if self.method.path_params_type.is_some() {
field_assignments.push(quote! { path });
}
if self.method.query_params_type.is_some() {
field_assignments.push(quote! { query });
}
if self.method.header_params_type.is_some() {
field_assignments.push(quote! {
header: (&headers).try_into().unwrap_or_default()
});
}
if let Some(body_info) = &self.method.body_info {
let needs_unwrap = matches!(
body_info.content_category,
ContentCategory::Json | ContentCategory::FormUrlEncoded | ContentCategory::Multipart
);
let body_expr = if needs_unwrap && body_info.optional {
quote! { body: body.map(|b| b.0) }
} else {
quote! { body }
};
field_assignments.push(body_expr);
}
tokens.extend(quote! {
let request = #request_type {
#(#field_assignments),*
};
});
}
}
#[derive(Clone, Debug)]
struct RouterFragment {
methods: Vec<ServerTraitMethod>,
vis: Visibility,
}
impl RouterFragment {
fn new(methods: Vec<ServerTraitMethod>, vis: Visibility) -> Self {
Self { methods, vis }
}
}
impl ToTokens for RouterFragment {
fn to_tokens(&self, tokens: &mut TokenStream) {
let vis = self.vis.to_tokens();
let routes_by_path: IndexMap<String, Vec<&ServerTraitMethod>> =
self.methods.iter().fold(IndexMap::new(), |mut acc, method| {
let path = method.path.to_axum_path();
acc.entry(path).or_default().push(method);
acc
});
let route_definitions = routes_by_path.into_iter().map(|(path, methods)| {
let method_handlers = methods.iter().map(|m| {
let fn_name = &m.name;
let http_method = HttpMethodFragment::new(m.http_method.clone());
quote! { #http_method(#fn_name::<S>) }
});
let chained = method_handlers.reduce(|acc, handler| quote! { #acc.#handler });
quote! { .route(#path, #chained) }
});
tokens.extend(quote! {
#vis fn router<S>(service: S) -> Router
where
S: ApiServer + Clone + Send + Sync + 'static,
{
Router::new()
#(#route_definitions)*
.with_state(service)
}
});
}
}
#[derive(Clone, Debug)]
struct HttpMethodFragment {
method: Method,
}
impl HttpMethodFragment {
fn new(method: Method) -> Self {
Self { method }
}
}
impl ToTokens for HttpMethodFragment {
fn to_tokens(&self, tokens: &mut TokenStream) {
let ts = match self.method {
Method::POST => quote! { post },
Method::PUT => quote! { put },
Method::DELETE => quote! { delete },
Method::PATCH => quote! { patch },
Method::HEAD => quote! { head },
Method::OPTIONS => quote! { options },
Method::TRACE => quote! { trace },
_ => quote! { get },
};
tokens.extend(ts);
}
}