diff --git a/README.md b/README.md index 901812f..7576b1e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Note: This crate is in an early stage and may not work in all cases. Please open - Generate a TypeScript file with: - Functions - Data types as Interfaces + - Generics, even multiple and nested ones, look for that [here](#complete-example) - Using no extra dependencies in the generated TypeScript file. ## How to use @@ -39,8 +40,7 @@ Note: This crate is in an early stage and may not work in all cases. Please open ### Step 1: Define Structs and Functions -Use the `#[metadata]` macro with the `#[meta(...)]` attribute to define your data structures and functions. This macro allows `gluer` to generate metadata for these structs and functions as `const` values with the same visibility as the function or struct. When splitting these into other modules, you need to import these `const` values, but they are recognized by Rust's compiler, so there is no need to worry about that. - +To define your data structures and functions, use the `#[metadata]` macro along with the `#[meta(...)] `attribute. This macro enables `gluer` to generate metadata for these structures and functions. It does so by implementing the `metadata` function on structs or by creating a struct that implements both the `metadata` function and the handler-specific function. ```rust use axum::{ Json, @@ -90,12 +90,12 @@ use axum::{ }; use gluer::{Api, extract, metadata}; -// done like above -#[metadata] +// without `#[metadata]`, it's non-API-important async fn root() -> String { "Hello, World!".to_string() } +// done like above #[metadata] async fn hello() -> Json { "Hello, World!".to_string().into() @@ -112,7 +112,7 @@ let mut app: Api<()> = Api::new() Generate the API file using the `generate_client` function on the `Api` struct. This generates the TypeScript file. -```rust +```rust,no_run use gluer::Api; let app: Api<()> = Api::new(); @@ -157,33 +157,44 @@ async fn fetch_root(Query(test): Query>, Path(p): Path { - name: String, +pub struct Hello { + name: S, vec: Vec, } #[metadata] #[derive(Serialize, Deserialize, Default)] struct Age { - #[meta(into = String)] + // #[meta(into = String)] age: AgeInner, } +#[metadata] #[derive(Serialize, Deserialize, Default)] struct AgeInner { age: u8, } #[metadata] -async fn add_root(Path(_): Path, Json(hello): Json>) -> Json { +#[derive(Serialize, Deserialize, Default)] +struct Huh { + huh: T, +} + +// Even deep nested generics are supported +#[metadata] +async fn add_root( + Path(_): Path, + Json(hello): Json, String>, String>>, +) -> Json { Json(hello.name.to_string()) } -#[tokio::main] -async fn main() { +#[tokio::test] +async fn main_test() { let app: Api<()> = Api::new().route("/:p", extract!(get(fetch_root).post(add_root))); app.generate_client("tests/api.ts").unwrap(); diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 01b200a..6974b7d 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,7 +1,10 @@ use proc_macro::{self as pc}; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use std::{collections::HashMap, fmt, vec}; +use std::{ + collections::{HashMap, HashSet}, + fmt, vec, +}; use syn::{parenthesized, parse::Parse, spanned::Spanned, TypeParam}; fn s_err(span: proc_macro2::Span, msg: impl fmt::Display) -> syn::Error { @@ -18,29 +21,31 @@ pub fn extract(input: pc::TokenStream) -> pc::TokenStream { } fn extract_inner(input: TokenStream) -> syn::Result { - let ExtractArgs { routes } = syn::parse2::(input.clone())?; - let original_input = input; + let ExtractArgs { + routes: original_routes, + } = syn::parse2::(input.clone())?; - let routes = routes.iter().map(|Route { method, handler }| { + let routes = original_routes.iter().map(|Route { method, handler }| { let method_name = method.to_string(); let handler_name = handler.to_string(); - let fn_info = syn::Ident::new( - &format!("FN_{}", handler_name.to_uppercase()), - proc_macro2::Span::call_site(), - ); + let fn_info = syn::Ident::new(&handler_name, proc_macro2::Span::call_site()); + let fn_info = quote! { #fn_info::metadata() }; quote! { - gluer::Route { - url: "", - method: #method_name, - fn_name: #handler_name, - fn_info: #fn_info, + { + const ROUTE: gluer::Route<'static> = gluer::Route { + url: "", + method: #method_name, + fn_name: #handler_name, + fn_info: #fn_info, + }; + ROUTE } } }); - Ok(quote! { ( #original_input, &[#(#routes,)*] )}) + Ok(quote! { ( #(#original_routes).*, &[#(#routes,)*] )}) } struct ExtractArgs { @@ -80,6 +85,14 @@ impl Parse for Route { } } +impl ToTokens for Route { + fn to_tokens(&self, tokens: &mut TokenStream) { + let method = &self.method; + let handler = &self.handler; + tokens.extend(quote! { #method(#handler::#handler) }); + } +} + /// Put before structs or functions to be usable by the `glue` crate. #[proc_macro_attribute] pub fn metadata(args: pc::TokenStream, input: pc::TokenStream) -> pc::TokenStream { @@ -94,22 +107,38 @@ fn metadata_inner(args: TokenStream, input: TokenStream) -> syn::Result(input)?; let _args = syn::parse2::(args)?; - let (generated_const, ret) = match item { - syn::Item::Struct(item_struct) => generate_struct_const(item_struct)?, - syn::Item::Fn(item_fn) => generate_fn_const(item_fn)?, + let out = match item { + syn::Item::Struct(item_struct) => generate_struct(item_struct)?, + syn::Item::Fn(item_fn) => generate_function(item_fn)?, _ => return Err(s_err(span, "Expected struct or function")), }; Ok(quote! { - #generated_const - #ret + #out }) } -fn generate_struct_const( - mut item_struct: syn::ItemStruct, -) -> syn::Result<(TokenStream, TokenStream)> { - let struct_name = item_struct.ident.to_string(); +struct NoArgs {} + +impl syn::parse::Parse for NoArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if !input.is_empty() { + return Err(input.error("No arguments expected")); + } + Ok(NoArgs {}) + } +} + +fn generate_struct(mut item_struct: syn::ItemStruct) -> syn::Result { + let struct_name_ident = item_struct.ident.clone(); + let generics_ident_no_types = + if let Some(g) = extract_type_params_as_type(&item_struct.generics)? { + quote! { #g } + } else { + quote! {} + }; + let generics_ident = item_struct.generics.clone(); + let struct_name = struct_name_ident.to_string(); let vis = &item_struct.vis; let generics: Vec = item_struct .generics @@ -123,7 +152,7 @@ fn generate_struct_const( }) .collect(); - let mut dependencies = HashMap::new(); + let mut dependencies: Vec = Vec::new(); let item_struct_fields = item_struct.fields.clone(); @@ -164,19 +193,36 @@ fn generate_struct_const( } if into.is_none() { - match basic_rust_type(&field.ty, &generics) { - Ok(Some(RustType { - is_basic, inner_ty, .. - })) => { - if !is_basic { - dependencies.insert( - inner_ty.clone(), - format!("STRUCT_{}", inner_ty.to_uppercase()), - ); + let _ = match check(&field.ty, field.span(), &mut dependencies, &generics) { + Ok(_) => {} + Err(e) => return Some(Err(e)), + }; + fn check( + ty: &syn::Type, + span: proc_macro2::Span, + dependencies: &mut Vec, + generics: &Vec, + ) -> syn::Result<()> { + match check_rust_type(ty) { + Some(RustType2::Custom(inner_ty)) + | Some(RustType2::CustomGeneric(inner_ty, _)) => { + if !dependencies.contains(&inner_ty) && !generics.contains(&inner_ty) { + dependencies.push(inner_ty); + } } + + Some(RustType2::Generic(_, inner_tys)) => { + for inner in inner_tys { + let ty = + syn::parse_str::(&inner.into_tokens().to_string())?; + check(&ty, span, dependencies, generics)?; + } + } + + Some(_) => {} + None => return Err(syn::Error::new(span, "Unsupported field type")), } - Ok(None) => return Some(Err(s_err(field.span(), "Unsupported field type"))), - Err(e) => return Some(Err(e)), + Ok(()) } } @@ -184,18 +230,13 @@ fn generate_struct_const( }) .collect::>>()?; - let const_ident = syn::Ident::new( - &format!("STRUCT_{}", struct_name.to_uppercase()), - proc_macro2::Span::call_site(), - ); - let const_value = fields.iter().map(|(ident, ty)| { quote! { gluer::Field { name: #ident, ty: #ty } } }); - let dependencies_quote = dependencies.values().map(|struct_name| { - let struct_const = syn::Ident::new(struct_name, proc_macro2::Span::call_site()); - quote! { #struct_const } + let dependencies_quote = dependencies.iter().map(|struct_name| { + let struct_ident = syn::Ident::new(struct_name, proc_macro2::Span::call_site()); + quote! { #struct_ident::metadata() } }); let generics_quote = generics.iter().map(|generic| { @@ -204,17 +245,38 @@ fn generate_struct_const( let item_struct = quote! { #item_struct }; - Ok(( - quote! { - #vis const #const_ident: gluer::StructInfo = gluer::StructInfo { - name: #struct_name, - generics: &[#(#generics_quote),*], - fields: &[#(#const_value),*], - dependencies: &[#(#dependencies_quote),*] - }; - }, - item_struct, - )) + Ok(quote! { + #item_struct + + impl #generics_ident #struct_name_ident #generics_ident_no_types { + #vis const fn metadata() -> gluer::StructInfo<'static> { + const STRUCT_INFO: gluer::StructInfo<'static> = gluer::StructInfo { + name: #struct_name, + generics: &[#(#generics_quote),*], + fields: &[#(#const_value),*], + dependencies: &[#(#dependencies_quote),*] + }; + STRUCT_INFO + } + } + }) +} + +// Function to extract type parameters and convert them into a syn::Type +fn extract_type_params_as_type(generics: &syn::Generics) -> syn::Result> { + let type_params: Vec = generics + .type_params() + .map(|type_param| type_param.ident.to_string()) + .collect(); + + if type_params.is_empty() { + return Ok(None); + } + + Ok(Some(syn::parse_str(&format!( + "<{}>", + type_params.join(", ") + ))?)) } struct MetaAttr { @@ -252,13 +314,10 @@ fn parse_field_attr(attrs: &[syn::Attribute]) -> syn::Result { Ok(meta_attr) } -fn generate_fn_const( - item_fn: syn::ItemFn, -) -> syn::Result<(proc_macro2::TokenStream, proc_macro2::TokenStream)> { - let fn_name = item_fn.sig.ident.to_string(); +fn generate_function(item_fn: syn::ItemFn) -> syn::Result { + let fn_name_ident = item_fn.sig.ident.clone(); let vis = &item_fn.vis; let mut structs = HashMap::new(); - let generics: Vec = vec![]; let params = item_fn .sig @@ -267,247 +326,260 @@ fn generate_fn_const( .filter_map(|param| match param { syn::FnArg::Typed(syn::PatType { pat, ty, .. }) => { let pat = pat.to_token_stream().to_string(); - if let Some(RustType { - is_basic, - outer_ty, - inner_ty, - is_generic: (is_generic, is_basic_generic), - }) = basic_rust_type(ty, &generics).ok()? - { - if !is_basic { - let struct_const = format!("STRUCT_{}", inner_ty.to_uppercase()); - structs.insert(inner_ty.clone(), struct_const); - } - if is_generic && !is_basic_generic { - let ty = outer_ty.clone(); - let ty = ty.split("<").last().unwrap(); - let ty = ty.replace('>', "").replace(" ", ""); - let struct_const = format!("STRUCT_{}", ty.to_uppercase()); - structs.insert(ty.clone(), struct_const); - } - Some(Ok((pat, outer_ty))) + if let Some(rust_type) = check_rust_type(ty) { + process_rust_type(&rust_type, &mut structs); + + Some(Ok((pat, rust_type))) } else { None } } - syn::FnArg::Receiver(_) => { - Some(Err(s_err(param.span(), "Receiver parameter not allowed"))) - } + syn::FnArg::Receiver(_) => Some(Err(syn::Error::new( + param.span(), + "Receiver parameter not allowed", + ))), }) .collect::>>()?; let response = match &item_fn.sig.output { syn::ReturnType::Type(_, ty) => { - if let Some(RustType { - is_basic, - outer_ty, - inner_ty, - is_generic: (is_generic, is_basic_generic), - }) = basic_rust_type(ty, &generics)? - { - if !is_basic { - let struct_const = format!("STRUCT_{}", inner_ty.to_uppercase()); - structs.insert(inner_ty.clone(), struct_const); - } - if is_generic && !is_basic_generic { - let ty = outer_ty.clone(); - let ty = ty.split("<").last().unwrap(); - let ty = ty.replace('>', "").replace(" ", ""); - let struct_const = format!("STRUCT_{}", ty.to_uppercase()); - structs.insert(ty.clone(), struct_const); - } - outer_ty + if let Some(rust_type) = check_rust_type(ty) { + process_rust_type(&rust_type, &mut structs); + + rust_type } else { - return Err(s_err(ty.span(), "Unsupported return type")); + return Err(syn::Error::new(ty.span(), "Unsupported return type")); } } - syn::ReturnType::Default => "()".to_string(), + syn::ReturnType::Default => RustType2::BuiltIn("()".to_string()), }; - let const_ident = syn::Ident::new( - &format!("FN_{}", fn_name.to_uppercase()), - proc_macro2::Span::call_site(), - ); - - let const_value = params.iter().map(|(pat, ty)| { + let params_types = params.iter().map(|(pat, ty)| { + let ty = ty.to_token_stream().to_string(); quote! { gluer::Field { name: #pat, ty: #ty } } }); - let structs_quote = structs.values().map(|struct_name| { - let struct_ident = syn::Ident::new(struct_name, proc_macro2::Span::call_site()); - quote! { #struct_ident } - }); + let response = { + let ty = response.to_token_stream().to_string(); + quote! { #ty } + }; - let item_fn = quote! { #item_fn }; + let structs_quote = structs + .iter() + .map(|(struct_name, generics_info)| { + generate_struct_metadata(struct_name, generics_info).map_err(|e| e.into()) + }) + .collect::, syn::Error>>()?; - Ok(( - quote! { - #vis const #const_ident: gluer::FnInfo = gluer::FnInfo { - params: &[#(#const_value),*], - response: #response, - structs: &[#(#structs_quote),*] - }; - }, - item_fn, - )) + Ok(quote! { + #[allow(non_camel_case_types, missing_docs)] + #vis struct #fn_name_ident; + + impl #fn_name_ident { + #item_fn + + #vis const fn metadata() -> gluer::FnInfo<'static> { + const FN_INFO: gluer::FnInfo<'static> = gluer::FnInfo { + params: &[#(#params_types),*], + response: #response, + structs: &[#(#structs_quote),*] + }; + FN_INFO + } + } + }) } -struct NoArgs {} - -impl syn::parse::Parse for NoArgs { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - if !input.is_empty() { - return Err(input.error("No arguments expected")); +fn process_rust_type(rust_type: &RustType2, structs: &mut HashMap>) { + match rust_type { + RustType2::Custom(inner_ty) => { + structs.entry(inner_ty.clone()).or_insert_with(Vec::new); } - Ok(NoArgs {}) + RustType2::CustomGeneric(outer_ty, inner_tys) => { + structs + .entry(outer_ty.clone()) + .or_insert_with(Vec::new) + .extend(inner_tys.clone()); + for inner_ty in inner_tys { + process_rust_type(inner_ty, structs); + } + } + RustType2::Generic(_, inner_tys) => { + for inner_ty in inner_tys { + process_rust_type(inner_ty, structs); + } + } + _ => {} } } +fn generate_struct_metadata( + struct_name: &str, + generics_info: &Vec, +) -> syn::Result { + let struct_ident = syn::Ident::new(struct_name, proc_macro2::Span::call_site()); -struct RustType { - is_basic: bool, - outer_ty: String, - inner_ty: String, - is_generic: (bool, bool), + let mut unique_generics = HashSet::new(); + collect_unique_generics(generics_info, &mut unique_generics); + + let generics_placeholder = unique_generics.iter().map(|_| quote! { () }); + + Ok(quote! { + #struct_ident::<#(#generics_placeholder),*>::metadata() + }) } -impl RustType { - fn new(is_basic: bool, outer_ty: String, inner_ty: String, is_generic: (bool, bool)) -> Self { - RustType { - is_basic, - outer_ty, - inner_ty, - is_generic, +fn collect_unique_generics(generics_info: &Vec, unique_generics: &mut HashSet) { + for generic in generics_info { + match generic { + RustType2::BuiltIn(_) => {} + RustType2::Custom(name) | RustType2::CustomGeneric(name, _) => { + unique_generics.insert(name.clone()); + } + RustType2::Generic(_, inner_types) => { + collect_unique_generics(inner_types, unique_generics); + } } } } -/// Returns a tuple (bool, outermost_type, innermost_type, (is_generic, is_basic_generic)) -fn basic_rust_type(ty: &syn::Type, generics: &Vec) -> syn::Result> { - let ty_str = ty.to_token_stream().to_string(); - match ty { - syn::Type::Path(syn::TypePath { path, .. }) => { - if let Some(segment) = path.segments.last() { - let ty_name = segment.ident.to_string(); - - if generics.contains(&ty_name) { - return Ok(Some(RustType::new( - true, - ty_name.clone(), - ty_name, - (true, true), - ))); - } +const RUST_TYPES: &[&str] = &[ + "bool", "char", "str", "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128", + "usize", "isize", "f32", "f64", "String", +]; - // Skip types like State<...> and more, see the `extract` section in axum's docs - if matches!( - ty_name.as_ref(), - "State" | "Headers" | "Bytes" | "Request" | "Extension" - ) { - return Ok(None); - } +const SKIP_TYPES: &[&str] = &["State", "Headers", "Bytes", "Request", "Extension"]; - match &segment.arguments { - syn::PathArguments::None => { - let is_basic = matches!( - ty_name.as_str(), - "bool" - | "char" - | "str" - | "u8" - | "u16" - | "u32" - | "u64" - | "u128" - | "i8" - | "i16" - | "i32" - | "i64" - | "i128" - | "usize" - | "isize" - | "f32" - | "f64" - | "String" - ); - return Ok(Some(RustType::new( - is_basic, - ty_name.clone(), - ty_name, - (false, false), - ))); - } - syn::PathArguments::AngleBracketed(ref args) => { - if matches!( - ty_name.as_str(), - "Query" | "HashMap" | "Path" | "Vec" | "Json" | "Option" | "Result" - ) { - for arg in &args.args { - if let syn::GenericArgument::Type(ref inner_ty) = arg { - if let Ok(Some(RustType { - is_basic, - outer_ty, - inner_ty, - is_generic, - })) = basic_rust_type(inner_ty, generics) - { - return Ok(Some(RustType::new( - is_basic, - format!("{}<{}>", ty_name, outer_ty), - inner_ty, - is_generic, - ))); - } - } - } - } +const BUILTIN_GENERICS: &[&str] = &[ + "Query", "HashMap", "Path", "Vec", "Json", "Option", "Result", +]; - let mut outer_ty = ty_name.clone(); - if let Some(generic_type) = args.args.get(0) { - outer_ty = format!("{}<{}>", outer_ty, generic_type.to_token_stream()); - } +#[derive(Debug, PartialEq, Clone)] +enum RustType2 { + BuiltIn(String), + Generic(String, Vec), + Custom(String), + CustomGeneric(String, Vec), +} - return Ok(Some(RustType::new(false, outer_ty, ty_name, (true, false)))); - } - _ => {} +impl RustType2 { + fn into_tokens(&self) -> TokenStream { + let mut tokens = TokenStream::new(); + self.to_tokens(&mut tokens); + tokens + } +} + +impl ToTokens for RustType2 { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + RustType2::BuiltIn(name) => { + let ty = syn::parse_str::(&name).unwrap(); + tokens.extend(quote! { #ty }); + } + RustType2::Generic(name, inner) => { + let inner = inner.iter().map(|inner| { + let ty = syn::parse_str::(&inner.into_tokens().to_string()).unwrap(); + quote! { #ty } + }); + let ty = syn::parse_str::(&name).unwrap(); + tokens.extend(quote! { #ty<#(#inner),*> }); + } + RustType2::Custom(name) => { + let ty = syn::parse_str::(&name).unwrap(); + tokens.extend(quote! { #ty }); + } + RustType2::CustomGeneric(name, inner) => { + let inner = inner.iter().map(|inner| { + let ty = syn::parse_str::(&inner.into_tokens().to_string()).unwrap(); + quote! { #ty } + }); + let ty = syn::parse_str::(&name).unwrap(); + tokens.extend(quote! { #ty<#(#inner),*> }); + } + } + } +} + +fn is_builtin_type(ident: &syn::Ident) -> bool { + RUST_TYPES.contains(&ident.to_string().as_str()) +} + +fn is_skip_type(ident: &syn::Ident) -> bool { + SKIP_TYPES.contains(&ident.to_string().as_str()) +} + +fn check_rust_type(ty: &syn::Type) -> Option { + match ty { + syn::Type::Path(type_path) => { + let segment = type_path.path.segments.last().unwrap(); + let ident = &segment.ident; + + if is_builtin_type(ident) { + Some(RustType2::BuiltIn(ident.to_string())) + } else if is_skip_type(ident) { + None + } else if BUILTIN_GENERICS.contains(&ident.to_string().as_str()) { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + let inner_types: Vec = args + .args + .iter() + .filter_map(|arg| { + if let syn::GenericArgument::Type(inner_ty) = arg { + check_rust_type(inner_ty) + } else { + None + } + }) + .collect(); + Some(RustType2::Generic(ident.to_string(), inner_types)) + } else { + Some(RustType2::Generic(ident.to_string(), vec![])) } + } else if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + let inner_types: Vec = args + .args + .iter() + .filter_map(|arg| { + if let syn::GenericArgument::Type(inner_ty) = arg { + check_rust_type(inner_ty) + } else { + None + } + }) + .collect(); + Some(RustType2::CustomGeneric(ident.to_string(), inner_types)) + } else { + Some(RustType2::Custom(ident.to_string())) } } syn::Type::Reference(syn::TypeReference { elem, .. }) | syn::Type::Paren(syn::TypeParen { elem, .. }) - | syn::Type::Group(syn::TypeGroup { elem, .. }) => return basic_rust_type(elem, generics), - syn::Type::Tuple(elems) => { - if elems.elems.len() == 1 { - return basic_rust_type(&elems.elems[0], generics); - } else if elems.elems.is_empty() { - return Ok(Some(RustType::new( - true, - "()".to_string(), - "()".to_string(), - (false, false), - ))); + | syn::Type::Group(syn::TypeGroup { elem, .. }) => check_rust_type(&elem), + syn::Type::Slice(type_slice) => { + if let Some(inner) = check_rust_type(&type_slice.elem) { + Some(RustType2::Generic("Vec".to_string(), vec![inner])) + } else { + None } } - syn::Type::Array(syn::TypeArray { elem, .. }) - | syn::Type::Slice(syn::TypeSlice { elem, .. }) => { - if let Some(RustType { - is_basic, - outer_ty, - inner_ty, - is_generic, - }) = basic_rust_type(elem, generics)? - { - let vec_type = format!("Vec<{}>", outer_ty); - return Ok(Some(RustType::new( - is_basic, vec_type, inner_ty, is_generic, - ))); + syn::Type::Tuple(type_tuple) => { + if type_tuple.elems.is_empty() { + return Some(RustType2::BuiltIn("()".to_string())); + } + let inner_types: Vec = type_tuple + .elems + .iter() + .filter_map(|inner_ty| check_rust_type(inner_ty)) + .collect(); + Some(RustType2::Generic("Tuple".to_string(), inner_types)) + } + syn::Type::Array(type_array) => { + if let Some(inner) = check_rust_type(&type_array.elem) { + Some(RustType2::Generic("Vec".to_string(), vec![inner])) } else { - return Ok(None); + None } } - _ => {} + _ => None, } - Err(s_err( - proc_macro2::Span::call_site(), - format!("Failed to parse type {}", ty_str), - )) } diff --git a/src/lib.rs b/src/lib.rs index a2a40ee..91a6296 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -232,14 +232,14 @@ fn generate_ts_function( Json(ty) => Some(format!("data: {}", ty)), Path(ty) => Some(format!("path: {}", ty)), Query(ty) => Some(format!("query: {}", ty)), - QueryMap(ty) => Some(format!("queryMap: Record", ty)), + QueryMap(ty) => Some(format!("queryMap: Record{}", ty)), Unknown(_) => None, }) .collect::>() .join(", "); let body_assignment = if params_str.contains("data") { - "\n body: JSON.stringify(data)" + "\n body: JSON.stringify(data)" } else { "" }; @@ -330,8 +330,8 @@ fn ty_to_ts<'a>( let ty = ty_to_ts(inner_ty, generics, ts_interfaces)?.unwrap(); Path(ty) } - t if t.starts_with("Query { - let inner_ty = &t[14..t.len() - 2]; + t if t.starts_with("Query { + let inner_ty = &t[13..t.len() - 1]; let ty = ty_to_ts(inner_ty, generics, ts_interfaces)?.unwrap(); QueryMap(ty) } @@ -352,22 +352,7 @@ fn ty_to_ts<'a>( } t if t.starts_with("&") => ty_to_ts(&t[1..t.len()], generics, ts_interfaces)?, t if t.starts_with("'static") => ty_to_ts(&t[7..t.len()], generics, ts_interfaces)?, - t if t.contains('<') && t.contains('>') => { - let split: Vec<&str> = t.split('<').collect(); - let base_ty = split[0]; - let generic_params = &split[1][..split[1].len() - 1]; - - let generic_ts = generic_params - .split(',') - .map(|param| ty_to_ts(param, generics, ts_interfaces)) - .collect::, _>>()? - .into_iter() - .map(|t| t.unwrap()) - .collect::>() - .join(", "); - - Unknown(format!("{}<{}>", base_ty, generic_ts)) - } + t if t.contains('<') && t.contains('>') => parse_generic_type(t, generics, ts_interfaces)?, t => { if let Some(t) = generics.iter().find(|p| **p == t) { Unknown(t.to_string()) @@ -378,6 +363,70 @@ fn ty_to_ts<'a>( }) } +fn parse_generic_type<'a>( + t: &'a str, + generics: &[&str], + ts_interfaces: &'a BTreeMap, +) -> Result, String> { + let mut base_ty = String::new(); + let mut generic_params = String::new(); + let mut depth = 0; + let mut inside_generic = false; + + for c in t.chars() { + if c == '<' { + depth += 1; + if depth == 1 { + inside_generic = true; + continue; + } + } else if c == '>' { + depth -= 1; + if depth == 0 { + inside_generic = false; + continue; + } + } + + if inside_generic { + generic_params.push(c); + } else { + base_ty.push(c); + } + } + + let mut params = Vec::new(); + let mut current_param = String::new(); + let mut param_depth = 0; + + for c in generic_params.chars() { + if c == '<' { + param_depth += 1; + } else if c == '>' { + param_depth -= 1; + } else if c == ',' && param_depth == 0 { + params.push(current_param.trim().to_string()); + current_param.clear(); + continue; + } + current_param.push(c); + } + if !current_param.is_empty() { + params.push(current_param.trim().to_string()); + } + + let generic_ts = params + .into_iter() + .map(|param| ty_to_ts(¶m, generics, ts_interfaces)) + .collect::, _>>()? + .into_iter() + .map(|t| t.unwrap()) + .collect::>() + .join(", "); + + Ok(Unknown(format!("{}<{}>", base_ty, generic_ts))) +} + fn write_to_file>( path: P, fetch_api_function: &str, diff --git a/tests/api.ts b/tests/api.ts index c7597e7..bbf2024 100644 --- a/tests/api.ts +++ b/tests/api.ts @@ -1,13 +1,21 @@ namespace api { export interface Age { - age: string; + age: AgeInner; } - export interface Hello { - name: string; + export interface AgeInner { + age: number; + } + + export interface Hello { + name: S; vec: T[]; } + export interface Huh { + huh: T; + } + async function fetchApi(endpoint: string, options: RequestInit): Promise { const response = await fetch(endpoint, { headers: { @@ -19,10 +27,10 @@ namespace api { return response.json(); } - export async function add_root(path: number, data: Hello): Promise { + export async function add_root(path: number, data: Hello, string>, string>): Promise { return fetchApi(`/${encodeURIComponent(path)}`, { method: "POST", - body: JSON.stringify(data) + body: JSON.stringify(data) }); } diff --git a/tests/main.rs b/tests/main.rs index 5f0c47c..877ba21 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -12,28 +12,39 @@ async fn fetch_root(Query(test): Query>, Path(p): Path { - name: String, +pub struct Hello { + name: S, vec: Vec, } #[metadata] #[derive(Serialize, Deserialize, Default)] struct Age { - #[meta(into = String)] + // #[meta(into = String)] age: AgeInner, } +#[metadata] #[derive(Serialize, Deserialize, Default)] struct AgeInner { age: u8, } #[metadata] -async fn add_root(Path(_): Path, Json(hello): Json>) -> Json { +#[derive(Serialize, Deserialize, Default)] +struct Huh { + huh: T, +} + +// Even deep nested generics are supported +#[metadata] +async fn add_root( + Path(_): Path, + Json(hello): Json, String>, String>>, +) -> Json { Json(hello.name.to_string()) }