Skip to content

Commit

Permalink
Rename struct level attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavo-shigueo committed Nov 11, 2024
1 parent c7d2c4a commit 033d096
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 178 deletions.
6 changes: 3 additions & 3 deletions macros/src/attr/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub struct StructAttr {
pub docs: String,
pub concrete: HashMap<Ident, Type>,
pub bound: Option<Vec<WherePredicate>>,
pub optional: Optional,
pub optional_fields: Optional,
}

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

Expand Down Expand Up @@ -154,7 +154,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" => out.optional = parse_optional(input)?,
"optional_fields" => out.optional_fields = parse_optional(input)?,
}
}

Expand Down
348 changes: 174 additions & 174 deletions macros/src/types/named.rs
Original file line number Diff line number Diff line change
@@ -1,174 +1,174 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_quote, Field, FieldsNamed, Path, Result};
use syn::spanned::Spanned;
use crate::{
attr::{Attr, ContainerAttr, FieldAttr, Inflection, Optional, StructAttr},
deps::Dependencies,
utils::{raw_name_to_ts_field, to_ts_ident},
DerivedTS,
};

pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Result<DerivedTS> {
let crate_rename = attr.crate_rename();

let mut formatted_fields = Vec::new();
let mut flattened_fields = Vec::new();
let mut dependencies = Dependencies::new(crate_rename.clone());

if let Some(tag) = &attr.tag {
let formatted = format!("\"{}\": \"{}\",", tag, name);
formatted_fields.push(quote! {
#formatted.to_string()
});
}

for field in &fields.named {
format_field(
&crate_rename,
&mut formatted_fields,
&mut flattened_fields,
&mut dependencies,
field,
&attr.rename_all,
attr.optional,
)?;
}

let fields = quote!(<[String]>::join(&[#(#formatted_fields),*], " "));
let flattened = quote!(<[String]>::join(&[#(#flattened_fields),*], " & "));

let inline = match (formatted_fields.len(), flattened_fields.len()) {
(0, 0) => quote!("{ }".to_owned()),
(_, 0) => quote!(format!("{{ {} }}", #fields)),
(0, 1) => quote! {{
if #flattened.starts_with('(') && #flattened.ends_with(')') {
#flattened[1..#flattened.len() - 1].trim().to_owned()
} else {
#flattened.trim().to_owned()
}
}},
(0, _) => quote!(#flattened),
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
};

let inline_flattened = match (formatted_fields.len(), flattened_fields.len()) {
(0, 0) => quote!("{ }".to_owned()),
(_, 0) => quote!(format!("{{ {} }}", #fields)),
(0, _) => quote!(#flattened),
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
};

Ok(DerivedTS {
crate_rename,
// the `replace` combines `{ ... } & { ... }` into just one `{ ... }`. Not necessary, but it
// results in simpler type definitions.
inline: quote!(#inline.replace(" } & { ", " ")),
inline_flattened: Some(quote!(#inline_flattened.replace(" } & { ", " "))),
docs: attr.docs.clone(),
dependencies,
export: attr.export,
export_to: attr.export_to.clone(),
ts_name: name.to_owned(),
concrete: attr.concrete.clone(),
bound: attr.bound.clone(),
})
}

// build an expression which expands to a string, representing a single field of a struct.
//
// formatted_fields will contain all the fields that do not contain the flatten
// attribute, in the format
// key: type,
//
// flattened_fields will contain all the fields that contain the flatten attribute
// in their respective formats, which for a named struct is the same as formatted_fields,
// but for enums is
// ({ /* variant data */ } | { /* variant data */ })
fn format_field(
crate_rename: &Path,
formatted_fields: &mut Vec<TokenStream>,
flattened_fields: &mut Vec<TokenStream>,
dependencies: &mut Dependencies,
field: &Field,
rename_all: &Option<Inflection>,
struct_optional: Optional,
) -> Result<()> {
let field_attr = FieldAttr::from_attrs(&field.attrs)?;

field_attr.assert_validity(field)?;

if field_attr.skip {
return Ok(());
}

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,
),
// `#[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);
return Ok(());
}

let formatted_ty = field_attr
.type_override
.map(|t| quote!(#t))
.unwrap_or_else(|| {
if field_attr.inline {
dependencies.append_from(&ty);
quote!(<#ty as #crate_rename::TS>::inline())
} else {
dependencies.push(&ty);
quote!(<#ty as #crate_rename::TS>::name())
}
});

let field_name = to_ts_ident(field.ident.as_ref().unwrap());
let name = match (field_attr.rename, rename_all) {
(Some(rn), _) => rn,
(None, Some(rn)) => rn.apply(&field_name),
(None, None) => field_name,
};
let valid_name = raw_name_to_ts_field(name);

// Start every doc string with a newline, because when other characters are in front, it is not "understood" by VSCode
let docs = match field_attr.docs.is_empty() {
true => "".to_string(),
false => format!("\n{}", &field_attr.docs),
};

formatted_fields.push(quote! {
format!("{}{}{}: {},", #docs, #valid_name, #optional_annotation, #formatted_ty)
});

Ok(())
}
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();

let mut formatted_fields = Vec::new();
let mut flattened_fields = Vec::new();
let mut dependencies = Dependencies::new(crate_rename.clone());

if let Some(tag) = &attr.tag {
let formatted = format!("\"{}\": \"{}\",", tag, name);
formatted_fields.push(quote! {
#formatted.to_string()
});
}

for field in &fields.named {
format_field(
&crate_rename,
&mut formatted_fields,
&mut flattened_fields,
&mut dependencies,
field,
&attr.rename_all,
attr.optional_fields,
)?;
}

let fields = quote!(<[String]>::join(&[#(#formatted_fields),*], " "));
let flattened = quote!(<[String]>::join(&[#(#flattened_fields),*], " & "));

let inline = match (formatted_fields.len(), flattened_fields.len()) {
(0, 0) => quote!("{ }".to_owned()),
(_, 0) => quote!(format!("{{ {} }}", #fields)),
(0, 1) => quote! {{
if #flattened.starts_with('(') && #flattened.ends_with(')') {
#flattened[1..#flattened.len() - 1].trim().to_owned()
} else {
#flattened.trim().to_owned()
}
}},
(0, _) => quote!(#flattened),
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
};

let inline_flattened = match (formatted_fields.len(), flattened_fields.len()) {
(0, 0) => quote!("{ }".to_owned()),
(_, 0) => quote!(format!("{{ {} }}", #fields)),
(0, _) => quote!(#flattened),
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
};

Ok(DerivedTS {
crate_rename,
// the `replace` combines `{ ... } & { ... }` into just one `{ ... }`. Not necessary, but it
// results in simpler type definitions.
inline: quote!(#inline.replace(" } & { ", " ")),
inline_flattened: Some(quote!(#inline_flattened.replace(" } & { ", " "))),
docs: attr.docs.clone(),
dependencies,
export: attr.export,
export_to: attr.export_to.clone(),
ts_name: name.to_owned(),
concrete: attr.concrete.clone(),
bound: attr.bound.clone(),
})
}

// build an expression which expands to a string, representing a single field of a struct.
//
// formatted_fields will contain all the fields that do not contain the flatten
// attribute, in the format
// key: type,
//
// flattened_fields will contain all the fields that contain the flatten attribute
// in their respective formats, which for a named struct is the same as formatted_fields,
// but for enums is
// ({ /* variant data */ } | { /* variant data */ })
fn format_field(
crate_rename: &Path,
formatted_fields: &mut Vec<TokenStream>,
flattened_fields: &mut Vec<TokenStream>,
dependencies: &mut Dependencies,
field: &Field,
rename_all: &Option<Inflection>,
struct_optional: Optional,
) -> Result<()> {
let field_attr = FieldAttr::from_attrs(&field.attrs)?;

field_attr.assert_validity(field)?;

if field_attr.skip {
return Ok(());
}

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,
),
// `#[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);
return Ok(());
}

let formatted_ty = field_attr
.type_override
.map(|t| quote!(#t))
.unwrap_or_else(|| {
if field_attr.inline {
dependencies.append_from(&ty);
quote!(<#ty as #crate_rename::TS>::inline())
} else {
dependencies.push(&ty);
quote!(<#ty as #crate_rename::TS>::name())
}
});

let field_name = to_ts_ident(field.ident.as_ref().unwrap());
let name = match (field_attr.rename, rename_all) {
(Some(rn), _) => rn,
(None, Some(rn)) => rn.apply(&field_name),
(None, None) => field_name,
};
let valid_name = raw_name_to_ts_field(name);

// Start every doc string with a newline, because when other characters are in front, it is not "understood" by VSCode
let docs = match field_attr.docs.is_empty() {
true => "".to_string(),
false => format!("\n{}", &field_attr.docs),
};

formatted_fields.push(quote! {
format!("{}{}{}: {},", #docs, #valid_name, #optional_annotation, #formatted_ty)
});

Ok(())
}
2 changes: 1 addition & 1 deletion ts-rs/tests/integration/optional_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ type Foo = Option<i32>;
type Bar<T> = Option<T>;

#[derive(TS)]
#[ts(export, export_to = "optional_field/", optional)]
#[ts(export, export_to = "optional_field/", optional_fields)]
struct OptionalStruct {
a: Option<i32>,
b: Option<i32>,
Expand Down

0 comments on commit 033d096

Please sign in to comment.