relay_event_derive/
lib.rs

1//! Derive for visitor traits on the Event schema.
2//!
3//! This derive provides the `ProcessValue` trait. It must be used within the `relay-event-schema`
4//! crate.
5
6#![warn(missing_docs)]
7#![doc(
8    html_logo_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png",
9    html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png"
10)]
11#![recursion_limit = "256"]
12
13use proc_macro2::{Span, TokenStream};
14use quote::{ToTokens, quote};
15use syn::meta::ParseNestedMeta;
16use syn::{Ident, Lit, LitBool, LitInt, LitStr};
17use synstructure::decl_derive;
18
19mod utils;
20
21use utils::SynstructureExt as _;
22
23decl_derive!(
24    [ProcessValue, attributes(metastructure)] =>
25    /// Derive the `ProcessValue` trait.
26    derive_process_value
27);
28
29fn derive_process_value(mut s: synstructure::Structure<'_>) -> syn::Result<TokenStream> {
30    let _ = s.bind_with(|_bi| synstructure::BindStyle::RefMut);
31    let _ = s.add_bounds(synstructure::AddBounds::Generics);
32
33    let type_attrs = parse_type_attributes(&s)?;
34    let process_func_call_tokens = type_attrs.process_func_call_tokens();
35
36    let process_value_arms = s.try_each_variant(|variant| {
37        if is_newtype(variant) {
38            // Process variant twice s.t. both processor functions are called.
39            //
40            // E.g.:
41            // - For `Value::String`, call `process_string` as well as `process_value`.
42            // - For `LenientString`, call `process_lenient_string` (not a thing.. yet) as well as
43            // `process_string`.
44
45            let bi = &variant.bindings()[0];
46            let ident = &bi.binding;
47            let field_attrs = parse_field_attributes(0, bi.ast(), &mut true)?;
48            let field_attrs_tokens = field_attrs.as_tokens(&type_attrs, Some(quote!(parent_attrs)));
49
50            Ok(quote! {
51                let parent_attrs = __state.attrs();
52                let attrs = #field_attrs_tokens;
53                let __state = &__state.enter_nothing(
54                    Some(::std::borrow::Cow::Owned(attrs))
55                );
56
57                // This is a copy of `funcs::process_value`, due to ownership issues. In particular
58                // we want to pass the same meta twice.
59                //
60                // NOTE: Handling for ProcessingAction is slightly different (early-return). This
61                // should be fine though.
62                let action = __processor.before_process(
63                    Some(&*#ident),
64                    __meta,
65                    &__state
66                )?;
67
68                crate::processor::ProcessValue::process_value(
69                    #ident,
70                    __meta,
71                    __processor,
72                    &__state
73                )?;
74
75                __processor.after_process(
76                    Some(#ident),
77                    __meta,
78                    &__state
79                )?;
80            })
81        } else {
82            Ok(quote!())
83        }
84    })?;
85
86    let process_child_values_arms = s.try_each_variant(|variant| {
87        let mut is_tuple_struct = false;
88
89        if is_newtype(variant) {
90            // `process_child_values` has to be a noop because otherwise we recurse into the
91            // subtree twice due to the weird `process_value` impl
92
93            return Ok(quote!());
94        }
95
96        let mut body = TokenStream::new();
97        for (index, bi) in variant.bindings().iter().enumerate() {
98            let field_attrs = parse_field_attributes(index, bi.ast(), &mut is_tuple_struct)?;
99            let ident = &bi.binding;
100            let field_attrs_name = Ident::new(&format!("FIELD_ATTRS_{index}"), Span::call_site());
101            let field_name = field_attrs.field_name.clone();
102
103            let field_attrs_tokens = field_attrs.as_tokens(&type_attrs, None);
104
105            (quote! {
106                static #field_attrs_name: crate::processor::FieldAttrs = #field_attrs_tokens;
107            })
108            .to_tokens(&mut body);
109
110            let enter_state = if field_attrs.additional_properties {
111                if is_tuple_struct {
112                    panic!("additional_properties not allowed in tuple struct");
113                }
114
115                quote! {
116                    __state.enter_nothing(Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)))
117                }
118            } else if field_attrs.flatten {
119                quote! {
120                    __state.enter_nothing(Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)))
121                }
122            } else if is_tuple_struct {
123                quote! {
124                    __state.enter_index(
125                        #index,
126                        Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)),
127                        crate::processor::ValueType::for_field(#ident),
128                    )
129                }
130            } else {
131                quote! {
132                    __state.enter_static(
133                        #field_name,
134                        Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)),
135                        crate::processor::ValueType::for_field(#ident),
136                    )
137                }
138            };
139
140            if field_attrs.additional_properties {
141                (quote! {
142                    __processor.process_other(#ident, &#enter_state)?;
143                })
144                .to_tokens(&mut body);
145            } else if field_attrs.flatten {
146                (quote! {
147                    crate::processor::ProcessValue::process_child_values(
148                        #ident,
149                        __processor,
150                       &#enter_state
151                    )?;
152                })
153                .to_tokens(&mut body);
154            } else {
155                (quote! {
156                    crate::processor::process_value(#ident, __processor, &#enter_state)?;
157                })
158                .to_tokens(&mut body);
159            }
160        }
161
162        Ok(quote!({ #body }))
163    })?;
164
165    let _ = s.bind_with(|_bi| synstructure::BindStyle::Ref);
166
167    let value_type_arms = s.each_variant(|variant| {
168        if !type_attrs.value_type.is_empty() {
169            let value_names = type_attrs
170                .value_type
171                .iter()
172                .map(|value_name| Ident::new(value_name, Span::call_site()));
173            quote! {
174                // enumset produces a deprecation warning because it thinks we use its internals
175                // directly, but we do actually use the macro
176                #[allow(deprecated)]
177                {
178                    enumset::enum_set!( #(crate::processor::ValueType::#value_names)|* )
179                }
180            }
181        } else if is_newtype(variant) {
182            let bi = &variant.bindings()[0];
183            let ident = &bi.binding;
184            quote!(crate::processor::ProcessValue::value_type(#ident))
185        } else {
186            quote!(enumset::EnumSet::empty())
187        }
188    });
189
190    Ok(s.gen_impl(quote! {
191        #[automatically_derived]
192        #[expect(non_local_definitions, reason = "crate needs to be migrated to syn")]
193        gen impl crate::processor::ProcessValue for @Self {
194            fn value_type(&self) -> enumset::EnumSet<crate::processor::ValueType> {
195                match *self {
196                    #value_type_arms
197                }
198            }
199
200            fn process_value<P>(
201                &mut self,
202                __meta: &mut relay_protocol::Meta,
203                __processor: &mut P,
204                __state: &crate::processor::ProcessingState<'_>,
205            ) -> crate::processor::ProcessingResult
206            where
207                P: crate::processor::Processor,
208            {
209                #process_func_call_tokens;
210                match *self {
211                    #process_value_arms
212                }
213
214                Ok(())
215            }
216
217            #[inline]
218            fn process_child_values<P>(
219                &mut self,
220                __processor: &mut P,
221                __state: &crate::processor::ProcessingState<'_>
222            ) -> crate::processor::ProcessingResult
223            where
224                P: crate::processor::Processor,
225            {
226                match *self {
227                    #process_child_values_arms
228                }
229
230                Ok(())
231            }
232        }
233    }))
234}
235
236#[derive(Default)]
237struct TypeAttrs {
238    process_func: Option<String>,
239    value_type: Vec<String>,
240    /// The default trim value for the container.
241    ///
242    /// If `trim` is specified on the container all fields of the container,
243    /// will default to this value for `trim`.
244    trim: Option<bool>,
245    /// The default pii value for the container.
246    ///
247    /// If `pii` is specified on the container, all fields of the container
248    /// will default to this value for `pii`.
249    pii: Option<Pii>,
250}
251
252impl TypeAttrs {
253    fn process_func_call_tokens(&self) -> TokenStream {
254        if let Some(ref func_name) = self.process_func {
255            let func_name = Ident::new(func_name, Span::call_site());
256            quote! {
257                __processor.#func_name(self, __meta, __state)?;
258            }
259        } else {
260            quote! {
261                self.process_child_values(__processor, __state)?;
262            }
263        }
264    }
265}
266
267fn parse_type_attributes(s: &synstructure::Structure<'_>) -> syn::Result<TypeAttrs> {
268    let mut rv = TypeAttrs::default();
269
270    for attr in &s.ast().attrs {
271        if !attr.path().is_ident("metastructure") {
272            continue;
273        }
274
275        attr.parse_nested_meta(|meta| {
276            let ident = meta.path.require_ident()?;
277
278            if ident == "process_func" {
279                let s = meta.value()?.parse::<LitStr>()?;
280                rv.process_func = Some(s.value());
281            } else if ident == "value_type" {
282                let s = meta.value()?.parse::<LitStr>()?;
283                rv.value_type.push(s.value());
284            } else if ident == "trim" {
285                let s = meta.value()?.parse::<LitBool>()?;
286                rv.trim = Some(s.value());
287            } else if ident == "pii" {
288                let s = meta.value()?.parse::<LitStr>()?;
289                rv.pii = parse_pii_value(s, &meta)?;
290            } else {
291                // Ignore other attributes used by `relay-protocol-derive`.
292                if !meta.input.peek(syn::Token![,]) {
293                    let _ = meta.value()?.parse::<Lit>()?;
294                }
295            }
296
297            Ok(())
298        })?;
299    }
300
301    Ok(rv)
302}
303
304#[derive(Copy, Clone, Debug)]
305enum Pii {
306    True,
307    False,
308    Maybe,
309}
310
311impl Pii {
312    fn as_tokens(self) -> TokenStream {
313        match self {
314            Pii::True => quote!(crate::processor::Pii::True),
315            Pii::False => quote!(crate::processor::Pii::False),
316            Pii::Maybe => quote!(crate::processor::Pii::Maybe),
317        }
318    }
319}
320
321#[derive(Default)]
322struct FieldAttrs {
323    additional_properties: bool,
324    omit_from_schema: bool,
325    field_name: String,
326    flatten: bool,
327    required: Option<bool>,
328    nonempty: Option<bool>,
329    trim_whitespace: Option<bool>,
330    pii: Option<Pii>,
331    retain: bool,
332    characters: Option<TokenStream>,
333    max_chars: Option<TokenStream>,
334    max_chars_allowance: Option<TokenStream>,
335    max_depth: Option<TokenStream>,
336    max_bytes: Option<TokenStream>,
337    trim: Option<bool>,
338}
339
340impl FieldAttrs {
341    fn as_tokens(
342        &self,
343        type_attrs: &TypeAttrs,
344        inherit_from_field_attrs: Option<TokenStream>,
345    ) -> TokenStream {
346        let field_name = &self.field_name;
347
348        if self.required.is_none() && self.nonempty.is_some() {
349            panic!(
350                "`required` has to be explicitly set to \"true\" or \"false\" if `nonempty` is used."
351            );
352        }
353        let required = if let Some(required) = self.required {
354            quote!(#required)
355        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
356            quote!(#parent_attrs.required)
357        } else {
358            quote!(false)
359        };
360
361        let nonempty = if let Some(nonempty) = self.nonempty {
362            quote!(#nonempty)
363        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
364            quote!(#parent_attrs.nonempty)
365        } else {
366            quote!(false)
367        };
368
369        let trim_whitespace = if let Some(trim_whitespace) = self.trim_whitespace {
370            quote!(#trim_whitespace)
371        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
372            quote!(#parent_attrs.trim_whitespace)
373        } else {
374            quote!(false)
375        };
376
377        let pii = if let Some(pii) = self.pii.or(type_attrs.pii) {
378            pii.as_tokens()
379        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
380            quote!(#parent_attrs.pii)
381        } else {
382            quote!(crate::processor::Pii::False)
383        };
384
385        let trim = if let Some(trim) = self.trim.or(type_attrs.trim) {
386            quote!(#trim)
387        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
388            quote!(#parent_attrs.trim)
389        } else {
390            quote!(true)
391        };
392
393        let retain = self.retain;
394
395        let max_chars = if let Some(ref max_chars) = self.max_chars {
396            quote!(Some(#max_chars))
397        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
398            quote!(#parent_attrs.max_chars)
399        } else {
400            quote!(None)
401        };
402
403        let max_chars_allowance = if let Some(ref max_chars_allowance) = self.max_chars_allowance {
404            quote!(#max_chars_allowance)
405        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
406            quote!(#parent_attrs.max_chars_allowance)
407        } else {
408            quote!(0)
409        };
410
411        let max_depth = if let Some(ref max_depth) = self.max_depth {
412            quote!(Some(#max_depth))
413        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
414            quote!(#parent_attrs.max_depth)
415        } else {
416            quote!(None)
417        };
418
419        let max_bytes = if let Some(ref max_bytes) = self.max_bytes {
420            quote!(Some(#max_bytes))
421        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
422            quote!(#parent_attrs.max_bytes)
423        } else {
424            quote!(None)
425        };
426
427        let characters = if let Some(ref characters) = self.characters {
428            quote!(Some(#characters))
429        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
430            quote!(#parent_attrs.characters)
431        } else {
432            quote!(None)
433        };
434
435        quote!({
436            crate::processor::FieldAttrs {
437                name: Some(#field_name),
438                required: #required,
439                nonempty: #nonempty,
440                trim_whitespace: #trim_whitespace,
441                max_chars: #max_chars,
442                max_chars_allowance: #max_chars_allowance,
443                characters: #characters,
444                max_depth: #max_depth,
445                max_bytes: #max_bytes,
446                pii: #pii,
447                retain: #retain,
448                trim: #trim,
449            }
450        })
451    }
452}
453
454fn parse_field_attributes(
455    index: usize,
456    bi_ast: &syn::Field,
457    is_tuple_struct: &mut bool,
458) -> syn::Result<FieldAttrs> {
459    if bi_ast.ident.is_none() {
460        *is_tuple_struct = true;
461    } else if *is_tuple_struct {
462        panic!("invalid tuple struct");
463    }
464
465    let mut rv = FieldAttrs {
466        field_name: bi_ast
467            .ident
468            .as_ref()
469            .map(ToString::to_string)
470            .unwrap_or_else(|| index.to_string()),
471        ..Default::default()
472    };
473
474    for attr in &bi_ast.attrs {
475        if !attr.path().is_ident("metastructure") {
476            continue;
477        }
478
479        attr.parse_nested_meta(|meta| {
480            let ident = meta.path.require_ident()?;
481
482            if ident == "additional_properties" {
483                rv.additional_properties = true;
484            } else if ident == "omit_from_schema" {
485                rv.omit_from_schema = true;
486            } else if ident == "field" {
487                let s = meta.value()?.parse::<LitStr>()?;
488                rv.field_name = s.value();
489            } else if ident == "flatten" {
490                rv.flatten = true;
491            } else if ident == "required" {
492                let s = meta.value()?.parse::<LitBool>()?;
493                rv.required = Some(s.value());
494            } else if ident == "nonempty" {
495                let s = meta.value()?.parse::<LitBool>()?;
496                rv.nonempty = Some(s.value());
497            } else if ident == "trim_whitespace" {
498                let s = meta.value()?.parse::<LitBool>()?;
499                rv.trim_whitespace = Some(s.value());
500            } else if ident == "allow_chars" || ident == "deny_chars" {
501                if rv.characters.is_some() {
502                    return Err(meta.error("allow_chars and deny_chars are mutually exclusive"));
503                }
504                let s = meta.value()?.parse::<LitStr>()?;
505                rv.characters = Some(parse_character_set(ident, &s.value()));
506            } else if ident == "max_chars" {
507                let s = meta.value()?.parse::<LitInt>()?;
508                rv.max_chars = Some(quote!(#s));
509            } else if ident == "max_chars_allowance" {
510                let s = meta.value()?.parse::<LitInt>()?;
511                rv.max_chars_allowance = Some(quote!(#s));
512            } else if ident == "max_depth" {
513                let s = meta.value()?.parse::<LitInt>()?;
514                rv.max_depth = Some(quote!(#s));
515            } else if ident == "max_bytes" {
516                let s = meta.value()?.parse::<LitInt>()?;
517                rv.max_bytes = Some(quote!(#s));
518            } else if ident == "pii" {
519                let s = meta.value()?.parse::<LitStr>()?;
520                rv.pii = parse_pii_value(s, &meta)?;
521            } else if ident == "retain" {
522                let s = meta.value()?.parse::<LitBool>()?;
523                rv.retain = s.value();
524            } else if ident == "trim" {
525                let s = meta.value()?.parse::<LitBool>()?;
526                rv.trim = Some(s.value());
527            } else if ident == "legacy_alias" || ident == "skip_serialization" {
528                let _ = meta.value()?.parse::<Lit>()?;
529            } else {
530                return Err(meta.error("Unknown argument"));
531            }
532
533            Ok(())
534        })?;
535    }
536
537    Ok(rv)
538}
539
540fn is_newtype(variant: &synstructure::VariantInfo) -> bool {
541    variant.bindings().len() == 1 && variant.bindings()[0].ast().ident.is_none()
542}
543
544fn parse_character_set(ident: &Ident, value: &str) -> TokenStream {
545    #[derive(Clone, Copy)]
546    enum State {
547        Blank,
548        OpenRange(char),
549        MidRange(char),
550    }
551
552    let mut state = State::Blank;
553    let mut ranges = Vec::new();
554
555    for c in value.chars() {
556        match (state, c) {
557            (State::Blank, a) => state = State::OpenRange(a),
558            (State::OpenRange(a), '-') => state = State::MidRange(a),
559            (State::OpenRange(a), c) => {
560                state = State::OpenRange(c);
561                ranges.push(quote!(#a..=#a));
562            }
563            (State::MidRange(a), b) => {
564                ranges.push(quote!(#a..=#b));
565                state = State::Blank;
566            }
567        }
568    }
569
570    match state {
571        State::OpenRange(a) => ranges.push(quote!(#a..=#a)),
572        State::MidRange(a) => {
573            ranges.push(quote!(#a..=#a));
574            ranges.push(quote!('-'..='-'));
575        }
576        State::Blank => {}
577    }
578
579    let is_negative = ident == "deny_chars";
580
581    quote! {
582        crate::processor::CharacterSet {
583            char_is_valid: |c: char| -> bool {
584                match c {
585                    #((#ranges) => !#is_negative,)*
586                    _ => #is_negative,
587                }
588            },
589            ranges: &[ #(#ranges,)* ],
590            is_negative: #is_negative,
591        }
592    }
593}
594
595fn parse_pii_value(value: LitStr, meta: &ParseNestedMeta) -> syn::Result<Option<Pii>> {
596    Ok(Some(match value.value().as_str() {
597        "true" => Pii::True,
598        "false" => Pii::False,
599        "maybe" => Pii::Maybe,
600        _ => return Err(meta.error("Expected one of `true`, `false`, `maybe`")),
601    }))
602}