Skip to content

Commit 033d096

Browse files
Rename struct level attribute
1 parent c7d2c4a commit 033d096

File tree

3 files changed

+178
-178
lines changed

3 files changed

+178
-178
lines changed

macros/src/attr/struct.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub struct StructAttr {
2424
pub docs: String,
2525
pub concrete: HashMap<Ident, Type>,
2626
pub bound: Option<Vec<WherePredicate>>,
27-
pub optional: Optional,
27+
pub optional_fields: Optional,
2828
}
2929

3030
impl StructAttr {
@@ -91,7 +91,7 @@ impl Attr for StructAttr {
9191
(Some(bound), None) | (None, Some(bound)) => Some(bound),
9292
(None, None) => None,
9393
},
94-
optional: self.optional.or(other.optional),
94+
optional_fields: self.optional_fields.or(other.optional_fields),
9595
}
9696
}
9797

@@ -154,7 +154,7 @@ impl_parse! {
154154
"export_to" => out.export_to = Some(parse_assign_str(input)?),
155155
"concrete" => out.concrete = parse_concrete(input)?,
156156
"bound" => out.bound = Some(parse_bound(input)?),
157-
"optional" => out.optional = parse_optional(input)?,
157+
"optional_fields" => out.optional_fields = parse_optional(input)?,
158158
}
159159
}
160160

macros/src/types/named.rs

