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, Copy, Debug)]
358enum Required {
359    True,
360    False,
361    ValueOrMeta,
362}
363
364impl Required {
365    fn as_tokens(&self) -> TokenStream {
366        match self {
367            Self::True => quote!(::relay_event_schema::processor::Required::Value),
368            Self::False => quote!(::relay_event_schema::processor::Required::False),
369            Self::ValueOrMeta => quote!(::relay_event_schema::processor::Required::ValueOrMeta),
370        }
371    }
372}
373
374impl syn::parse::Parse for Required {
375    fn parse(input: ParseStream) -> syn::Result<Self> {
376        let head = input.fork();
377        if let Ok(bool) = input.parse::<LitBool>() {
378            return match bool.value() {
379                true => Ok(Self::True),
380                false => Ok(Self::False),
381            };
382        }
383
384        let input = head.fork();
385        let value = input.parse::<LitStr>()?;
386
387        match value.value().as_str() {
388            "value_or_meta" => Ok(Self::ValueOrMeta),
389            _ => Err(head.error(r#"Expected one of `true`, `false`, or `"value_or_meta"`"#)),
390        }
391    }
392}
393
394#[derive(Clone, Debug)]
395enum Size {
396    Static(usize),
397    Dynamic(ExprPath),
398}
399
400impl syn::parse::Parse for Size {
401    fn parse(input: ParseStream) -> syn::Result<Self> {
402        if input.peek(LitInt) {
403            let lit = input.parse::<LitInt>()?;
404            return Ok(Self::Static(lit.base10_parse()?));
405        }
406
407        let head = input.fork();
408        let path = input
409            .parse::<LitStr>()?
410            .parse()
411            .map_err(|_| head.error("Expected a function name"))?;
412        Ok(Self::Dynamic(path))
413    }
414}
415
416impl Size {
417    fn as_tokens(&self) -> TokenStream {
418        match self {
419            Self::Static(n) => quote!(::relay_event_schema::processor::SizeMode::Static(Some(#n))),
420            Self::Dynamic(fun) => quote!(::relay_event_schema::processor::SizeMode::Dynamic(#fun)),
421        }
422    }
423}
424
425#[derive(Default, Debug)]
426struct VariantAttrs {
427    fallback_variant: bool,
428    retain: bool,
429}
430
431fn parse_variant_attributes(attrs: &[syn::Attribute]) -> syn::Result<VariantAttrs> {
432    let mut rv = VariantAttrs::default();
433    for attr in attrs {
434        if !attr.path().is_ident("metastructure") {
435            continue;
436        }
437
438        attr.parse_nested_meta(|meta| {
439            let ident = meta.path.require_ident()?;
440
441            if ident == "fallback_variant" {
442                rv.fallback_variant = true;
443            } else if ident == "retain" {
444                let s = meta.value()?.parse::<LitBool>()?;
445                rv.retain = s.value();
446            } else {
447                // Ignore other attributes used by `FromValue`/`IntoValue` derive macros.
448                if let Ok(v) = meta.value() {
449                    let _ = v.parse::<Lit>()?;
450                }
451            }
452
453            Ok(())
454        })?;
455    }
456
457    Ok(rv)
458}
459
460#[derive(Default, Debug)]
461struct FieldAttrs {
462    additional_properties: bool,
463    omit_from_schema: bool,
464    field_name: String,
465    flatten: bool,
466    required: Option<Required>,
467    nonempty: Option<bool>,
468    trim_whitespace: Option<bool>,
469    pii: Option<Pii>,
470    retain: bool,
471    characters: Option<TokenStream>,
472    max_chars: Option<Size>,
473    max_chars_allowance: Option<TokenStream>,
474    max_depth: Option<TokenStream>,
475    max_bytes: Option<Size>,
476    bytes_size: Option<Size>,
477    trim: Option<bool>,
478}
479
480impl FieldAttrs {
481    fn as_tokens(
482        &self,
483        type_attrs: &TypeAttrs,
484        inherit_from_field_attrs: Option<TokenStream>,
485    ) -> TokenStream {
486        let field_name = &self.field_name;
487
488        if self.required.is_none() && self.nonempty.is_some() {
489            panic!(
490                "`required` has to be explicitly set to \"true\" or \"false\" if `nonempty` is used."
491            );
492        }
493        let required = if let Some(required) = self.required {
494            required.as_tokens()
495        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
496            quote!(#parent_attrs.required)
497        } else {
498            quote!(::relay_event_schema::processor::Required::False)
499        };
500
501        let nonempty = if let Some(nonempty) = self.nonempty {
502            quote!(#nonempty)
503        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
504            quote!(#parent_attrs.nonempty)
505        } else {
506            quote!(false)
507        };
508
509        let trim_whitespace = if let Some(trim_whitespace) = self.trim_whitespace {
510            quote!(#trim_whitespace)
511        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
512            quote!(#parent_attrs.trim_whitespace)
513        } else {
514            quote!(false)
515        };
516
517        let pii = if let Some(pii) = self.pii.as_ref().or(type_attrs.pii.as_ref()) {
518            pii.as_tokens()
519        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
520            quote!(#parent_attrs.pii)
521        } else {
522            quote!(::relay_event_schema::processor::PiiMode::Static(
523                ::relay_event_schema::processor::Pii::False
524            ))
525        };
526
527        let trim = if let Some(trim) = self.trim.or(type_attrs.trim) {
528            quote!(#trim)
529        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
530            quote!(#parent_attrs.trim)
531        } else {
532            quote!(true)
533        };
534
535        let retain = self.retain;
536
537        let max_chars = if let Some(ref max_chars) = self.max_chars {
538            max_chars.as_tokens()
539        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
540            quote!(#parent_attrs.max_chars)
541        } else {
542            quote!(::relay_event_schema::processor::SizeMode::Static(None))
543        };
544
545        let max_chars_allowance = if let Some(ref max_chars_allowance) = self.max_chars_allowance {
546            quote!(#max_chars_allowance)
547        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
548            quote!(#parent_attrs.max_chars_allowance)
549        } else {
550            quote!(0)
551        };
552
553        let max_depth = if let Some(ref max_depth) = self.max_depth {
554            quote!(Some(#max_depth))
555        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
556            quote!(#parent_attrs.max_depth)
557        } else {
558            quote!(None)
559        };
560
561        let max_bytes = if let Some(ref max_bytes) = self.max_bytes {
562            max_bytes.as_tokens()
563        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
564            quote!(#parent_attrs.max_bytes)
565        } else {
566            quote!(::relay_event_schema::processor::SizeMode::Static(None))
567        };
568
569        let bytes_size = if let Some(ref bytes_size) = self.bytes_size {
570            bytes_size.as_tokens()
571        } else {
572            quote!(::relay_event_schema::processor::SizeMode::Static(None))
573        };
574
575        let characters = if let Some(ref characters) = self.characters {
576            quote!(Some(#characters))
577        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
578            quote!(#parent_attrs.characters)
579        } else {
580            quote!(None)
581        };
582
583        quote!({
584            ::relay_event_schema::processor::FieldAttrs {
585                name: Some(#field_name),
586                required: #required,
587                nonempty: #nonempty,
588                trim_whitespace: #trim_whitespace,
589                max_chars: #max_chars,
590                max_chars_allowance: #max_chars_allowance,
591                characters: #characters,
592                max_depth: #max_depth,
593                max_bytes: #max_bytes,
594                bytes_size: #bytes_size,
595                pii: #pii,
596                retain: #retain,
597                trim: #trim,
598            }
599        })
600    }
601}
602
603fn parse_field_attributes(
604    index: usize,
605    bi_ast: &syn::Field,
606    is_tuple_struct: &mut bool,
607) -> syn::Result<FieldAttrs> {
608    if bi_ast.ident.is_none() {
609        *is_tuple_struct = true;
610    } else if *is_tuple_struct {
611        panic!("invalid tuple struct");
612    }
613
614    let mut rv = FieldAttrs {
615        field_name: bi_ast
616            .ident
617            .as_ref()
618            .map(ToString::to_string)
619            .unwrap_or_else(|| index.to_string()),
620        ..Default::default()
621    };
622
623    for attr in &bi_ast.attrs {
624        if !attr.path().is_ident("metastructure") {
625            continue;
626        }
627
628        attr.parse_nested_meta(|meta| {
629            let ident = meta.path.require_ident()?;
630
631            if ident == "additional_properties" {
632                rv.additional_properties = true;
633            } else if ident == "omit_from_schema" {
634                rv.omit_from_schema = true;
635            } else if ident == "field" {
636                let s = meta.value()?.parse::<LitStr>()?;
637                rv.field_name = s.value();
638            } else if ident == "flatten" {
639                rv.flatten = true;
640            } else if ident == "required" {
641                rv.required = Some(meta.value()?.parse::<Required>()?);
642            } else if ident == "nonempty" {
643                let s = meta.value()?.parse::<LitBool>()?;
644                rv.nonempty = Some(s.value());
645            } else if ident == "trim_whitespace" {
646                let s = meta.value()?.parse::<LitBool>()?;
647                rv.trim_whitespace = Some(s.value());
648            } else if ident == "allow_chars" || ident == "deny_chars" {
649                if rv.characters.is_some() {
650                    return Err(meta.error("allow_chars and deny_chars are mutually exclusive"));
651                }
652                let s = meta.value()?.parse::<LitStr>()?;
653                rv.characters = Some(parse_character_set(ident, &s.value()));
654            } else if ident == "max_chars" {
655                rv.max_chars = Some(meta.value()?.parse()?);
656            } else if ident == "max_chars_allowance" {
657                let s = meta.value()?.parse::<LitInt>()?;
658                rv.max_chars_allowance = Some(quote!(#s));
659            } else if ident == "max_depth" {
660                let s = meta.value()?.parse::<LitInt>()?;
661                rv.max_depth = Some(quote!(#s));
662            } else if ident == "max_bytes" {
663                rv.max_bytes = Some(meta.value()?.parse()?);
664            } else if ident == "bytes_size" {
665                rv.bytes_size = Some(meta.value()?.parse()?);
666            } else if ident == "pii" {
667                rv.pii = Some(meta.value()?.parse()?);
668            } else if ident == "retain" {
669                let s = meta.value()?.parse::<LitBool>()?;
670                rv.retain = s.value();
671            } else if ident == "trim" {
672                let s = meta.value()?.parse::<LitBool>()?;
673                rv.trim = Some(s.value());
674            } else if ident == "legacy_alias" || ident == "skip_serialization" {
675                let _ = meta.value()?.parse::<Lit>()?;
676            } else {
677                return Err(meta.error("Unknown argument"));
678            }
679
680            Ok(())
681        })?;
682    }
683
684    Ok(rv)
685}
686
687fn is_newtype(variant: &synstructure::VariantInfo) -> bool {
688    variant.bindings().len() == 1 && variant.bindings()[0].ast().ident.is_none()
689}
690
691fn parse_character_set(ident: &Ident, value: &str) -> TokenStream {
692    #[derive(Clone, Copy)]
693    enum State {
694        Blank,
695        OpenRange(char),
696        MidRange(char),
697    }
698
699    let mut state = State::Blank;
700    let mut ranges = Vec::new();
701
702    for c in value.chars() {
703        match (state, c) {
704            (State::Blank, a) => state = State::OpenRange(a),
705            (State::OpenRange(a), '-') => state = State::MidRange(a),
706            (State::OpenRange(a), c) => {
707                state = State::OpenRange(c);
708                ranges.push(quote!(#a..=#a));
709            }
710            (State::MidRange(a), b) => {
711                ranges.push(quote!(#a..=#b));
712                state = State::Blank;
713            }
714        }
715    }
716
717    match state {
718        State::OpenRange(a) => ranges.push(quote!(#a..=#a)),
719        State::MidRange(a) => {
720            ranges.push(quote!(#a..=#a));
721            ranges.push(quote!('-'..='-'));
722        }
723        State::Blank => {}
724    }
725
726    let is_negative = ident == "deny_chars";
727
728    quote! {
729        ::relay_event_schema::processor::CharacterSet {
730            char_is_valid: |c: char| -> bool {
731                match c {
732                    #((#ranges) => !#is_negative,)*
733                    _ => #is_negative,
734                }
735            },
736            ranges: &[ #(#ranges,)* ],
737            is_negative: #is_negative,
738        }
739    }
740}