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::{ExprPath, 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_borrowed(
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        gen impl crate::processor::ProcessValue for @Self {
193            fn value_type(&self) -> enumset::EnumSet<crate::processor::ValueType> {
194                match *self {
195                    #value_type_arms
196                }
197            }
198
199            fn process_value<P>(
200                &mut self,
201                __meta: &mut relay_protocol::Meta,
202                __processor: &mut P,
203                __state: &crate::processor::ProcessingState<'_>,
204            ) -> crate::processor::ProcessingResult
205            where
206                P: crate::processor::Processor,
207            {
208                #process_func_call_tokens;
209                match *self {
210                    #process_value_arms
211                }
212
213                Ok(())
214            }
215
216            #[inline]
217            fn process_child_values<P>(
218                &mut self,
219                __processor: &mut P,
220                __state: &crate::processor::ProcessingState<'_>
221            ) -> crate::processor::ProcessingResult
222            where
223                P: crate::processor::Processor,
224            {
225                match *self {
226                    #process_child_values_arms
227                }
228
229                Ok(())
230            }
231        }
232    }))
233}
234
235#[derive(Default)]
236struct TypeAttrs {
237    process_func: Option<String>,
238    value_type: Vec<String>,
239    /// The default trim value for the container.
240    ///
241    /// If `trim` is specified on the container all fields of the container,
242    /// will default to this value for `trim`.
243    trim: Option<bool>,
244    /// The default pii value for the container.
245    ///
246    /// If `pii` is specified on the container, all fields of the container
247    /// will default to this value for `pii`.
248    pii: Option<Pii>,
249}
250
251impl TypeAttrs {
252    fn process_func_call_tokens(&self) -> TokenStream {
253        if let Some(ref func_name) = self.process_func {
254            let func_name = Ident::new(func_name, Span::call_site());
255            quote! {
256                __processor.#func_name(self, __meta, __state)?;
257            }
258        } else {
259            quote! {
260                self.process_child_values(__processor, __state)?;
261            }
262        }
263    }
264}
265
266fn parse_type_attributes(s: &synstructure::Structure<'_>) -> syn::Result<TypeAttrs> {
267    let mut rv = TypeAttrs::default();
268
269    for attr in &s.ast().attrs {
270        if !attr.path().is_ident("metastructure") {
271            continue;
272        }
273
274        attr.parse_nested_meta(|meta| {
275            let ident = meta.path.require_ident()?;
276
277            if ident == "process_func" {
278                let s = meta.value()?.parse::<LitStr>()?;
279                rv.process_func = Some(s.value());
280            } else if ident == "value_type" {
281                let s = meta.value()?.parse::<LitStr>()?;
282                rv.value_type.push(s.value());
283            } else if ident == "trim" {
284                let s = meta.value()?.parse::<LitBool>()?;
285                rv.trim = Some(s.value());
286            } else if ident == "pii" {
287                let s = meta.value()?.parse::<LitStr>()?;
288                rv.pii = parse_pii_value(s, &meta)?;
289            } else {
290                // Ignore other attributes used by `relay-protocol-derive`.
291                if !meta.input.peek(syn::Token![,]) {
292                    let _ = meta.value()?.parse::<Lit>()?;
293                }
294            }
295
296            Ok(())
297        })?;
298    }
299
300    Ok(rv)
301}
302
303#[derive(Clone, Debug)]
304enum Pii {
305    True,
306    False,
307    Maybe,
308    Dynamic(ExprPath),
309}
310
311impl Pii {
312    fn as_tokens(&self) -> TokenStream {
313        match self {
314            Pii::True => quote!(crate::processor::PiiMode::Static(
315                crate::processor::Pii::True
316            )),
317            Pii::False => quote!(crate::processor::PiiMode::Static(
318                crate::processor::Pii::False
319            )),
320            Pii::Maybe => quote!(crate::processor::PiiMode::Static(
321                crate::processor::Pii::Maybe
322            )),
323            Pii::Dynamic(fun) => quote!(crate::processor::PiiMode::Dynamic(#fun)),
324        }
325    }
326}
327
328#[derive(Default)]
329struct FieldAttrs {
330    additional_properties: bool,
331    omit_from_schema: bool,
332    field_name: String,
333    flatten: bool,
334    required: Option<bool>,
335    nonempty: Option<bool>,
336    trim_whitespace: Option<bool>,
337    pii: Option<Pii>,
338    retain: bool,
339    characters: Option<TokenStream>,
340    max_chars: Option<TokenStream>,
341    max_chars_allowance: Option<TokenStream>,
342    max_depth: Option<TokenStream>,
343    max_bytes: Option<TokenStream>,
344    trim: Option<bool>,
345}
346
347impl FieldAttrs {
348    fn as_tokens(
349        &self,
350        type_attrs: &TypeAttrs,
351        inherit_from_field_attrs: Option<TokenStream>,
352    ) -> TokenStream {
353        let field_name = &self.field_name;
354
355        if self.required.is_none() && self.nonempty.is_some() {
356            panic!(
357                "`required` has to be explicitly set to \"true\" or \"false\" if `nonempty` is used."
358            );
359        }
360        let required = if let Some(required) = self.required {
361            quote!(#required)
362        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
363            quote!(#parent_attrs.required)
364        } else {
365            quote!(false)
366        };
367
368        let nonempty = if let Some(nonempty) = self.nonempty {
369            quote!(#nonempty)
370        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
371            quote!(#parent_attrs.nonempty)
372        } else {
373            quote!(false)
374        };
375
376        let trim_whitespace = if let Some(trim_whitespace) = self.trim_whitespace {
377            quote!(#trim_whitespace)
378        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
379            quote!(#parent_attrs.trim_whitespace)
380        } else {
381            quote!(false)
382        };
383
384        let pii = if let Some(pii) = self.pii.as_ref().or(type_attrs.pii.as_ref()) {
385            pii.as_tokens()
386        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
387            quote!(#parent_attrs.pii)
388        } else {
389            quote!(crate::processor::PiiMode::Static(
390                crate::processor::Pii::False
391            ))
392        };
393
394        let trim = if let Some(trim) = self.trim.or(type_attrs.trim) {
395            quote!(#trim)
396        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
397            quote!(#parent_attrs.trim)
398        } else {
399            quote!(true)
400        };
401
402        let retain = self.retain;
403
404        let max_chars = if let Some(ref max_chars) = self.max_chars {
405            quote!(Some(#max_chars))
406        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
407            quote!(#parent_attrs.max_chars)
408        } else {
409            quote!(None)
410        };
411
412        let max_chars_allowance = if let Some(ref max_chars_allowance) = self.max_chars_allowance {
413            quote!(#max_chars_allowance)
414        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
415            quote!(#parent_attrs.max_chars_allowance)
416        } else {
417            quote!(0)
418        };
419
420        let max_depth = if let Some(ref max_depth) = self.max_depth {
421            quote!(Some(#max_depth))
422        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
423            quote!(#parent_attrs.max_depth)
424        } else {
425            quote!(None)
426        };
427
428        let max_bytes = if let Some(ref max_bytes) = self.max_bytes {
429            quote!(Some(#max_bytes))
430        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
431            quote!(#parent_attrs.max_bytes)
432        } else {
433            quote!(None)
434        };
435
436        let characters = if let Some(ref characters) = self.characters {
437            quote!(Some(#characters))
438        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
439            quote!(#parent_attrs.characters)
440        } else {
441            quote!(None)
442        };
443
444        quote!({
445            crate::processor::FieldAttrs {
446                name: Some(#field_name),
447                required: #required,
448                nonempty: #nonempty,
449                trim_whitespace: #trim_whitespace,
450                max_chars: #max_chars,
451                max_chars_allowance: #max_chars_allowance,
452                characters: #characters,
453                max_depth: #max_depth,
454                max_bytes: #max_bytes,
455                pii: #pii,
456                retain: #retain,
457                trim: #trim,
458            }
459        })
460    }
461}
462
463fn parse_field_attributes(
464    index: usize,
465    bi_ast: &syn::Field,
466    is_tuple_struct: &mut bool,
467) -> syn::Result<FieldAttrs> {
468    if bi_ast.ident.is_none() {
469        *is_tuple_struct = true;
470    } else if *is_tuple_struct {
471        panic!("invalid tuple struct");
472    }
473
474    let mut rv = FieldAttrs {
475        field_name: bi_ast
476            .ident
477            .as_ref()
478            .map(ToString::to_string)
479            .unwrap_or_else(|| index.to_string()),
480        ..Default::default()
481    };
482
483    for attr in &bi_ast.attrs {
484        if !attr.path().is_ident("metastructure") {
485            continue;
486        }
487
488        attr.parse_nested_meta(|meta| {
489            let ident = meta.path.require_ident()?;
490
491            if ident == "additional_properties" {
492                rv.additional_properties = true;
493            } else if ident == "omit_from_schema" {
494                rv.omit_from_schema = true;
495            } else if ident == "field" {
496                let s = meta.value()?.parse::<LitStr>()?;
497                rv.field_name = s.value();
498            } else if ident == "flatten" {
499                rv.flatten = true;
500            } else if ident == "required" {
501                let s = meta.value()?.parse::<LitBool>()?;
502                rv.required = Some(s.value());
503            } else if ident == "nonempty" {
504                let s = meta.value()?.parse::<LitBool>()?;
505                rv.nonempty = Some(s.value());
506            } else if ident == "trim_whitespace" {
507                let s = meta.value()?.parse::<LitBool>()?;
508                rv.trim_whitespace = Some(s.value());
509            } else if ident == "allow_chars" || ident == "deny_chars" {
510                if rv.characters.is_some() {
511                    return Err(meta.error("allow_chars and deny_chars are mutually exclusive"));
512                }
513                let s = meta.value()?.parse::<LitStr>()?;
514                rv.characters = Some(parse_character_set(ident, &s.value()));
515            } else if ident == "max_chars" {
516                let s = meta.value()?.parse::<LitInt>()?;
517                rv.max_chars = Some(quote!(#s));
518            } else if ident == "max_chars_allowance" {
519                let s = meta.value()?.parse::<LitInt>()?;
520                rv.max_chars_allowance = Some(quote!(#s));
521            } else if ident == "max_depth" {
522                let s = meta.value()?.parse::<LitInt>()?;
523                rv.max_depth = Some(quote!(#s));
524            } else if ident == "max_bytes" {
525                let s = meta.value()?.parse::<LitInt>()?;
526                rv.max_bytes = Some(quote!(#s));
527            } else if ident == "pii" {
528                let s = meta.value()?.parse::<LitStr>()?;
529                rv.pii = parse_pii_value(s, &meta)?;
530            } else if ident == "retain" {
531                let s = meta.value()?.parse::<LitBool>()?;
532                rv.retain = s.value();
533            } else if ident == "trim" {
534                let s = meta.value()?.parse::<LitBool>()?;
535                rv.trim = Some(s.value());
536            } else if ident == "legacy_alias" || ident == "skip_serialization" {
537                let _ = meta.value()?.parse::<Lit>()?;
538            } else {
539                return Err(meta.error("Unknown argument"));
540            }
541
542            Ok(())
543        })?;
544    }
545
546    Ok(rv)
547}
548
549fn is_newtype(variant: &synstructure::VariantInfo) -> bool {
550    variant.bindings().len() == 1 && variant.bindings()[0].ast().ident.is_none()
551}
552
553fn parse_character_set(ident: &Ident, value: &str) -> TokenStream {
554    #[derive(Clone, Copy)]
555    enum State {
556        Blank,
557        OpenRange(char),
558        MidRange(char),
559    }
560
561    let mut state = State::Blank;
562    let mut ranges = Vec::new();
563
564    for c in value.chars() {
565        match (state, c) {
566            (State::Blank, a) => state = State::OpenRange(a),
567            (State::OpenRange(a), '-') => state = State::MidRange(a),
568            (State::OpenRange(a), c) => {
569                state = State::OpenRange(c);
570                ranges.push(quote!(#a..=#a));
571            }
572            (State::MidRange(a), b) => {
573                ranges.push(quote!(#a..=#b));
574                state = State::Blank;
575            }
576        }
577    }
578
579    match state {
580        State::OpenRange(a) => ranges.push(quote!(#a..=#a)),
581        State::MidRange(a) => {
582            ranges.push(quote!(#a..=#a));
583            ranges.push(quote!('-'..='-'));
584        }
585        State::Blank => {}
586    }
587
588    let is_negative = ident == "deny_chars";
589
590    quote! {
591        crate::processor::CharacterSet {
592            char_is_valid: |c: char| -> bool {
593                match c {
594                    #((#ranges) => !#is_negative,)*
595                    _ => #is_negative,
596                }
597            },
598            ranges: &[ #(#ranges,)* ],
599            is_negative: #is_negative,
600        }
601    }
602}
603
604fn parse_pii_value(value: LitStr, meta: &ParseNestedMeta) -> syn::Result<Option<Pii>> {
605    Ok(Some(match value.value().as_str() {
606        "true" => Pii::True,
607        "false" => Pii::False,
608        "maybe" => Pii::Maybe,
609        _ => Pii::Dynamic(value.parse().map_err(|_| {
610            meta.error("Expected one of `true`, `false`, `maybe`, or a function name")
611        })?),
612    }))
613}