diff --git a/src/typescript.rs b/src/typescript.rs index a0b22c3..1c1dc6b 100644 --- a/src/typescript.rs +++ b/src/typescript.rs @@ -20,94 +20,94 @@ fn convert_generic(gen_ty: &syn::GenericArgument) -> TsType { } } -fn check_cow(cow: &str) -> String { - if !cow.contains("Cow<") { - return cow.to_owned(); - } - - if let Some(comma_pos) = cow - .chars() - .enumerate() - .find(|(_, c)| c == &',') - .map(|(i, _)| i) - { - let cow = cow[comma_pos + 1..].trim(); - if let Some(c) = cow.strip_suffix('>') { - return try_match_ident_str(c); - } - } - - cow.to_owned() -} - -fn try_match_ident_str(ident: &str) -> String { +/// Returns Err(()) when no match is found +fn try_match_ident_str(ident: &str) -> Result { match ident { - "i8" => "number".to_owned(), - "u8" => "number".to_owned(), - "i16" => "number".to_owned(), - "u16" => "number".to_owned(), - "i32" => "number".to_owned(), - "u32" => "number".to_owned(), - "i64" => "number".to_owned(), - "u64" => "number".to_owned(), - "i128" => "number".to_owned(), - "u128" => "number".to_owned(), - "isize" => "number".to_owned(), - "usize" => "number".to_owned(), - "f32" => "number".to_owned(), - "f64" => "number".to_owned(), - "bool" => "boolean".to_owned(), - "char" => "string".to_owned(), - "str" => "string".to_owned(), - "String" => "string".to_owned(), - "NaiveDateTime" => "Date".to_owned(), - "DateTime" => "Date".to_owned(), - "Uuid" => "string".to_owned(), - _ => ident.to_owned(), + "i8" => Ok("number".to_owned()), + "u8" => Ok("number".to_owned()), + "i16" => Ok("number".to_owned()), + "u16" => Ok("number".to_owned()), + "i32" => Ok("number".to_owned()), + "u32" => Ok("number".to_owned()), + "i64" => Ok("number".to_owned()), + "u64" => Ok("number".to_owned()), + "i128" => Ok("number".to_owned()), + "u128" => Ok("number".to_owned()), + "isize" => Ok("number".to_owned()), + "usize" => Ok("number".to_owned()), + "f32" => Ok("number".to_owned()), + "f64" => Ok("number".to_owned()), + "bool" => Ok("boolean".to_owned()), + "char" => Ok("string".to_owned()), + "str" => Ok("string".to_owned()), + "String" => Ok("string".to_owned()), + "NaiveDateTime" => Ok("Date".to_owned()), + "DateTime" => Ok("Date".to_owned()), + "Uuid" => Ok("string".to_owned()), + _ => Err(()), } } -fn try_match_with_args(ident: &str, args: &syn::PathArguments) -> TsType { +/// Returns Err(()) when no match is found +fn try_match_with_args(ident: &str, args: &syn::PathArguments) -> Result { match ident { - "Cow" => { - match &args { - syn::PathArguments::AngleBracketed(angle_bracketed_argument) => { - let Some(arg) = angle_bracketed_argument - .args - .iter() - .find(|arg| matches!(arg, syn::GenericArgument::Type(_))) - else { - return "unknown".to_owned().into(); - }; + "Cow" => Ok(match &args { + syn::PathArguments::AngleBracketed(angle_bracketed_argument) => { + let Some(arg) = angle_bracketed_argument + .args + .iter() + .find(|arg| matches!(arg, syn::GenericArgument::Type(_))) + else { + return Ok("unknown".to_owned().into()); + }; - convert_generic(arg).ts_type.into() - }, - _ => "unknown".to_owned().into(), - } - } - "Option" => { - TsType { - is_optional: true, - ts_type: match &args { - syn::PathArguments::Parenthesized(parenthesized_argument) => { - format!("{:?}", parenthesized_argument) - } - syn::PathArguments::AngleBracketed(angle_bracketed_argument) => { - convert_generic(angle_bracketed_argument.args.first().unwrap()).ts_type - } - _ => "unknown".to_owned(), - }, + convert_generic(arg).ts_type.into() } - } - "Vec" => { - match &args { + _ => "unknown".to_owned().into(), + }), + "Option" => Ok(TsType { + is_optional: true, + ts_type: match &args { syn::PathArguments::Parenthesized(parenthesized_argument) => { - format!("{:?}", parenthesized_argument).into() + format!("{:?}", parenthesized_argument) } syn::PathArguments::AngleBracketed(angle_bracketed_argument) => { - format!( - "Array<{}>", - match convert_generic(angle_bracketed_argument.args.first().unwrap()) { + convert_generic(angle_bracketed_argument.args.first().unwrap()).ts_type + } + _ => "unknown".to_owned(), + }, + }), + "Vec" => Ok(match &args { + syn::PathArguments::Parenthesized(parenthesized_argument) => { + format!("{:?}", parenthesized_argument).into() + } + syn::PathArguments::AngleBracketed(angle_bracketed_argument) => format!( + "Array<{}>", + match convert_generic(angle_bracketed_argument.args.first().unwrap()) { + TsType { + is_optional: true, + ts_type, + } => format!("{} | undefined", ts_type), + TsType { + is_optional: false, + ts_type, + } => ts_type, + } + ) + .into(), + _ => "unknown".to_owned().into(), + }), + "HashMap" => Ok(match &args { + syn::PathArguments::Parenthesized(parenthesized_argument) => { + format!("{:?}", parenthesized_argument).into() + } + syn::PathArguments::AngleBracketed(angle_bracketed_argument) => format!( + "Record<{}>", + angle_bracketed_argument + .args + .iter() + .map(|arg| { + match convert_generic(arg) { TsType { is_optional: true, ts_type, @@ -117,49 +117,48 @@ fn try_match_with_args(ident: &str, args: &syn::PathArguments) -> TsType { ts_type, } => ts_type, } - ) - .into() - } - _ => "unknown".to_owned().into(), - } + }) + .collect::>() + .join(", ") + ) + .into(), + _ => "unknown".to_owned().into(), + }), + _ => Err(()), + } +} + +pub fn extract_custom_type(segment: &syn::PathSegment) -> Result { + let ident = segment.ident.to_string(); + let args = &segment.arguments; + + match args { + syn::PathArguments::None => Ok(ident.into()), + syn::PathArguments::AngleBracketed(angle_bracketed_argument) => { + let args = angle_bracketed_argument + .args + .iter() + .map(|arg| match convert_generic(arg) { + TsType { + is_optional: true, + ts_type, + } => format!("{} | undefined", ts_type), + TsType { + is_optional: false, + ts_type, + } => ts_type, + }) + .collect::>() + .join(", "); + + Ok(format!("{}<{}>", ident, args).into()) } - "HashMap" => { - match &args { - syn::PathArguments::Parenthesized(parenthesized_argument) => { - format!("{:?}", parenthesized_argument).into() - } - syn::PathArguments::AngleBracketed(angle_bracketed_argument) => { - format!( - "Record<{}>", - angle_bracketed_argument - .args - .iter() - .map(|arg| { - match convert_generic(arg) { - TsType { - is_optional: true, - ts_type, - } => format!("{} | undefined", ts_type), - TsType { - is_optional: false, - ts_type, - } => ts_type, - } - }) - .collect::>() - .join(", ") - ) - .into() - } - _ => "unknown".to_owned().into(), - } + syn::PathArguments::Parenthesized(parenthesized_argument) => { + Err(()) // tuples are not supported yet } - _ => ident.to_owned().into(), } } -const COMPLEX_TYPES: [&str; 4usize] = ["Option", "Vec", "HashMap", "Cow"]; - pub fn convert_type(ty: &syn::Type) -> TsType { match ty { syn::Type::Reference(p) => convert_type(&p.elem), @@ -167,10 +166,14 @@ pub fn convert_type(ty: &syn::Type) -> TsType { let segment = p.path.segments.last().unwrap(); let identifier = segment.ident.to_string(); - if COMPLEX_TYPES.contains(&identifier.as_str()) { - try_match_with_args(&identifier, &segment.arguments) + if let Ok(ts_type) = try_match_ident_str(&identifier) { + ts_type.into() + } else if let Ok(ts_type) = try_match_with_args(&identifier, &segment.arguments) { + ts_type.into() + } else if let Ok(ts_type) = extract_custom_type(&segment) { + ts_type.into() } else { - try_match_ident_str(&identifier).into() + "unknown".to_owned().into() } } _ => "unknown".to_owned().into(), diff --git a/src/utils.rs b/src/utils.rs index 521b7bc..7bd33a9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -52,7 +52,9 @@ fn check_token(token: proc_macro2::TokenTree, arg: &str) -> Option { // this detects the '(...)' part in #[serde(rename_all = "UPPERCASE", tag = "type")] // we can use this to get the value of a particular argument // or to see if it exists at all - let proc_macro2::TokenTree::Group(group) = token else { return None; }; + let proc_macro2::TokenTree::Group(group) = token else { + return None; + }; // Make sure the delimiter is what we're expecting, otherwise return right away. if group.delimiter() != proc_macro2::Delimiter::Parenthesis { @@ -60,7 +62,10 @@ fn check_token(token: proc_macro2::TokenTree, arg: &str) -> Option { } // First check to see if the group is a `MetaNameValue`, (.e.g `feature = "nightly"`) - match Parser::parse2(Punctuated::::parse_terminated, group.stream()) { + match Parser::parse2( + Punctuated::::parse_terminated, + group.stream(), + ) { Ok(name_value_pairs) => { // If so move the pairs into an iterator name_value_pairs @@ -76,19 +81,22 @@ fn check_token(token: proc_macro2::TokenTree, arg: &str) -> Option { // Otherwise, check to see if the group is a `Expr` of `Punctuated<_, P>` attributes, // separated by `P`, `Token![,]` in this case. // (.e.g `default, skip_serializing`) - Parser::parse2(Punctuated::::parse_terminated, group.stream()) - // If the expression cannot be parsed, return None - .map_or(None, |comma_seperated_values| { - // Otherwise move the pairs into an iterator - comma_seperated_values - .into_iter() - // Checking each is a `ExprPath`, object, yielding elements while the method - // returns true. - .map_while(check_expression_is_path) - // Check if any yielded paths equal `arg` - .any(|expr_path| expr_path.path.segments[0].ident.to_string().eq(arg)) - // If so, return `Some(arg)`, otherwise `None`. - .then_some(arg.to_owned()) + Parser::parse2( + Punctuated::::parse_terminated, + group.stream(), + ) + // If the expression cannot be parsed, return None + .map_or(None, |comma_seperated_values| { + // Otherwise move the pairs into an iterator + comma_seperated_values + .into_iter() + // Checking each is a `ExprPath`, object, yielding elements while the method + // returns true. + .map_while(check_expression_is_path) + // Check if any yielded paths equal `arg` + .any(|expr_path| expr_path.path.segments[0].ident.to_string().eq(arg)) + // If so, return `Some(arg)`, otherwise `None`. + .then_some(arg.to_owned()) }) } } @@ -123,7 +131,9 @@ pub fn has_attribute_arg(needle: &str, arg: &str, attributes: &[syn::Attribute]) /// Given an attribute like `#[doc = "Single line doc comments"]`, only `Single line doc comments` /// should be returned. fn check_doc_tokens(tt: proc_macro2::TokenTree) -> Option { - let proc_macro2::TokenTree::Literal(comment) = tt else { return None; }; + let proc_macro2::TokenTree::Literal(comment) = tt else { + return None; + }; let c = comment.to_string(); Some(c[1..c.len() - 1].trim().to_owned()) } @@ -135,7 +145,9 @@ fn check_doc_tokens(tt: proc_macro2::TokenTree) -> Option { fn check_doc_attribute(attr: &syn::Attribute) -> Vec { // Check if the attribute's meta is a NameValue, otherwise return // right away. - let syn::Meta::NameValue(ref nv) = attr.meta else { return Default::default(); }; + let syn::Meta::NameValue(ref nv) = attr.meta else { + return Default::default(); + }; // Convert the value to a token stream, then iterate it, collecting // only valid comment string. @@ -152,12 +164,7 @@ fn check_doc_attribute(attr: &syn::Attribute) -> Vec { pub fn get_comments(mut attributes: Vec) -> Vec { // Retains only attributes that have segments equal to "doc". // (.e.g. #[doc = "Single line doc comments"]) - attributes.retain(|x| { - x.path() - .segments - .iter() - .any(|seg| seg.ident == "doc") - }); + attributes.retain(|x| x.path().segments.iter().any(|seg| seg.ident == "doc")); attributes .iter() @@ -222,7 +229,7 @@ pub fn get_attribute<'a>( .segments .iter() .any(|segment| segment.ident == needle) - }) + }) } pub(crate) fn parse_serde_case(val: impl Into>) -> Option { diff --git a/test/generic/rust.rs b/test/generic/rust.rs new file mode 100644 index 0000000..111dbbe --- /dev/null +++ b/test/generic/rust.rs @@ -0,0 +1,19 @@ +use tsync::tsync; + +/* + * This test was introduced because of a bug where the "Paginated" type would + * be converted to "Paginated" without the generic type. + */ + +#[tsync] +struct Folder { + name: String, + children: Paginated, +} + +#[tsync] +struct Paginated { + data: Vec, + page: u32, + total_pages: u32, +} diff --git a/test/generic/tsync.sh b/test/generic/tsync.sh new file mode 100755 index 0000000..a89f293 --- /dev/null +++ b/test/generic/tsync.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +cd $SCRIPT_DIR + +cargo run -- -i rust.rs -o typescript.d.ts +cargo run -- -i rust.rs -o typescript.ts diff --git a/test/generic/typescript.d.ts b/test/generic/typescript.d.ts new file mode 100644 index 0000000..6f40c65 --- /dev/null +++ b/test/generic/typescript.d.ts @@ -0,0 +1,12 @@ +/* This file is generated and managed by tsync */ + +interface Folder { + name: string; + children: Paginated; +} + +interface Paginated { + data: Array; + page: number; + total_pages: number; +} diff --git a/test/generic/typescript.ts b/test/generic/typescript.ts new file mode 100644 index 0000000..adba890 --- /dev/null +++ b/test/generic/typescript.ts @@ -0,0 +1,12 @@ +/* This file is generated and managed by tsync */ + +export interface Folder { + name: string; + children: Paginated; +} + +export interface Paginated { + data: Array; + page: number; + total_pages: number; +} diff --git a/test/test_all.sh b/test/test_all.sh index ca3f5bd..80efb95 100755 --- a/test/test_all.sh +++ b/test/test_all.sh @@ -10,3 +10,4 @@ cd $SCRIPT_DIR ./const/tsync.sh ./enum/tsync.sh ./doc_comments/tsync.sh +./generic/tsync.sh