Skip to main content

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::parse::ParseStream;
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 variant_attrs = parse_variant_attributes(variant.ast().attrs)?;
48            let mut field_attrs = parse_field_attributes(0, bi.ast(), &mut true)?;
49            // `fallback_variant` is defined on the variant, so is `retain`, but downstream code
50            // expects `retain` to be set as a field attribute.
51            field_attrs.retain |= variant_attrs.retain;
52            let field_attrs_tokens = field_attrs.as_tokens(&type_attrs, Some(quote!(parent_attrs)));
53
54            let process_value = if variant_attrs.fallback_variant {
55                quote! {
56                    __processor.process_other(#ident, &__state)?;
57                }
58            } else {
59                quote! {
60                    ::relay_event_schema::processor::ProcessValue::process_value(
61                        #ident,
62                        __meta,
63                        __processor,
64                        &__state
65                    )?;
66
67                }
68            };
69            Ok(quote! {
70                let parent_attrs = __state.attrs();
71                let attrs = #field_attrs_tokens;
72                let __state = &__state.enter_nothing(
73                    Some(::std::borrow::Cow::Owned(attrs))
74                );
75
76                // This is a copy of `funcs::process_value`, due to ownership issues. In particular
77                // we want to pass the same meta twice.
78                //
79                // NOTE: Handling for ProcessingAction is slightly different (early-return). This
80                // should be fine though.
81                let action = __processor.before_process(
82                    Some(&*#ident),
83                    __meta,
84                    &__state
85                )?;
86
87                #process_value
88
89                __processor.after_process(
90                    Some(#ident),
91                    __meta,
92                    &__state
93                )?;
94            })
95        } else {
96            Ok(quote!())
97        }
98    })?;
99
100    let process_child_values_arms = s.try_each_variant(|variant| {
101        let mut is_tuple_struct = false;
102
103        if is_newtype(variant) {
104            // `process_child_values` has to be a noop because otherwise we recurse into the
105            // subtree twice due to the weird `process_value` impl
106
107            return Ok(quote!());
108        }
109
110        let mut body = TokenStream::new();
111        for (index, bi) in variant.bindings().iter().enumerate() {
112            let field_attrs = parse_field_attributes(index, bi.ast(), &mut is_tuple_struct)?;
113            let ident = &bi.binding;
114            let field_attrs_name = Ident::new(&format!("FIELD_ATTRS_{index}"), Span::call_site());
115            let field_name = field_attrs.field_name.clone();
116
117            let field_attrs_tokens = field_attrs.as_tokens(&type_attrs, None);
118
119            (quote! {
120                static #field_attrs_name: ::relay_event_schema::processor::FieldAttrs = #field_attrs_tokens;
121            })
122            .to_tokens(&mut body);
123
124            let enter_state = if field_attrs.additional_properties {
125                if is_tuple_struct {
126                    panic!("additional_properties not allowed in tuple struct");
127                }
128
129                quote! {
130                    __state.enter_nothing(Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)))
131                }
132            } else if field_attrs.flatten {
133                quote! {
134                    __state.enter_nothing(Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)))
135                }
136            } else if is_tuple_struct {
137                quote! {
138                    __state.enter_index(
139                        #index,
140                        Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)),
141                        ::relay_event_schema::processor::ValueType::for_field(#ident),
142                    )
143                }
144            } else {
145                quote! {
146                    __state.enter_borrowed(
147                        #field_name,
148                        Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)),
149                        ::relay_event_schema::processor::ValueType::for_field(#ident),
150                    )
151                }
152            };
153
154            if field_attrs.additional_properties {
155                (quote! {
156                    __processor.process_other(#ident, &#enter_state)?;
157                })
158                .to_tokens(&mut body);
159            } else if field_attrs.flatten {
160                (quote! {
161                    ::relay_event_schema::processor::ProcessValue::process_child_values(
162                        #ident,
163                        __processor,
164                       &#enter_state
165                    )?;
166                })
167                .to_tokens(&mut body);
168            } else {
169                (quote! {
170                    ::relay_event_schema::processor::process_value(#ident, __processor, &#enter_state)?;
171                })
172                .to_tokens(&mut body);
173            }
174        }
175
176        Ok(quote!({ #body }))
177    })?;
178
179    let _ = s.bind_with(|_bi| synstructure::BindStyle::Ref);
180
181    let value_type_arms = s.each_variant(|variant| {
182        if !type_attrs.value_type.is_empty() {
183            let value_names = type_attrs
184                .value_type
185                .iter()
186                .map(|value_name| Ident::new(value_name, Span::call_site()));
187            quote! {
188                // enumset produces a deprecation warning because it thinks we use its internals
189                // directly, but we do actually use the macro
190                #[allow(deprecated)]
191                {
192                    enumset::enum_set!( #(::relay_event_schema::processor::ValueType::#value_names)|* )
193                }
194            }
195        } else if is_newtype(variant) {
196            let bi = &variant.bindings()[0];
197            let ident = &bi.binding;
198            quote!(::relay_event_schema::processor::ProcessValue::value_type(#ident))
199        } else {
200            quote!(::relay_event_schema::processor::EnumSet::empty())
201        }
202    });
203
204    Ok(s.gen_impl(quote! {
205        #[automatically_derived]
206        gen impl ::relay_event_schema::processor::ProcessValue for @Self {
207            fn value_type(&self) -> ::relay_event_schema::processor::EnumSet<::relay_event_schema::processor::ValueType> {
208                match *self {
209                    #value_type_arms
210                }
211            }
212
213            fn process_value<P>(
214                &mut self,
215                __meta: &mut relay_protocol::Meta,
216                __processor: &mut P,
217                __state: &::relay_event_schema::processor::ProcessingState<'_>,
218            ) -> ::relay_event_schema::processor::ProcessingResult
219            where
220                P: ::relay_event_schema::processor::Processor,
221            {
222                #process_func_call_tokens;
223                match *self {
224                    #process_value_arms
225                }
226
227                Ok(())
228            }
229
230            #[inline]
231            fn process_child_values<P>(
232                &mut self,
233                __processor: &mut P,
234                __state: &::relay_event_schema::processor::ProcessingState<'_>
235            ) -> ::relay_event_schema::processor::ProcessingResult
236            where
237                P: ::relay_event_schema::processor::Processor,
238            {
239                match *self {
240                    #process_child_values_arms
241                }
242
243                Ok(())
244            }
245        }
246    }))
247}
248
249#[derive(Default)]
250struct TypeAttrs {
251    process_func: Option<String>,
252    value_type: Vec<String>,
253    /// The default trim value for the container.
254    ///
255    /// If `trim` is specified on the container all fields of the container,
256    /// will default to this value for `trim`.
257    trim: Option<bool>,
258    /// The default pii value for the container.
259    ///
260    /// If `pii` is specified on the container, all fields of the container
261    /// will default to this value for `pii`.
262    pii: Option<Pii>,
263}
264
265impl TypeAttrs {
266    fn process_func_call_tokens(&self) -> TokenStream {
267        if let Some(ref func_name) = self.process_func {
268            let func_name = Ident::new(func_name, Span::call_site());
269            quote! {
270                __processor.#func_name(self, __meta, __state)?;
271            }
272        } else {
273            quote! {
274                self.process_child_values(__processor, __state)?;
275            }
276        }
277    }
278}
279
280fn parse_type_attributes(s: &synstructure::Structure<'_>) -> syn::Result<TypeAttrs> {
281    let mut rv = TypeAttrs::default();
282
283    for attr in &s.ast().attrs {
284        if !attr.path().is_ident("metastructure") {
285            continue;
286        }
287
288        attr.parse_nested_meta(|meta| {
289            let ident = meta.path.require_ident()?;
290
291            if ident == "process_func" {
292                let s = meta.value()?.parse::<LitStr>()?;
293                rv.process_func = Some(s.value());
294            } else if ident == "value_type" {
295                let s = meta.value()?.parse::<LitStr>()?;
296                rv.value_type.push(s.value());
297            } else if ident == "trim" {
298                let s = meta.value()?.parse::<LitBool>()?;
299                rv.trim = Some(s.value());
300            } else if ident == "pii" {
301                rv.pii = Some(meta.value()?.parse()?);
302            } else {
303                // Ignore other attributes used by `relay-protocol-derive`.
304                if !meta.input.peek(syn::Token![,]) {
305                    let _ = meta.value()?.parse::<Lit>()?;
306                }
307            }
308
309            Ok(())
310        })?;
311    }
312
313    Ok(rv)
314}
315
316#[derive(Clone, Debug)]
317enum Pii {
318    True,
319    False,
320    Maybe,
321    Dynamic(ExprPath),
322}
323
324impl syn::parse::Parse for Pii {
325    fn parse(input: ParseStream) -> syn::Result<Self> {
326        let head = input.fork();
327        let value = input.parse::<LitStr>()?;
328        let pii = match value.value().as_str() {
329            "true" => Self::True,
330            "false" => Self::False,
331            "maybe" => Self::Maybe,
332            _ => Self::Dynamic(value.parse().map_err(|_| {
333                head.error("Expected one of `true`, `false`, `maybe`, or a function name")
334            })?),
335        };
336        Ok(pii)
337    }
338}
339
340impl Pii {
341    fn as_tokens(&self) -> TokenStream {
342        match self {
343            Self::True => quote!(::relay_event_schema::processor::PiiMode::Static(
344                ::relay_event_schema::processor::Pii::True
345            )),
346            Self::False => quote!(::relay_event_schema::processor::PiiMode::Static(
347                ::relay_event_schema::processor::Pii::False
348            )),
349            Self::Maybe => quote!(::relay_event_schema::processor::PiiMode::Static(
350                ::relay_event_schema::processor::Pii::Maybe
351            )),
352            Self::Dynamic(fun) => quote!(::relay_event_schema::processor::PiiMode::Dynamic(#fun)),
353        }
354    }
355}
356
357#[derive(Clone, Debug)]
358enum Size {
359    Static(usize),
360    Dynamic(ExprPath),
361}
362
363impl syn::parse::Parse for Size {
364    fn parse(input: ParseStream) -> syn::Result<Self> {
365        if input.peek(LitInt) {
366            let lit = input.parse::<LitInt>()?;
367            return Ok(Self::Static(lit.base10_parse()?));
368        }
369
370        let head = input.fork();
371        let path = input
372            .parse::<LitStr>()?
373            .parse()
374            .map_err(|_| head.error("Expected a function name"))?;
375        Ok(Self::Dynamic(path))
376    }
377}
378
379impl Size {
380    fn as_tokens(&self) -> TokenStream {
381        match self {
382            Self::Static(n) => quote!(::relay_event_schema::processor::SizeMode::Static(Some(#n))),
383            Self::Dynamic(fun) => quote!(::relay_event_schema::processor::SizeMode::Dynamic(#fun)),
384        }
385    }
386}
387
388#[derive(Default, Debug)]
389struct VariantAttrs {
390    fallback_variant: bool,
391    retain: bool,
392}
393
394fn parse_variant_attributes(attrs: &[syn::Attribute]) -> syn::Result<VariantAttrs> {
395    let mut rv = VariantAttrs::default();
396    for attr in attrs {
397        if !attr.path().is_ident("metastructure") {
398            continue;
399        }
400
401        attr.parse_nested_meta(|meta| {
402            let ident = meta.path.require_ident()?;
403
404            if ident == "fallback_variant" {
405                rv.fallback_variant = true;
406            } else if ident == "retain" {
407                let s = meta.value()?.parse::<LitBool>()?;
408                rv.retain = s.value();
409            } else {
410                // Ignore other attributes used by `FromValue`/`IntoValue` derive macros.
411                if let Ok(v) = meta.value() {
412                    let _ = v.parse::<Lit>()?;
413                }
414            }
415
416            Ok(())
417        })?;
418    }
419
420    Ok(rv)
421}
422
423#[derive(Default, Debug)]
424struct FieldAttrs {
425    additional_properties: bool,
426    omit_from_schema: bool,
427    field_name: String,
428    flatten: bool,
429    required: Option<bool>,
430    nonempty: Option<bool>,
431    trim_whitespace: Option<bool>,
432    pii: Option<Pii>,
433    retain: bool,
434    characters: Option<TokenStream>,
435    max_chars: Option<Size>,
436    max_chars_allowance: Option<TokenStream>,
437    max_depth: Option<TokenStream>,
438    max_bytes: Option<Size>,
439    bytes_size: Option<Size>,
440    trim: Option<bool>,
441}
442
443impl FieldAttrs {
444    fn as_tokens(
445        &self,
446        type_attrs: &TypeAttrs,
447        inherit_from_field_attrs: Option<TokenStream>,
448    ) -> TokenStream {
449        let field_name = &self.field_name;
450
451        if self.required.is_none() && self.nonempty.is_some() {
452            panic!(
453                "`required` has to be explicitly set to \"true\" or \"false\" if `nonempty` is used."
454            );
455        }
456        let required = if let Some(required) = self.required {
457            quote!(#required)
458        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
459            quote!(#parent_attrs.required)
460        } else {
461            quote!(false)
462        };
463
464        let nonempty = if let Some(nonempty) = self.nonempty {
465            quote!(#nonempty)
466        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
467            quote!(#parent_attrs.nonempty)
468        } else {
469            quote!(false)
470        };
471
472        let trim_whitespace = if let Some(trim_whitespace) = self.trim_whitespace {
473            quote!(#trim_whitespace)
474        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
475            quote!(#parent_attrs.trim_whitespace)
476        } else {
477            quote!(false)
478        };
479
480        let pii = if let Some(pii) = self.pii.as_ref().or(type_attrs.pii.as_ref()) {
481            pii.as_tokens()
482        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
483            quote!(#parent_attrs.pii)
484        } else {
485            quote!(::relay_event_schema::processor::PiiMode::Static(
486                ::relay_event_schema::processor::Pii::False
487            ))
488        };
489
490        let trim = if let Some(trim) = self.trim.or(type_attrs.trim) {
491            quote!(#trim)
492        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
493            quote!(#parent_attrs.trim)
494        } else {
495            quote!(true)
496        };
497
498        let retain = self.retain;
499
500        let max_chars = if let Some(ref max_chars) = self.max_chars {
501            max_chars.as_tokens()
502        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
503            quote!(#parent_attrs.max_chars)
504        } else {
505            quote!(::relay_event_schema::processor::SizeMode::Static(None))
506        };
507
508        let max_chars_allowance = if let Some(ref max_chars_allowance) = self.max_chars_allowance {
509            quote!(#max_chars_allowance)
510        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
511            quote!(#parent_attrs.max_chars_allowance)
512        } else {
513            quote!(0)
514        };
515
516        let max_depth = if let Some(ref max_depth) = self.max_depth {
517            quote!(Some(#max_depth))
518        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
519            quote!(#parent_attrs.max_depth)
520        } else {
521            quote!(None)
522        };
523
524        let max_bytes = if let Some(ref max_bytes) = self.max_bytes {
525            max_bytes.as_tokens()
526        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
527            quote!(#parent_attrs.max_bytes)
528        } else {
529            quote!(::relay_event_schema::processor::SizeMode::Static(None))
530        };
531
532        let bytes_size = if let Some(ref bytes_size) = self.bytes_size {
533            bytes_size.as_tokens()
534        } else {
535            quote!(::relay_event_schema::processor::SizeMode::Static(None))
536        };
537
538        let characters = if let Some(ref characters) = self.characters {
539            quote!(Some(#characters))
540        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
541            quote!(#parent_attrs.characters)
542        } else {
543            quote!(None)
544        };
545
546        quote!({
547            ::relay_event_schema::processor::FieldAttrs {
548                name: Some(#field_name),
549                required: #required,
550                nonempty: #nonempty,
551                trim_whitespace: #trim_whitespace,
552                max_chars: #max_chars,
553                max_chars_allowance: #max_chars_allowance,
554                characters: #characters,
555                max_depth: #max_depth,
556                max_bytes: #max_bytes,
557                bytes_size: #bytes_size,
558                pii: #pii,
559                retain: #retain,
560                trim: #trim,
561            }
562        })
563    }
564}
565
566fn parse_field_attributes(
567    index: usize,
568    bi_ast: &syn::Field,
569    is_tuple_struct: &mut bool,
570) -> syn::Result<FieldAttrs> {
571    if bi_ast.ident.is_none() {
572        *is_tuple_struct = true;
573    } else if *is_tuple_struct {
574        panic!("invalid tuple struct");
575    }
576
577    let mut rv = FieldAttrs {
578        field_name: bi_ast
579            .ident
580            .as_ref()
581            .map(ToString::to_string)
582            .unwrap_or_else(|| index.to_string()),
583        ..Default::default()
584    };
585
586    for attr in &bi_ast.attrs {
587        if !attr.path().is_ident("metastructure") {
588            continue;
589        }
590
591        attr.parse_nested_meta(|meta| {
592            let ident = meta.path.require_ident()?;
593
594            if ident == "additional_properties" {
595                rv.additional_properties = true;
596            } else if ident == "omit_from_schema" {
597                rv.omit_from_schema = true;
598            } else if ident == "field" {
599                let s = meta.value()?.parse::<LitStr>()?;
600                rv.field_name = s.value();
601            } else if ident == "flatten" {
602                rv.flatten = true;
603            } else if ident == "required" {
604                let s = meta.value()?.parse::<LitBool>()?;
605                rv.required = Some(s.value());
606            } else if ident == "nonempty" {
607                let s = meta.value()?.parse::<LitBool>()?;
608                rv.nonempty = Some(s.value());
609            } else if ident == "trim_whitespace" {
610                let s = meta.value()?.parse::<LitBool>()?;
611                rv.trim_whitespace = Some(s.value());
612            } else if ident == "allow_chars" || ident == "deny_chars" {
613                if rv.characters.is_some() {
614                    return Err(meta.error("allow_chars and deny_chars are mutually exclusive"));
615                }
616                let s = meta.value()?.parse::<LitStr>()?;
617                rv.characters = Some(parse_character_set(ident, &s.value()));
618            } else if ident == "max_chars" {
619                rv.max_chars = Some(meta.value()?.parse()?);
620            } else if ident == "max_chars_allowance" {
621                let s = meta.value()?.parse::<LitInt>()?;
622                rv.max_chars_allowance = Some(quote!(#s));
623            } else if ident == "max_depth" {
624                let s = meta.value()?.parse::<LitInt>()?;
625                rv.max_depth = Some(quote!(#s));
626            } else if ident == "max_bytes" {
627                rv.max_bytes = Some(meta.value()?.parse()?);
628            } else if ident == "bytes_size" {
629                rv.bytes_size = Some(meta.value()?.parse()?);
630            } else if ident == "pii" {
631                rv.pii = Some(meta.value()?.parse()?);
632            } else if ident == "retain" {
633                let s = meta.value()?.parse::<LitBool>()?;
634                rv.retain = s.value();
635            } else if ident == "trim" {
636                let s = meta.value()?.parse::<LitBool>()?;
637                rv.trim = Some(s.value());
638            } else if ident == "legacy_alias" || ident == "skip_serialization" {
639                let _ = meta.value()?.parse::<Lit>()?;
640            } else {
641                return Err(meta.error("Unknown argument"));
642            }
643
644            Ok(())
645        })?;
646    }
647
648    Ok(rv)
649}
650
651fn is_newtype(variant: &synstructure::VariantInfo) -> bool {
652    variant.bindings().len() == 1 && variant.bindings()[0].ast().ident.is_none()
653}
654
655fn parse_character_set(ident: &Ident, value: &str) -> TokenStream {
656    #[derive(Clone, Copy)]
657    enum State {
658        Blank,
659        OpenRange(char),
660        MidRange(char),
661    }
662
663    let mut state = State::Blank;
664    let mut ranges = Vec::new();
665
666    for c in value.chars() {
667        match (state, c) {
668            (State::Blank, a) => state = State::OpenRange(a),
669            (State::OpenRange(a), '-') => state = State::MidRange(a),
670            (State::OpenRange(a), c) => {
671                state = State::OpenRange(c);
672                ranges.push(quote!(#a..=#a));
673            }
674            (State::MidRange(a), b) => {
675                ranges.push(quote!(#a..=#b));
676                state = State::Blank;
677            }
678        }
679    }
680
681    match state {
682        State::OpenRange(a) => ranges.push(quote!(#a..=#a)),
683        State::MidRange(a) => {
684            ranges.push(quote!(#a..=#a));
685            ranges.push(quote!('-'..='-'));
686        }
687        State::Blank => {}
688    }
689
690    let is_negative = ident == "deny_chars";
691
692    quote! {
693        ::relay_event_schema::processor::CharacterSet {
694            char_is_valid: |c: char| -> bool {
695                match c {
696                    #((#ranges) => !#is_negative,)*
697                    _ => #is_negative,
698                }
699            },
700            ranges: &[ #(#ranges,)* ],
701            is_negative: #is_negative,
702        }
703    }
704}