Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add #[ts(optional)] to struct #366

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 5 additions & 33 deletions macros/src/attr/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use syn::{
TypeSlice, TypeTuple,
};

use super::{parse_assign_from_str, parse_assign_str, Attr, Serde};
use super::{parse_assign_from_str, parse_assign_str, parse_optional, Attr, Optional, Serde};
use crate::utils::{parse_attrs, parse_docs};

#[derive(Default)]
Expand All @@ -21,15 +21,6 @@ pub struct FieldAttr {
pub using_serde_with: bool,
}

/// Indicates whether the field is marked with `#[ts(optional)]`.
/// `#[ts(optional)]` turns an `t: Option<T>` into `t?: T`, while
/// `#[ts(optional = nullable)]` turns it into `t?: T | null`.
#[derive(Default)]
pub struct Optional {
pub optional: bool,
pub nullable: bool,
}

impl FieldAttr {
pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
let mut result = parse_attrs::<Self>(attrs)?;
Expand Down Expand Up @@ -64,10 +55,7 @@ impl Attr for FieldAttr {
rename: self.rename.or(other.rename),
inline: self.inline || other.inline,
skip: self.skip || other.skip,
optional: Optional {
optional: self.optional.optional || other.optional.optional,
nullable: self.optional.nullable || other.optional.nullable,
},
optional: self.optional.or(other.optional),
flatten: self.flatten || other.flatten,

using_serde_with: self.using_serde_with || other.using_serde_with,
Expand Down Expand Up @@ -133,7 +121,7 @@ impl Attr for FieldAttr {
);
}

if self.optional.optional {
if let Optional::Optional { .. } = self.optional {
syn_err_spanned!(
field;
"`optional` is not compatible with `flatten`"
Expand All @@ -156,7 +144,7 @@ impl Attr for FieldAttr {
);
}

if self.optional.optional {
if let Optional::Optional { .. } = self.optional {
syn_err_spanned!(
field;
"`optional` cannot with tuple struct fields"
Expand All @@ -175,23 +163,7 @@ impl_parse! {
"rename" => out.rename = Some(parse_assign_str(input)?),
"inline" => out.inline = true,
"skip" => out.skip = true,
"optional" => {
use syn::{Token, Error};
let nullable = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
let span = input.span();
match Ident::parse(input)?.to_string().as_str() {
"nullable" => true,
_ => Err(Error::new(span, "expected 'nullable'"))?
}
} else {
false
};
out.optional = Optional {
optional: true,
nullable,
}
},
"optional" => out.optional = parse_optional(input)?,
"flatten" => out.flatten = true,
}
}
Expand Down
45 changes: 44 additions & 1 deletion macros/src/attr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub use r#struct::*;
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
Error, Lit, Path, Result, Token, WherePredicate,
Error, Ident, Lit, Path, Result, Token, WherePredicate,
};
pub use variant::*;

Expand All @@ -15,6 +15,34 @@ mod field;
mod r#struct;
mod variant;

/// Indicates whether the field is marked with `#[ts(optional)]`.
/// `#[ts(optional)]` turns an `t: Option<T>` into `t?: T`, while
/// `#[ts(optional = nullable)]` turns it into `t?: T | null`.
#[derive(Default, Clone, Copy)]
pub enum Optional {
Optional {
nullable: bool,
},

#[default]
NotOptional,
}

impl Optional {
pub fn or(self, other: Optional) -> Self {
match (self, other) {
(Self::NotOptional, Self::NotOptional) => Self::NotOptional,

(Self::Optional { nullable }, Self::NotOptional)
| (Self::NotOptional, Self::Optional { nullable }) => Self::Optional { nullable },

(Self::Optional { nullable: a }, Self::Optional { nullable: b }) => {
Self::Optional { nullable: a || b }
}
}
}
}

#[derive(Copy, Clone, Debug)]
pub enum Inflection {
Lower,
Expand Down Expand Up @@ -180,3 +208,18 @@ fn parse_bound(input: ParseStream) -> Result<Vec<WherePredicate>> {
other => Err(Error::new(other.span(), "expected string")),
}
}

fn parse_optional(input: ParseStream) -> Result<Optional> {
let nullable = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
let span = input.span();
match Ident::parse(input)?.to_string().as_str() {
"nullable" => true,
_ => Err(Error::new(span, "expected 'nullable'"))?,
}
} else {
false
};

Ok(Optional::Optional { nullable })
}
19 changes: 17 additions & 2 deletions macros/src/attr/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::collections::HashMap;
use syn::{parse_quote, Attribute, Fields, Ident, Path, Result, Type, WherePredicate};

use super::{
parse_assign_from_str, parse_assign_inflection, parse_bound, parse_concrete, Attr,
ContainerAttr, Serde, Tagged,
parse_assign_from_str, parse_assign_inflection, parse_bound, parse_concrete, parse_optional,
Attr, ContainerAttr, Optional, Serde, Tagged,
};
use crate::{
attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr},
Expand All @@ -24,6 +24,7 @@ pub struct StructAttr {
pub docs: String,
pub concrete: HashMap<Ident, Type>,
pub bound: Option<Vec<WherePredicate>>,
pub optional_fields: Optional,
}

impl StructAttr {
Expand Down Expand Up @@ -90,6 +91,7 @@ impl Attr for StructAttr {
(Some(bound), None) | (None, Some(bound)) => Some(bound),
(None, None) => None,
},
optional_fields: self.optional_fields.or(other.optional_fields),
}
}

Expand All @@ -106,6 +108,10 @@ impl Attr for StructAttr {
if self.tag.is_some() {
syn_err!("`tag` is not compatible with `type`");
}

if let Optional::Optional { .. } = self.optional_fields {
syn_err!("`optional_fields` is not compatible with `type`");
}
}

if self.type_as.is_some() {
Expand All @@ -116,6 +122,10 @@ impl Attr for StructAttr {
if self.rename_all.is_some() {
syn_err!("`rename_all` is not compatible with `as`");
}

if let Optional::Optional { .. } = self.optional_fields {
syn_err!("`optional_fields` is not compatible with `as`");
}
}

if !matches!(item, Fields::Named(_)) {
Expand All @@ -126,6 +136,10 @@ impl Attr for StructAttr {
if self.rename_all.is_some() {
syn_err!("`rename_all` cannot be used with unit or tuple structs");
}

if let Optional::Optional { .. } = self.optional_fields {
syn_err!("`optional_fields` cannot be used with unit or tuple structs");
}
}

Ok(())
Expand All @@ -152,6 +166,7 @@ impl_parse! {
"export_to" => out.export_to = Some(parse_assign_str(input)?),
"concrete" => out.concrete = parse_concrete(input)?,
"bound" => out.bound = Some(parse_bound(input)?),
"optional_fields" => out.optional_fields = parse_optional(input)?,
}
}

Expand Down
2 changes: 2 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ impl DerivedTS {
quote! {
#impl_start {
#assoc_type
type OptionInnerType = Self;

fn ident() -> String {
#ident.to_owned()
Expand Down Expand Up @@ -156,6 +157,7 @@ impl DerivedTS {
}
impl #crate_rename::TS for #generics {
type WithoutGenerics = #generics;
type OptionInnerType = Self;
fn name() -> String { stringify!(#generics).to_owned() }
fn inline() -> String { panic!("{} cannot be inlined", #name) }
fn inline_flattened() -> String { stringify!(#generics).to_owned() }
Expand Down
2 changes: 1 addition & 1 deletion macros/src/types/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
if let Some(attr_type_override) = &enum_attr.type_override {
return type_override::type_override_enum(&enum_attr, &name, attr_type_override);
}

if let Some(attr_type_as) = &enum_attr.type_as {
return type_as::type_as_enum(&enum_attr, &name, attr_type_as);
}
Expand Down
86 changes: 37 additions & 49 deletions macros/src/types/named.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
spanned::Spanned, Field, FieldsNamed, GenericArgument, Path, PathArguments, Result, Type,
};

use crate::{
attr::{Attr, ContainerAttr, FieldAttr, Inflection, Optional, StructAttr},
deps::Dependencies,
utils::{raw_name_to_ts_field, to_ts_ident},
DerivedTS,
};
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{parse_quote, Field, FieldsNamed, Path, Result};

pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Result<DerivedTS> {
let crate_rename = attr.crate_rename();
Expand All @@ -33,6 +31,7 @@ pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Resu
&mut dependencies,
field,
&attr.rename_all,
attr.optional_fields,
)?;
}

Expand Down Expand Up @@ -93,6 +92,7 @@ fn format_field(
dependencies: &mut Dependencies,
field: &Field,
rename_all: &Option<Inflection>,
struct_optional: Optional,
) -> Result<()> {
let field_attr = FieldAttr::from_attrs(&field.attrs)?;

Expand All @@ -102,27 +102,40 @@ fn format_field(
return Ok(());
}

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

let (ty, optional_annotation) = match field_attr.optional {
Optional {
optional: true,
let ty = field_attr.type_as(&field.ty);

let (optional_annotation, nullable) = match (struct_optional, field_attr.optional) {
// `#[ts(optional)]` on field takes precedence, and is enforced **AT COMPILE TIME**
(_, Optional::Optional { nullable }) => (
// expression that evaluates to the string "?", but fails to compile if `ty` is not an `Option`.
quote_spanned! { field.span() => {
fn check_that_field_is_option<T: #crate_rename::IsOption>(_: std::marker::PhantomData<T>) {}
let x: std::marker::PhantomData<#ty> = std::marker::PhantomData;
check_that_field_is_option(x);
"?"
}},
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 {
optional: false, ..
} => (&parsed_ty, ""),
),
// `#[ts(optional)]` on the struct acts as `#[ts(optional)]` on a field, but does not error on non-`Option`
// fields. Instead, it is a no-op.
(Optional::Optional { nullable }, _) => (
quote! {
if <#ty as #crate_rename::TS>::IS_OPTION { "?" } else { "" }
},
nullable,
),
_ => (quote!(""), true),
};

let ty = if nullable {
ty
} else {
parse_quote! {<#ty as #crate_rename::TS>::OptionInnerType}
};

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 @@ -131,10 +144,10 @@ fn format_field(
.map(|t| quote!(#t))
.unwrap_or_else(|| {
if field_attr.inline {
dependencies.append_from(ty);
dependencies.append_from(&ty);
quote!(<#ty as #crate_rename::TS>::inline())
} else {
dependencies.push(ty);
dependencies.push(&ty);
quote!(<#ty as #crate_rename::TS>::name())
}
});
Expand All @@ -159,28 +172,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"),
}
}
6 changes: 6 additions & 0 deletions ts-rs/src/chrono.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ macro_rules! impl_dummy {
($($t:ty),*) => {$(
impl TS for $t {
type WithoutGenerics = $t;
type OptionInnerType = Self;

fn name() -> String { String::new() }
fn inline() -> String { String::new() }
fn inline_flattened() -> String { panic!("{} cannot be flattened", Self::name()) }
Expand All @@ -26,6 +28,8 @@ impl_dummy!(Utc, Local, FixedOffset);

impl<T: TimeZone + 'static> TS for DateTime<T> {
type WithoutGenerics = Self;
type OptionInnerType = Self;

fn ident() -> String {
"string".to_owned()
}
Expand All @@ -48,6 +52,8 @@ impl<T: TimeZone + 'static> TS for DateTime<T> {

impl<T: TimeZone + 'static> TS for Date<T> {
type WithoutGenerics = Self;
type OptionInnerType = Self;

fn ident() -> String {
"string".to_owned()
}
Expand Down
Loading