Skip to content

Commit

Permalink
Add support for CustomType<WithGenerics>
Browse files Browse the repository at this point in the history
  • Loading branch information
Wulf committed Jan 4, 2024
1 parent 9f5b950 commit fbd9fad
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 143 deletions.
241 changes: 122 additions & 119 deletions src/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, ()> {
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<TsType, ()> {
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,
Expand All @@ -117,60 +117,63 @@ fn try_match_with_args(ident: &str, args: &syn::PathArguments) -> TsType {
ts_type,
} => ts_type,
}
)
.into()
}
_ => "unknown".to_owned().into(),
}
})
.collect::<Vec<String>>()
.join(", ")
)
.into(),
_ => "unknown".to_owned().into(),
}),
_ => Err(()),
}
}

pub fn extract_custom_type(segment: &syn::PathSegment) -> Result<TsType, ()> {
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::<Vec<String>>()
.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::<Vec<String>>()
.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),
syn::Type::Path(p) => {
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(),
Expand Down
55 changes: 31 additions & 24 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,20 @@ fn check_token(token: proc_macro2::TokenTree, arg: &str) -> Option<String> {
// 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 {
return None;
}

// First check to see if the group is a `MetaNameValue`, (.e.g `feature = "nightly"`)
match Parser::parse2(Punctuated::<MetaNameValue, Token![,]>::parse_terminated, group.stream()) {
match Parser::parse2(
Punctuated::<MetaNameValue, Token![,]>::parse_terminated,
group.stream(),
) {
Ok(name_value_pairs) => {
// If so move the pairs into an iterator
name_value_pairs
Expand All @@ -76,19 +81,22 @@ fn check_token(token: proc_macro2::TokenTree, arg: &str) -> Option<String> {
// 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::<Expr, Token![,]>::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::<Expr, Token![,]>::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())
})
}
}
Expand Down Expand Up @@ -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<String> {
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())
}
Expand All @@ -135,7 +145,9 @@ fn check_doc_tokens(tt: proc_macro2::TokenTree) -> Option<String> {
fn check_doc_attribute(attr: &syn::Attribute) -> Vec<String> {
// 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.
Expand All @@ -152,12 +164,7 @@ fn check_doc_attribute(attr: &syn::Attribute) -> Vec<String> {
pub fn get_comments(mut attributes: Vec<syn::Attribute>) -> Vec<String> {
// 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()
Expand Down Expand Up @@ -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<String>>) -> Option<convert_case::Case> {
Expand Down
Loading

0 comments on commit fbd9fad

Please sign in to comment.