Skip to content

Commit

Permalink
Add runtime validation for #[ts(optional)]
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavo-shigueo committed Nov 10, 2024
1 parent 0aa63ea commit 69e9987
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 65 deletions.
116 changes: 52 additions & 64 deletions macros/src/types/named.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
spanned::Spanned, Field, FieldsNamed, GenericArgument, Path, PathArguments, Result, Type,
};
use syn::{Field, FieldsNamed, Path, Result};

use crate::{
attr::{Attr, ContainerAttr, FieldAttr, Inflection, Optional, StructAttr},
Expand Down Expand Up @@ -104,53 +102,47 @@ fn format_field(
return Ok(());
}

let parsed_ty = field_attr.type_as(&field.ty);
let ty = field_attr.type_as(&field.ty);

let (ty, optional_annotation) = match (struct_optional, field_attr.optional) {
let opt = match (struct_optional, field_attr.optional) {
(
Optional {
optional: true,
nullable,
},
opt @ Optional { optional: true, .. },
Optional {
optional: false, ..
},
) => match extract_option_argument(&parsed_ty) {
Ok(inner_type) => {
if nullable {
(&parsed_ty, "?")
} else {
(inner_type, "?")
}
}
Err(_) => (&parsed_ty, ""),
},
) => opt,
(_, opt @ Optional { optional: true, .. }) => opt,
(
_,
Optional {
optional: true,
nullable,
},
) => {
let inner_type = extract_option_argument(&parsed_ty)?; // inner type of the optional
match nullable {
true => (&parsed_ty, "?"), // if it's nullable, we keep the original type
false => (inner_type, "?"), // if not, we use the Option's inner type
}
}
(
Optional {
opt @ Optional {
optional: false, ..
},
Optional {
optional: false, ..
},
) => (&parsed_ty, ""),
) => opt,
};

let optional_annotation = if opt.optional {
quote! { if <#ty as #crate_rename::TS>::IS_OPTION { "?" } else { "" } }
} else {
quote! { "" }
};

let optional_annotation = if field_attr.optional.optional {
quote! {
if <#ty as #crate_rename::TS>::IS_OPTION {
#optional_annotation
} else {
panic!("`#[ts(optional)]` can only be used with the Option type")
}
}
} else {
optional_annotation
};

if field_attr.flatten {
flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened()));
dependencies.append_from(ty);
dependencies.append_from(&ty);
return Ok(());
}

Expand All @@ -159,11 +151,32 @@ fn format_field(
.map(|t| quote!(#t))
.unwrap_or_else(|| {
if field_attr.inline {
dependencies.append_from(ty);
quote!(<#ty as #crate_rename::TS>::inline())
dependencies.append_from(&ty);

if opt.optional && !opt.nullable {
quote! {
if <#ty as #crate_rename::TS>::IS_OPTION {
<#ty as #crate_rename::TS>::option_inner_inline().unwrap()
} else {
<#ty as #crate_rename::TS>::inline()
}
}
} else {
quote!(<#ty as #crate_rename::TS>::inline())
}
} else {
dependencies.push(ty);
quote!(<#ty as #crate_rename::TS>::name())
dependencies.push(&ty);
if opt.optional && !opt.nullable {
quote! {
if <#ty as #crate_rename::TS>::IS_OPTION {
<#ty as #crate_rename::TS>::option_inner_name().unwrap()
} else {
<#ty as #crate_rename::TS>::name()
}
}
} else {
quote!(<#ty as #crate_rename::TS>::name())
}
}
});

Expand All @@ -187,28 +200,3 @@ fn format_field(

Ok(())
}

fn extract_option_argument(ty: &Type) -> Result<&Type> {
match ty {
Type::Path(type_path)
if type_path.qself.is_none()
&& type_path.path.leading_colon.is_none()
&& type_path.path.segments.len() == 1
&& type_path.path.segments[0].ident == "Option" =>
{
let segment = &type_path.path.segments[0];
match &segment.arguments {
PathArguments::AngleBracketed(args) if args.args.len() == 1 => {
match &args.args[0] {
GenericArgument::Type(inner_ty) => Ok(inner_ty),
other => syn_err!(other.span(); "`Option` argument must be a type"),
}
}
other => {
syn_err!(other.span(); "`Option` type must have a single generic argument")
}
}
}
other => syn_err!(other.span(); "`optional` can only be used on an Option<T> type"),
}
}
22 changes: 22 additions & 0 deletions ts-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,19 @@ pub trait TS {
/// automatically read from your doc comments or `#[doc = ".."]` attributes
const DOCS: Option<&'static str> = None;

#[doc(hidden)]
const IS_OPTION: bool = false;

#[doc(hidden)]
fn option_inner_name() -> Option<String> {
None
}

#[doc(hidden)]
fn option_inner_inline() -> Option<String> {
None
}

/// Identifier of this type, excluding generic parameters.
fn ident() -> String {
// by default, fall back to `TS::name()`.
Expand Down Expand Up @@ -722,6 +735,7 @@ macro_rules! impl_shadow {

impl<T: TS> TS for Option<T> {
type WithoutGenerics = Self;
const IS_OPTION: bool = true;

fn name() -> String {
format!("{} | null", T::name())
Expand All @@ -731,6 +745,14 @@ impl<T: TS> TS for Option<T> {
format!("{} | null", T::inline())
}

fn option_inner_name() -> Option<String> {
Some(T::name())
}

fn option_inner_inline() -> Option<String> {
Some(T::inline())
}

fn visit_dependencies(v: &mut impl TypeVisitor)
where
Self: 'static,
Expand Down
11 changes: 10 additions & 1 deletion ts-rs/tests/integration/optional_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ fn inline() {
assert_eq!(Inline::inline(), format!("{{ x: {{ {a}, {b}, {c}, }}, }}"));
}

type Foo = Option<i32>;
type Bar<T> = Option<T>;

#[derive(TS)]
#[ts(export, export_to = "optional_field/", optional)]
struct OptionalStruct {
Expand All @@ -97,13 +100,19 @@ struct OptionalStruct {
#[ts(optional = nullable)]
c: Option<i32>,

#[ts(optional = nullable)]
d: i32,

e: Foo,
f: Bar<i32>,
}

#[test]
fn struct_optional() {
assert_eq!(
OptionalStruct::inline(),
format!("{{ a?: number, b?: number, c?: number | null, d: number, }}")
format!(
"{{ a?: number, b?: number, c?: number | null, d: number, e?: number, f?: number, }}"
)
)
}

0 comments on commit 69e9987

Please sign in to comment.