Skip to content

Commit

Permalink
Merge pull request #213 from Aleph-Alpha/optional-nullable
Browse files Browse the repository at this point in the history
Implement `#[ts(optional = nullable)]`
  • Loading branch information
escritorio-gustavo authored Jan 30, 2024
2 parents a38ac4b + 456ec72 commit 5a5b518
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 17 deletions.
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}, }}, }}"));
}

0 comments on commit 5a5b518

Please sign in to comment.