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

Implement #[ts(optional = nullable)] #213

Merged
merged 2 commits into from
Jan 30, 2024
Merged
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
39 changes: 34 additions & 5 deletions macros/src/attr/field.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
use syn::{Attribute, Ident, Result};
use syn::spanned::Spanned;

use super::parse_assign_str;
use crate::utils::parse_attrs;

use super::parse_assign_str;

#[derive(Default)]
pub struct FieldAttr {
pub type_override: Option<String>,
pub rename: Option<String>,
pub inline: bool,
pub skip: bool,
pub optional: bool,
pub optional: Optional,
pub flatten: 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,
}

#[cfg(feature = "serde-compat")]
#[derive(Default)]
pub struct SerdeFieldAttr(FieldAttr);
Expand All @@ -36,15 +47,18 @@ impl FieldAttr {
rename,
inline,
skip,
optional,
optional: Optional { optional, nullable },
flatten,
}: FieldAttr,
) {
self.rename = self.rename.take().or(rename);
self.type_override = self.type_override.take().or(type_override);
self.inline = self.inline || inline;
self.skip = self.skip || skip;
self.optional |= optional;
self.optional = Optional {
optional: self.optional.optional || optional,
nullable: self.optional.nullable || nullable
};
self.flatten |= flatten;
}
}
Expand All @@ -55,7 +69,22 @@ impl_parse! {
"rename" => out.rename = Some(parse_assign_str(input)?),
"inline" => out.inline = true,
"skip" => out.skip = true,
"optional" => out.optional = true,
"optional" => {
use syn::{Token, Error};
let nullable = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
match Ident::parse(input)?.to_string().as_str() {
"nullable" => true,
other => Err(Error::new(other.span(), "expected 'nullable'"))?
}
} else {
false
};
out.optional = Optional {
optional: true,
nullable,
}
},
"flatten" => out.flatten = true,
}
}
Expand Down
11 changes: 9 additions & 2 deletions macros/src/types/named.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
utils::{raw_name_to_ts_field, to_ts_ident},
DerivedTS,
};
use crate::attr::Optional;

pub(crate) fn named(
attr: &StructAttr,
Expand Down Expand Up @@ -92,8 +93,14 @@ fn format_field(
}

let (ty, optional_annotation) = match optional {
true => (extract_option_argument(&field.ty)?, "?"),
false => (&field.ty, ""),
Optional { optional: true, nullable } => {
let inner_type = extract_option_argument(&field.ty)?; // inner type of the optional
match nullable {
true => (&field.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, .. } => (&field.ty, "")
};

if flatten {
Expand Down
2 changes: 1 addition & 1 deletion macros/src/types/newtype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub(crate) fn newtype(
flatten,
} = FieldAttr::from_attrs(&inner.attrs)?;

match (&rename_inner, skip, optional, flatten) {
match (&rename_inner, skip, optional.optional, flatten) {
(Some(_), ..) => syn_err!("`rename` is not applicable to newtype fields"),
(_, true, ..) => return super::unit::null(attr, name),
(_, _, true, ..) => syn_err!("`optional` is not applicable to newtype fields"),
Expand Down
2 changes: 1 addition & 1 deletion macros/src/types/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fn format_field(
if rename.is_some() {
syn_err!("`rename` is not applicable to tuple structs")
}
if optional {
if optional.optional {
syn_err!("`optional` is not applicable to tuple fields")
}
if flatten {
Expand Down
5 changes: 4 additions & 1 deletion ts-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,10 @@ mod export;
/// Skip this field
///
/// - `#[ts(optional)]`:
/// Indicates the field may be omitted from the serialized struct
/// May be applied on a struct field of type `Option<T>`.
/// By default, such a field would turn into `t: T | null`.
/// If `#[ts(optional)]` is present, `t?: T` is generated instead.
/// If `#[ts(optional = nullable)]` is present, `t?: T | null` is generated.
///
/// - `#[ts(flatten)]`:
/// Flatten this field (only works if the field is a struct)
Expand Down
73 changes: 66 additions & 7 deletions ts-rs/tests/optional_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,73 @@
use serde::Serialize;
use ts_rs::TS;

#[derive(Serialize, TS)]
struct Optional {
#[ts(optional)]
a: Option<i32>,
b: Option<String>,
#[test]
fn in_struct() {
#[derive(Serialize, TS)]
struct Optional {
#[ts(optional)]
a: Option<i32>,
#[ts(optional = nullable)]
b: Option<i32>,
c: Option<i32>,
}

let a = "a?: number";
let b = "b?: number | null";
let c = "c: number | null";
assert_eq!(Optional::inline(), format!("{{ {a}, {b}, {c}, }}"));
}

#[test]
fn test() {
assert_eq!(Optional::inline(), "{ a?: number, b: string | null, }");
fn in_enum() {
#[derive(Serialize, TS)]
enum Optional {
A { #[ts(optional)] a: Option<i32> },
B { b: Option<String>, }
}

assert_eq!(Optional::inline(), r#"{ "A": { a?: number, } } | { "B": { b: string | null, } }"#);
}

#[test]
fn flatten() {
#[derive(Serialize, TS)]
struct Optional {
#[ts(optional)]
a: Option<i32>,
#[ts(optional = nullable)]
b: Option<i32>,
c: Option<i32>,
}

#[derive(Serialize, TS)]
struct Flatten {
#[ts(flatten)]
x: Optional,
}

assert_eq!(Flatten::inline(), Optional::inline());
}

#[test]
fn inline() {
#[derive(Serialize, TS)]
struct Optional {
#[ts(optional)]
a: Option<i32>,
#[ts(optional = nullable)]
b: Option<i32>,
c: Option<i32>,
}

#[derive(Serialize, TS)]
struct Inline {
#[ts(inline)]
x: Optional,
}

let a = "a?: number";
let b = "b?: number | null";
let c = "c: number | null";
assert_eq!(Inline::inline(), format!("{{ x: {{ {a}, {b}, {c}, }}, }}"));
}
Loading