telemetry_safe_derive/
lib.rs1use proc_macro::TokenStream;
2use quote::quote;
3use syn::parse_macro_input;
4use syn::spanned::Spanned;
5use syn::{
6 Attribute, Data, DataEnum, DataStruct, DeriveInput, Error, Expr, Fields, LitStr, Result, Token,
7};
8
9#[proc_macro_derive(ToTelemetry, attributes(telemetry))]
10pub fn derive_to_telemetry(input: TokenStream) -> TokenStream {
11 let input = parse_macro_input!(input as DeriveInput);
12 match expand_derive(&input) {
13 Ok(tokens) => tokens.into(),
14 Err(err) => err.to_compile_error().into(),
15 }
16}
17
18fn expand_derive(input: &DeriveInput) -> Result<proc_macro2::TokenStream> {
19 let ident = &input.ident;
20 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
21
22 let body = match &input.data {
23 Data::Struct(data) => expand_struct(ident, data)?,
24 Data::Enum(data) => expand_enum(data)?,
25 Data::Union(data) => {
26 return Err(Error::new(
27 data.union_token.span(),
28 "ToTelemetry cannot be derived for unions",
29 ));
30 }
31 };
32
33 Ok(quote! {
34 impl #impl_generics ::telemetry_safe::ToTelemetry for #ident #ty_generics #where_clause {
35 fn fmt_telemetry(
36 &self,
37 f: &mut ::std::fmt::Formatter<'_>,
38 ) -> ::std::fmt::Result {
39 #body
40 }
41 }
42 })
43}
44
45fn expand_struct(ident: &syn::Ident, data: &DataStruct) -> Result<proc_macro2::TokenStream> {
46 match &data.fields {
47 Fields::Named(fields) => {
48 let mut field_exprs = Vec::new();
49 for field in &fields.named {
50 let attr = parse_field_attr(&field.attrs).transpose()?;
51 if matches!(attr, Some(FieldAttr::Skip)) {
52 continue;
53 }
54
55 let name = field.ident.as_ref().expect("named field");
56 let key = LitStr::new(&name.to_string(), name.span());
57 let value = field_expr(field, quote! { self.#name }, attr)?;
58 field_exprs.push(quote! {
59 ds.field(#key, &#value);
60 });
61 }
62
63 Ok(quote! {
64 let mut ds = f.debug_struct(stringify!(#ident));
65 #(#field_exprs)*
66 ds.finish()
67 })
68 }
69 Fields::Unnamed(fields) => {
70 let mut field_exprs = Vec::new();
71 for (index, field) in fields.unnamed.iter().enumerate() {
72 let attr = parse_field_attr(&field.attrs).transpose()?;
73 if matches!(attr, Some(FieldAttr::Skip)) {
74 continue;
75 }
76
77 let accessor = syn::Index::from(index);
78 let value = field_expr(field, quote! { self.#accessor }, attr)?;
79 field_exprs.push(quote! {
80 ds.field(&#value);
81 });
82 }
83
84 Ok(quote! {
85 let mut ds = f.debug_tuple(stringify!(#ident));
86 #(#field_exprs)*
87 ds.finish()
88 })
89 }
90 Fields::Unit => Ok(quote! {
91 f.write_str(stringify!(#ident))
92 }),
93 }
94}
95
96fn expand_enum(data: &DataEnum) -> Result<proc_macro2::TokenStream> {
97 let arms = data
98 .variants
99 .iter()
100 .map(|variant| {
101 let ident = &variant.ident;
102 match &variant.fields {
103 Fields::Named(fields) => {
104 let mut bindings = Vec::new();
105 let mut formatter = Vec::new();
106 for field in &fields.named {
107 let attr = parse_field_attr(&field.attrs).transpose()?;
108 let name = field.ident.as_ref().expect("named field");
109
110 if matches!(attr, Some(FieldAttr::Skip)) {
111 bindings.push(quote! { #name: _ });
112 continue;
113 }
114
115 if !field_attr_requires_binding(attr.as_ref()) {
116 bindings.push(quote! { #name: _ });
120 } else {
121 bindings.push(quote! { #name });
122 }
123
124 let key = LitStr::new(&name.to_string(), name.span());
125 let value = field_expr(field, quote! { #name }, attr)?;
126 formatter.push(quote! {
127 ds.field(#key, &#value);
128 });
129 }
130
131 Ok(quote! {
132 Self::#ident { #(#bindings),* } => {
133 let mut ds = f.debug_struct(stringify!(#ident));
134 #(#formatter)*
135 ds.finish()
136 }
137 })
138 }
139 Fields::Unnamed(fields) => {
140 let mut bindings = Vec::new();
141 let mut formatter = Vec::new();
142 for (index, field) in fields.unnamed.iter().enumerate() {
143 let attr = parse_field_attr(&field.attrs).transpose()?;
144 let binding = syn::Ident::new(&format!("field_{index}"), ident.span());
145
146 if matches!(attr, Some(FieldAttr::Skip)) {
147 bindings.push(quote! { _ });
148 continue;
149 }
150
151 if !field_attr_requires_binding(attr.as_ref()) {
152 bindings.push(quote! { _ });
153 } else {
154 bindings.push(quote! { #binding });
155 }
156
157 let value = field_expr(field, quote! { #binding }, attr)?;
158 formatter.push(quote! {
159 ds.field(&#value);
160 });
161 }
162
163 Ok(quote! {
164 Self::#ident(#(#bindings),*) => {
165 let mut ds = f.debug_tuple(stringify!(#ident));
166 #(#formatter)*
167 ds.finish()
168 }
169 })
170 }
171 Fields::Unit => Ok(quote! {
172 Self::#ident => f.write_str(stringify!(#ident))
173 }),
174 }
175 })
176 .collect::<Result<Vec<_>>>()?;
177
178 Ok(quote! {
179 match self {
180 #(#arms),*
181 }
182 })
183}
184
185fn field_expr(
186 field: &syn::Field,
187 accessor: proc_macro2::TokenStream,
188 attr: Option<FieldAttr>,
189) -> Result<proc_macro2::TokenStream> {
190 match attr {
191 Some(FieldAttr::Literal(literal)) => Ok(quote! {
192 ::std::format_args!("{}", #literal)
193 }),
194 Some(FieldAttr::Display(format)) => {
195 match format {
198 DisplayFormat::Implicit => Ok(quote! {
199 ::std::format_args!("{}", #accessor)
200 }),
201 DisplayFormat::Interpolated(format) => Ok(quote! {
202 ::std::format_args!(#format, #accessor)
203 }),
204 }
205 }
206 Some(FieldAttr::Skip) | None => {
207 let ty = &field.ty;
208 Ok(quote! {{
209 let value: &#ty = &#accessor;
210 ::telemetry_safe::telemetry_debug(value)
211 }})
212 }
213 }
214}
215
216enum FieldAttr {
217 Literal(LitStr),
218 Display(DisplayFormat),
219 Skip,
220}
221
222enum DisplayFormat {
223 Implicit,
224 Interpolated(LitStr),
225}
226
227fn field_attr_requires_binding(attr: Option<&FieldAttr>) -> bool {
228 !matches!(attr, Some(FieldAttr::Skip | FieldAttr::Literal(_)))
229}
230
231fn parse_field_attr(attrs: &[Attribute]) -> Option<Result<FieldAttr>> {
232 attrs
233 .iter()
234 .find(|attr| attr.path().is_ident("telemetry"))
235 .map(parse_single_field_attr)
236}
237
238fn parse_single_field_attr(attr: &Attribute) -> Result<FieldAttr> {
239 attr.parse_args_with(|input: syn::parse::ParseStream<'_>| {
240 if input.peek(syn::Ident) {
241 let ident: syn::Ident = input.parse()?;
242 if ident == "skip" {
243 if !input.is_empty() {
244 return Err(input.error("unexpected tokens after skip"));
245 }
246 return Ok(FieldAttr::Skip);
247 }
248
249 if ident == "display" {
250 if input.is_empty() {
251 return Ok(FieldAttr::Display(DisplayFormat::Implicit));
252 }
253
254 let _eq: Token![=] = input.parse()?;
255 let format: LitStr = input.parse()?;
256 if !input.is_empty() {
257 return Err(input.error("unexpected tokens after display format"));
258 }
259
260 return Ok(FieldAttr::Display(parse_display_format(format)?));
261 }
262
263 return Err(Error::new(
264 ident.span(),
265 "unsupported telemetry attribute; expected `skip`, `display`, or a string literal",
266 ));
267 }
268
269 let format: Expr = input.parse()?;
270 if !input.is_empty() {
271 let _comma: Token![,] = input.parse()?;
272 if !input.is_empty() {
273 return Err(input.error("expected a single format string or `skip`"));
274 }
275 }
276
277 match format {
278 Expr::Lit(expr_lit) => match expr_lit.lit {
279 syn::Lit::Str(lit) => Ok(FieldAttr::Literal(parse_literal_format(lit)?)),
280 other => Err(Error::new(other.span(), "expected string literal")),
281 },
282 other => Err(Error::new(other.span(), "expected string literal")),
283 }
284 })
285}
286
287fn parse_literal_format(format: LitStr) -> Result<LitStr> {
288 let value = format.value();
289 if value.contains(['{', '}']) {
290 return Err(Error::new(
291 format.span(),
292 "string literal telemetry formats cannot contain `{` or `}`; use `display` to opt into Display formatting",
293 ));
294 }
295
296 Ok(format)
297}
298
299fn parse_display_format(format: LitStr) -> Result<DisplayFormat> {
300 let value = format.value();
301 let placeholder_count = value.matches("{}").count();
302
303 if value.replace("{}", "").contains(['{', '}']) {
305 return Err(Error::new(
306 format.span(),
307 "display format must contain exactly one `{}` placeholder",
308 ));
309 }
310
311 match placeholder_count {
312 1 => Ok(DisplayFormat::Interpolated(format)),
313 _ => Err(Error::new(
314 format.span(),
315 "display format must contain exactly one `{}` placeholder",
316 )),
317 }
318}