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 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8e8fc64
Fix clippy warning
gustavo-shigueo Nov 9, 2024
6692151
Add test
gustavo-shigueo Nov 9, 2024
2b7a95a
Parse #[ts(optional)] on structs
gustavo-shigueo Nov 9, 2024
1a13390
Add non Option<T> field to test
gustavo-shigueo Nov 10, 2024
0aa63ea
Fix struct optional non-optional field
gustavo-shigueo Nov 10, 2024
69e9987
Add runtime validation for `#[ts(optional)]`
gustavo-shigueo Nov 10, 2024
a562561
Turn Optional into an enum
gustavo-shigueo Nov 10, 2024
6f064f5
Remove invalid optional attribute from test
gustavo-shigueo Nov 10, 2024
ad4f024
Remove extra methods for option inner type
gustavo-shigueo Nov 10, 2024
ade6a2f
Remove panic
gustavo-shigueo Nov 10, 2024
dcc7a46
compile-time checked `#[ts(optional)]` (#367)
NyxCode Nov 11, 2024
95e0401
Remove invalid optional from test
gustavo-shigueo Nov 11, 2024
4d1a6d4
Fix doc comment
gustavo-shigueo Nov 11, 2024
0d51986
Show error message on the compiler's ascii graphic too
gustavo-shigueo Nov 11, 2024
c7d2c4a
Remove invalid optional from test - again, oops
gustavo-shigueo Nov 11, 2024
033d096
Rename struct level attribute
gustavo-shigueo Nov 11, 2024
f72238f
Validate `optional_fields` compatibility with other attributes
gustavo-shigueo Nov 11, 2024
21d03f5
Add extra test for `optional_fields = nullable`
gustavo-shigueo Dec 1, 2024
faa431f
Add docs
gustavo-shigueo Dec 1, 2024
53941b5
#[ts(type)] overrides #[ts(optional_fields)]
gustavo-shigueo Dec 2, 2024
3d0a048
Make #[ts(type)] and #[ts(optional)] incompatible
gustavo-shigueo Dec 2, 2024
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
45 changes: 12 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 @@ -109,6 +97,13 @@ impl Attr for FieldAttr {
"`type` is not compatible with `flatten`"
);
}

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

if self.flatten {
Expand All @@ -133,7 +128,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 +151,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 +170,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
Loading