use std::collections::{BTreeMap, HashMap, HashSet};
use syn::{ReturnType, Type};
use vespera_core::route::{Header, MediaType, Response};
use super::schema::parse_type_to_schema_ref;
use crate::parser::is_keyword_type::{KeywordType, is_keyword_type, is_keyword_type_by_type_path};
fn unwrap_json(ty: &Type) -> &Type {
if let Type::Path(type_path) = ty
&& let Some(segment) = type_path.path.segments.last()
&& segment.ident == "Json"
&& let syn::PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
{
return inner_ty;
}
ty
}
fn extract_result_types(ty: &Type) -> Option<(Type, Type)> {
let unwrapped = unwrap_json(ty);
let result_type = if let Type::Path(type_path) = unwrapped {
type_path
} else if let Type::Reference(type_ref) = unwrapped
&& let Type::Path(type_path) = type_ref.elem.as_ref()
{
type_path
} else {
return None;
};
let path = &result_type.path;
if path.segments.is_empty() {
return None;
}
if is_keyword_type_by_type_path(result_type, &KeywordType::Result)
&& let Some(segment) = path.segments.last()
&& let syn::PathArguments::AngleBracketed(args) = &segment.arguments
&& args.args.len() >= 2
&& let (Some(syn::GenericArgument::Type(ok_ty)), Some(syn::GenericArgument::Type(err_ty))) =
(args.args.first(), args.args.get(1))
{
let ok_ty_unwrapped = unwrap_json(ok_ty);
return Some((ok_ty_unwrapped.clone(), err_ty.clone()));
}
None
}
fn extract_status_code_tuple(err_ty: &Type) -> Option<(u16, Type)> {
if let Type::Tuple(tuple) = err_ty
&& tuple
.elems
.iter()
.any(|ty| is_keyword_type(ty, &KeywordType::StatusCode))
{
Some((400, unwrap_json(tuple.elems.last().unwrap()).clone()))
} else {
None
}
}
fn is_non_body_type(ty: &Type) -> bool {
is_keyword_type(ty, &KeywordType::StatusCode)
|| is_keyword_type(ty, &KeywordType::HeaderMap)
|| is_keyword_type(ty, &KeywordType::CookieJar)
}
fn extract_ok_payload_and_headers(ok_ty: &Type) -> (Type, Option<BTreeMap<String, Header>>) {
if let Type::Tuple(tuple) = ok_ty {
let payload_ty = tuple
.elems
.iter()
.rev()
.find(|ty| !is_non_body_type(ty))
.map(|ty| unwrap_json(ty).clone());
if let Some(payload_ty) = payload_ty {
let headers = if tuple
.elems
.iter()
.any(|ty| is_keyword_type(ty, &KeywordType::HeaderMap))
{
Some(BTreeMap::new())
} else {
None
};
return (payload_ty, headers);
}
}
(ok_ty.clone(), None)
}
fn is_string_like(ty: &Type) -> bool {
match ty {
Type::Reference(reference) => is_string_like(&reference.elem),
Type::Path(type_path) => type_path
.path
.segments
.last()
.is_some_and(|seg| seg.ident == "String" || seg.ident == "str"),
_ => false,
}
}
fn body_content_type(ty: &Type) -> &'static str {
if is_string_like(ty) {
"text/plain"
} else {
"application/json"
}
}
fn tuple_body(ty: &Type) -> &Type {
if let Type::Tuple(tuple) = ty {
tuple
.elems
.iter()
.rev()
.find(|elem| !is_non_body_type(elem))
.unwrap_or(ty)
} else {
ty
}
}
fn result_args(ty: &Type) -> Option<(&Type, &Type)> {
let type_path = match unwrap_json(ty) {
Type::Path(type_path) => type_path,
Type::Reference(type_ref) => match type_ref.elem.as_ref() {
Type::Path(type_path) => type_path,
_ => return None,
},
_ => return None,
};
if is_keyword_type_by_type_path(type_path, &KeywordType::Result)
&& let Some(segment) = type_path.path.segments.last()
&& let syn::PathArguments::AngleBracketed(args) = &segment.arguments
&& args.args.len() >= 2
&& let (Some(syn::GenericArgument::Type(ok)), Some(syn::GenericArgument::Type(err))) =
(args.args.first(), args.args.get(1))
{
Some((ok, err))
} else {
None
}
}
fn response_content_types(ty: &Type) -> (&'static str, &'static str) {
if let Some((ok, err)) = result_args(ty) {
(
body_content_type(tuple_body(ok)),
body_content_type(tuple_body(err)),
)
} else {
(body_content_type(tuple_body(ty)), "application/json")
}
}
fn content_for_type(
ty: &Type,
content_type: &str,
known_schemas: &HashSet<&str>,
struct_definitions: &HashMap<&str, &str>,
) -> Option<BTreeMap<String, MediaType>> {
if is_keyword_type(ty, &KeywordType::StatusCode) {
return None;
}
let schema = parse_type_to_schema_ref(ty, known_schemas, struct_definitions);
let mut content = BTreeMap::new();
content.insert(
content_type.to_string(),
MediaType {
schema: Some(schema),
example: None,
examples: None,
},
);
Some(content)
}
fn successful_response(
content: Option<BTreeMap<String, MediaType>>,
headers: Option<BTreeMap<String, Header>>,
) -> Response {
Response {
description: "Successful response".to_string(),
headers,
content,
}
}
fn error_response(content: Option<BTreeMap<String, MediaType>>) -> Response {
Response {
description: "Error response".to_string(),
headers: None,
content,
}
}
fn insert_result_responses(
responses: &mut BTreeMap<String, Response>,
ok_ty: &Type,
err_ty: &Type,
ok_content_type: &str,
err_content_type: &str,
known_schemas: &HashSet<&str>,
struct_definitions: &HashMap<&str, &str>,
) {
let (ok_payload_ty, ok_headers) = extract_ok_payload_and_headers(ok_ty);
let ok_content = content_for_type(
&ok_payload_ty,
ok_content_type,
known_schemas,
struct_definitions,
);
responses.insert(
"200".to_string(),
successful_response(ok_content, ok_headers),
);
if let Some((status_code, error_type)) = extract_status_code_tuple(err_ty) {
let err_content = content_for_type(
&error_type,
err_content_type,
known_schemas,
struct_definitions,
);
responses.insert(status_code.to_string(), error_response(err_content));
} else {
let err_ty_unwrapped = unwrap_json(err_ty);
let err_content = content_for_type(
err_ty_unwrapped,
err_content_type,
known_schemas,
struct_definitions,
);
responses.insert("400".to_string(), error_response(err_content));
}
}
fn insert_plain_response(
responses: &mut BTreeMap<String, Response>,
ty: &Type,
content_type: &str,
known_schemas: &HashSet<&str>,
struct_definitions: &HashMap<&str, &str>,
) {
let unwrapped_ty = unwrap_json(ty);
let content = content_for_type(
unwrapped_ty,
content_type,
known_schemas,
struct_definitions,
);
responses.insert("200".to_string(), successful_response(content, None));
}
pub fn parse_return_type(
return_type: &ReturnType,
known_schemas: &HashSet<&str>,
struct_definitions: &HashMap<&str, &str>,
) -> BTreeMap<String, Response> {
let mut responses = BTreeMap::new();
match return_type {
ReturnType::Default => {
responses.insert(
"200".to_string(),
Response {
description: "Successful response".to_string(),
headers: None,
content: None,
},
);
}
ReturnType::Type(_, ty) => {
let (ok_content_type, err_content_type) = response_content_types(ty);
if let Some((ok_ty, err_ty)) = extract_result_types(ty) {
insert_result_responses(
&mut responses,
&ok_ty,
&err_ty,
ok_content_type,
err_content_type,
known_schemas,
struct_definitions,
);
} else {
insert_plain_response(
&mut responses,
ty,
ok_content_type,
known_schemas,
struct_definitions,
);
}
}
}
responses
}
#[cfg(test)]
mod tests;