+174-174
Original file line numberDiff line numberDiff line change
@@ -1,174 +1,174 @@
1-
use proc_macro2::TokenStream;
2-
use quote::{quote, quote_spanned};
3-
use syn::{parse_quote, Field, FieldsNamed, Path, Result};
4-
use syn::spanned::Spanned;
5-
use crate::{
6-
attr::{Attr, ContainerAttr, FieldAttr, Inflection, Optional, StructAttr},
7-
deps::Dependencies,
8-
utils::{raw_name_to_ts_field, to_ts_ident},
9-
DerivedTS,
10-
};
11-
12-
pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Result<DerivedTS> {
13-
let crate_rename = attr.crate_rename();
14-
15-
let mut formatted_fields = Vec::new();
16-
let mut flattened_fields = Vec::new();
17-
let mut dependencies = Dependencies::new(crate_rename.clone());
18-
19-
if let Some(tag) = &attr.tag {
20-
let formatted = format!("\"{}\": \"{}\",", tag, name);
21-
formatted_fields.push(quote! {
22-
#formatted.to_string()
23-
});
24-
}
25-
26-
for field in &fields.named {
27-
format_field(
28-
&crate_rename,
29-
&mut formatted_fields,
30-
&mut flattened_fields,
31-
&mut dependencies,
32-
field,
33-
&attr.rename_all,
34-
attr.optional,
35-
)?;
36-
}
37-
38-
let fields = quote!(<[String]>::join(&[#(#formatted_fields),*], " "));
39-
let flattened = quote!(<[String]>::join(&[#(#flattened_fields),*], " & "));
40-
41-
let inline = match (formatted_fields.len(), flattened_fields.len()) {
42-
(0, 0) => quote!("{ }".to_owned()),
43-
(_, 0) => quote!(format!("{{ {} }}", #fields)),
44-
(0, 1) => quote! {{
45-
if #flattened.starts_with('(') && #flattened.ends_with(')') {
46-
#flattened[1..#flattened.len() - 1].trim().to_owned()
47-
} else {
48-
#flattened.trim().to_owned()
49-
}
50-
}},
51-
(0, _) => quote!(#flattened),
52-
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
53-
};
54-
55-
let inline_flattened = match (formatted_fields.len(), flattened_fields.len()) {
56-
(0, 0) => quote!("{ }".to_owned()),
57-
(_, 0) => quote!(format!("{{ {} }}", #fields)),
58-
(0, _) => quote!(#flattened),
59-
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
60-
};
61-
62-
Ok(DerivedTS {
63-
crate_rename,
64-
// the `replace` combines `{ ... } & { ... }` into just one `{ ... }`. Not necessary, but it
65-
// results in simpler type definitions.
66-
inline: quote!(#inline.replace(" } & { ", " ")),
67-
inline_flattened: Some(quote!(#inline_flattened.replace(" } & { ", " "))),
68-
docs: attr.docs.clone(),
69-
dependencies,
70-
export: attr.export,
71-
export_to: attr.export_to.clone(),
72-
ts_name: name.to_owned(),
73-
concrete: attr.concrete.clone(),
74-
bound: attr.bound.clone(),
75-
})
76-
}
77-
78-
// build an expression which expands to a string, representing a single field of a struct.
79-
//
80-
// formatted_fields will contain all the fields that do not contain the flatten
81-
// attribute, in the format
82-
// key: type,
83-
//
84-
// flattened_fields will contain all the fields that contain the flatten attribute
85-
// in their respective formats, which for a named struct is the same as formatted_fields,
86-
// but for enums is
87-
// ({ /* variant data */ } | { /* variant data */ })
88-
fn format_field(
89-
crate_rename: &Path,
90-
formatted_fields: &mut Vec<TokenStream>,
91-
flattened_fields: &mut Vec<TokenStream>,
92-
dependencies: &mut Dependencies,
93-
field: &Field,
94-
rename_all: &Option<Inflection>,
95-
struct_optional: Optional,
96-
) -> Result<()> {
97-
let field_attr = FieldAttr::from_attrs(&field.attrs)?;
98-
99-
field_attr.assert_validity(field)?;
100-
101-
if field_attr.skip {
102-
return Ok(());
103-
}
104-
105-
let ty = field_attr.type_as(&field.ty);
106-
107-
let (optional_annotation, nullable) = match (struct_optional, field_attr.optional) {
108-
// `#[ts(optional)]` on field takes precedence, and is enforced **AT COMPILE TIME**
109-
(_, Optional::Optional { nullable }) => (
110-
// expression that evaluates to the string "?", but fails to compile if `ty` is not an `Option`.
111-
quote_spanned! { field.span() => {
112-
fn check_that_field_is_option<T: #crate_rename::IsOption>(_: std::marker::PhantomData<T>) {}
113-
let x: std::marker::PhantomData<#ty> = std::marker::PhantomData;
114-
check_that_field_is_option(x);
115-
"?"
116-
}},
117-
nullable,
118-
),
119-
// `#[ts(optional)]` on the struct acts as `#[ts(optional)]` on a field, but does not error on non-`Option`
120-
// fields. Instead, it is a no-op.
121-
(Optional::Optional { nullable }, _) => (
122-
quote! {
123-
if <#ty as #crate_rename::TS>::IS_OPTION { "?" } else { "" }
124-
},
125-
nullable,
126-
),
127-
_ => (quote!(""), true),
128-
};
129-
130-
let ty = if nullable {
131-
ty
132-
} else {
133-
parse_quote! {<#ty as #crate_rename::TS>::OptionInnerType}
134-
};
135-
136-
if field_attr.flatten {
137-
flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened()));
138-
dependencies.append_from(&ty);
139-
return Ok(());
140-
}
141-
142-
let formatted_ty = field_attr
143-
.type_override
144-
.map(|t| quote!(#t))
145-
.unwrap_or_else(|| {
146-
if field_attr.inline {
147-
dependencies.append_from(&ty);
148-
quote!(<#ty as #crate_rename::TS>::inline())
149-
} else {
150-
dependencies.push(&ty);
151-
quote!(<#ty as #crate_rename::TS>::name())
152-
}
153-
});
154-
155-
let field_name = to_ts_ident(field.ident.as_ref().unwrap());
156-
let name = match (field_attr.rename, rename_all) {
157-
(Some(rn), _) => rn,
158-
(None, Some(rn)) => rn.apply(&field_name),
159-
(None, None) => field_name,
160-
};
161-
let valid_name = raw_name_to_ts_field(name);
162-
163-
// Start every doc string with a newline, because when other characters are in front, it is not "understood" by VSCode
164-
let docs = match field_attr.docs.is_empty() {
165-
true => "".to_string(),
166-
false => format!("\n{}", &field_attr.docs),
167-
};
168-
169-
formatted_fields.push(quote! {
170-
format!("{}{}{}: {},", #docs, #valid_name, #optional_annotation, #formatted_ty)
171-
});
172-
173-
Ok(())
174-
}
1+
use crate::{
2+
attr::{Attr, ContainerAttr, FieldAttr, Inflection, Optional, StructAttr},
3+
deps::Dependencies,
4+
utils::{raw_name_to_ts_field, to_ts_ident},
5+
DerivedTS,
6+
};
7+
use proc_macro2::TokenStream;
8+
use quote::{quote, quote_spanned};
9+
use syn::spanned::Spanned;
10+
use syn::{parse_quote, Field, FieldsNamed, Path, Result};
11+
12+
pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Result<DerivedTS> {
13+
let crate_rename = attr.crate_rename();
14+
15+
let mut formatted_fields = Vec::new();
16+
let mut flattened_fields = Vec::new();
17+
let mut dependencies = Dependencies::new(crate_rename.clone());
18+
19+
if let Some(tag) = &attr.tag {
20+
let formatted = format!("\"{}\": \"{}\",", tag, name);
21+
formatted_fields.push(quote! {
22+
#formatted.to_string()
23+
});
24+
}
25+
26+
for field in &fields.named {
27+
format_field(
28+
&crate_rename,
29+
&mut formatted_fields,
30+
&mut flattened_fields,
31+
&mut dependencies,
32+
field,
33+
&attr.rename_all,
34+
attr.optional_fields,
35+
)?;
36+
}
37+
38+
let fields = quote!(<[String]>::join(&[#(#formatted_fields),*], " "));
39+
let flattened = quote!(<[String]>::join(&[#(#flattened_fields),*], " & "));
40+
41+
let inline = match (formatted_fields.len(), flattened_fields.len()) {
42+
(0, 0) => quote!("{ }".to_owned()),
43+
(_, 0) => quote!(format!("{{ {} }}", #fields)),
44+
(0, 1) => quote! {{
45+
if #flattened.starts_with('(') && #flattened.ends_with(')') {
46+
#flattened[1..#flattened.len() - 1].trim().to_owned()
47+
} else {
48+
#flattened.trim().to_owned()
49+
}
50+
}},
51+
(0, _) => quote!(#flattened),
52+
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
53+
};
54+
55+
let inline_flattened = match (formatted_fields.len(), flattened_fields.len()) {
56+
(0, 0) => quote!("{ }".to_owned()),
57+
(_, 0) => quote!(format!("{{ {} }}", #fields)),
58+
(0, _) => quote!(#flattened),
59+
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
60+
};
61+
62+
Ok(DerivedTS {
63+
crate_rename,
64+
// the `replace` combines `{ ... } & { ... }` into just one `{ ... }`. Not necessary, but it
65+
// results in simpler type definitions.
66+
inline: quote!(#inline.replace(" } & { ", " ")),
67+
inline_flattened: Some(quote!(#inline_flattened.replace(" } & { ", " "))),
68+
docs: attr.docs.clone(),
69+
dependencies,
70+
export: attr.export,
71+
export_to: attr.export_to.clone(),
72+
ts_name: name.to_owned(),
73+
concrete: attr.concrete.clone(),
74+
bound: attr.bound.clone(),
75+
})
76+
}
77+
78+
// build an expression which expands to a string, representing a single field of a struct.
79+
//
80+
// formatted_fields will contain all the fields that do not contain the flatten
81+
// attribute, in the format
82+
// key: type,
83+
//
84+
// flattened_fields will contain all the fields that contain the flatten attribute
85+
// in their respective formats, which for a named struct is the same as formatted_fields,
86+
// but for enums is
87+
// ({ /* variant data */ } | { /* variant data */ })
88+
fn format_field(
89+
crate_rename: &Path,
90+
formatted_fields: &mut Vec<TokenStream>,
91+
flattened_fields: &mut Vec<TokenStream>,
92+
dependencies: &mut Dependencies,
93+
field: &Field,
94+
rename_all: &Option<Inflection>,
95+
struct_optional: Optional,
96+
) -> Result<()> {
97+
let field_attr = FieldAttr::from_attrs(&field.attrs)?;
98+
99+
field_attr.assert_validity(field)?;
100+
101+
if field_attr.skip {
102+
return Ok(());
103+
}
104+
105+
let ty = field_attr.type_as(&field.ty);
106+
107+
let (optional_annotation, nullable) = match (struct_optional, field_attr.optional) {
108+
// `#[ts(optional)]` on field takes precedence, and is enforced **AT COMPILE TIME**
109+
(_, Optional::Optional { nullable }) => (
110+
// expression that evaluates to the string "?", but fails to compile if `ty` is not an `Option`.
111+
quote_spanned! { field.span() => {
112+
fn check_that_field_is_option<T: #crate_rename::IsOption>(_: std::marker::PhantomData<T>) {}
113+
let x: std::marker::PhantomData<#ty> = std::marker::PhantomData;
114+
check_that_field_is_option(x);
115+
"?"
116+
}},
117+
nullable,
118+
),
119+
// `#[ts(optional)]` on the struct acts as `#[ts(optional)]` on a field, but does not error on non-`Option`
120+
// fields. Instead, it is a no-op.
121+
(Optional::Optional { nullable }, _) => (
122+
quote! {
123+
if <#ty as #crate_rename::TS>::IS_OPTION { "?" } else { "" }
124+
},
125+
nullable,
126+
),
127+
_ => (quote!(""), true),
128+
};
129+
130+
let ty = if nullable {
131+
ty
132+
} else {
133+
parse_quote! {<#ty as #crate_rename::TS>::OptionInnerType}
134+
};
135+
136+
if field_attr.flatten {
137+
flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened()));
138+
dependencies.append_from(&ty);
139+
return Ok(());
140+
}
141+
142+
let formatted_ty = field_attr
143+
.type_override
144+
.map(|t| quote!(#t))
145+
.unwrap_or_else(|| {
146+
if field_attr.inline {
147+
dependencies.append_from(&ty);
148+
quote!(<#ty as #crate_rename::TS>::inline())
149+
} else {
150+
dependencies.push(&ty);
151+
quote!(<#ty as #crate_rename::TS>::name())
152+
}
153+
});
154+
155+
let field_name = to_ts_ident(field.ident.as_ref().unwrap());
156+
let name = match (field_attr.rename, rename_all) {
157+
(Some(rn), _) => rn,
158+
(None, Some(rn)) => rn.apply(&field_name),
159+
(None, None) => field_name,
160+
};
161+
let valid_name = raw_name_to_ts_field(name);
162+
163+
// Start every doc string with a newline, because when other characters are in front, it is not "understood" by VSCode
164+
let docs = match field_attr.docs.is_empty() {
165+
true => "".to_string(),
166+
false => format!("\n{}", &field_attr.docs),
167+
};
168+
169+
formatted_fields.push(quote! {
170+
format!("{}{}{}: {},", #docs, #valid_name, #optional_annotation, #formatted_ty)
171+
});
172+
173+
Ok(())
174+
}

ts-rs/tests/integration/optional_field.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ type Foo = Option<i32>;
9292
type Bar<T> = Option<T>;
9393

9494
#[derive(TS)]
95-
#[ts(export, export_to = "optional_field/", optional)]
95+
#[ts(export, export_to = "optional_field/", optional_fields)]
9696
struct OptionalStruct {
9797
a: Option<i32>,
9898
b: Option<i32>,

0 commit comments

Comments
 (0)