ters_macros/lib.rs
1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{parse_macro_input, ItemStruct};
4
5/// Generate getters and setters procedurally.
6///
7/// Annotate fields with `#[get]` to generate a getter method.
8/// ```ignore
9/// use ters::ters;
10///
11/// #[ters]
12/// struct Foo {
13/// a: i32,
14/// #[get]
15/// b: bool,
16/// }
17///
18/// fn getters() {
19/// let foo = Foo { a: 42, b: true };
20/// assert_eq!(foo.b(), &true);
21/// }
22/// ```
23///
24/// Annotate fields with `#[set]` to generate a setter method.
25/// ```ignore
26/// use ters::ters;
27///
28/// #[ters]
29/// struct Foo {
30/// #[set]
31/// a: i32,
32/// b: bool,
33/// }
34///
35/// fn setters() {
36/// let mut foo = Foo { a: 42, b: true };
37/// foo.set_a(31);
38/// }
39/// ```
40///
41/// Annotate fields with `#[get]` and `#[set]` to generate both a getter and a setter method.
42/// ```ignore
43/// use ters::ters;
44///
45/// #[ters]
46/// struct Foo {
47/// #[get]
48/// #[set]
49/// a: i32,
50/// b: bool,
51/// }
52///
53/// fn getters_and_setters() {
54/// let mut foo = Foo { a: 42, b: true };
55/// assert_eq!(foo.a(), &42);
56/// foo.set_a(31);
57///
58/// assert_eq!(foo.a(), &31);
59/// }
60/// ```
61///
62/// Unannotated fields will not have generated getters or setters.
63/// ```ignore
64/// use ters::ters;
65///
66/// #[ters]
67/// struct Foo {
68/// a: i32,
69/// #[get]
70/// b: bool,
71/// }
72///
73/// fn getters_not_generated() {
74/// let foo = Foo { a: 42, b: true };
75/// assert_eq!(foo.a(), &42); // this method doesn't exist
76/// }
77/// ```
78#[proc_macro_attribute]
79pub fn ters(_args: TokenStream, tokens: TokenStream) -> TokenStream {
80 let item = parse_macro_input!(tokens as ItemStruct);
81
82 ters_inner(item).into()
83}
84
85fn ters_inner(mut item: ItemStruct) -> proc_macro2::TokenStream {
86 let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
87
88 let mut fields = Vec::new();
89
90 for field in item.fields.iter_mut() {
91 let mut get = false;
92 let mut set = false;
93
94 field.attrs.retain(|attr| {
95 if attr.path().is_ident("get") {
96 get = true;
97 false
98 } else if attr.path().is_ident("set") {
99 set = true;
100 false
101 } else {
102 true
103 }
104 });
105
106 fields.push((
107 field.ident.clone().unwrap(),
108 field.ty.clone(),
109 get,
110 set,
111 field
112 .attrs
113 .iter()
114 .filter(|attr| {
115 attr.path()
116 .get_ident()
117 .map(|ident| ident == "doc")
118 .is_some_and(|is_doc| is_doc)
119 })
120 .cloned()
121 .collect::<Vec<_>>(),
122 ));
123 }
124
125 let accessors = fields
126 .iter()
127 .filter_map(|(ident, ty, get, set, docs)| {
128 let set_ident = format_ident!("set_{ident}");
129 let str_ident = ident.to_string();
130
131 let mut body = quote! {};
132
133 if *get {
134 body.extend(quote! {
135 #[doc = "Getter for `"]
136 #[doc = #str_ident]
137 #[doc = "`.\n\n"]
138 #(#docs)*
139 #[inline]
140 pub fn #ident(&self) -> &#ty {
141 &self.#ident
142 }
143 });
144 }
145
146 if *set {
147 body.extend(quote! {
148 #[doc = "Setter for `"]
149 #[doc = #str_ident]
150 #[doc = "`.\n\n"]
151 #(#docs)*
152 #[inline]
153 pub fn #set_ident(&mut self, value: #ty) {
154 self.#ident = value;
155 }
156 });
157 }
158
159 (!body.is_empty()).then_some(body)
160 })
161 .collect::<Vec<_>>();
162
163 let ident = &item.ident;
164
165 let impl_ = (!accessors.is_empty()).then_some(quote! {
166 impl #impl_generics #ident #ty_generics #where_clause {
167 #(
168 #accessors
169 )*
170 }
171 });
172
173 quote! {
174 #item
175 #impl_
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use quote::quote;
182 use syn::parse_quote;
183
184 use crate::ters_inner;
185
186 #[test]
187 fn docs() {
188 let input = parse_quote! {
189 struct Foo {
190 /// Baz.
191 #[get]
192 bar: u8,
193 }
194 };
195
196 let expected = quote! {
197 struct Foo {
198 /// Baz.
199 bar: u8,
200 }
201
202 impl Foo {
203 #[doc = "Getter for `"]
204 #[doc = "bar"]
205 #[doc = "`.\n\n"]
206 /// Baz.
207 #[inline]
208 pub fn bar(&self) -> &u8 {
209 &self.bar
210 }
211 }
212 };
213
214 let out: proc_macro2::TokenStream = ters_inner(input).into();
215
216 assert_eq!(out.to_string(), expected.to_string());
217 }
218}