relay_event_normalization/
schema.rs

1use relay_event_schema::processor::{
2    ProcessValue, ProcessingAction, ProcessingResult, ProcessingState, Processor,
3};
4use relay_protocol::{Annotated, Array, Empty, Error, ErrorKind, Meta, Object, Value};
5use smallvec::SmallVec;
6
7/// Mode how `required` values should be validated in a [`SchemaProcessor`].
8#[derive(Debug, Default)]
9pub enum RequiredMode {
10    /// The default mode, which only deletes the value and leaves a remark.
11    ///
12    /// # Examples:
13    ///
14    /// ```
15    /// # use relay_event_schema::processor::{self, ProcessingState, ProcessValue};
16    /// # use relay_protocol::{Annotated, FromValue, IntoValue, Empty};
17    /// # use relay_event_normalization::{RequiredMode, SchemaProcessor};
18    /// #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
19    /// struct Item {
20    ///     #[metastructure(required = true)]
21    ///     name: Annotated<String>,
22    /// }
23    ///
24    /// let mut item = Annotated::new(Item {
25    ///     name: Annotated::empty(),
26    /// });
27    ///
28    /// let mut processor = SchemaProcessor::new().with_required(Default::default());
29    /// processor::process_value(&mut item, &mut processor, ProcessingState::root()).unwrap();
30    ///
31    /// let name = &item.value().unwrap().name;
32    /// assert!(name.value().is_none());
33    /// assert!(name.meta().has_errors());
34    /// ```
35    #[default]
36    DeleteValue,
37    /// Instead of removing the value, the entire container containing the value is removed.
38    ///
39    /// # Examples:
40    ///
41    /// ```
42    /// # use relay_event_schema::processor::{self, ProcessingState, ProcessValue};
43    /// # use relay_protocol::{Annotated, FromValue, IntoValue, Empty};
44    /// # use relay_event_normalization::{RequiredMode, SchemaProcessor};
45    /// #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
46    /// struct Item {
47    ///     #[metastructure(required = true)]
48    ///     name: Annotated<String>,
49    /// }
50    ///
51    /// let mut item = Annotated::new(Item {
52    ///     name: Annotated::empty(),
53    /// });
54    ///
55    /// let mut processor = SchemaProcessor::new().with_required(RequiredMode::DeleteParent);
56    /// processor::process_value(&mut item, &mut processor, ProcessingState::root()).unwrap();
57    ///
58    /// assert!(item.meta().has_errors());
59    /// assert!(item.value().is_none());
60    /// ```
61    DeleteParent,
62}
63
64/// Validates constraints such as empty strings or arrays and invalid characters.
65#[derive(Debug, Default)]
66pub struct SchemaProcessor {
67    required: RequiredMode,
68    verbose_errors: bool,
69    stack: SmallVec<[SchemaState; 10]>,
70}
71
72impl SchemaProcessor {
73    /// Creates a new [`SchemaProcessor`].
74    pub fn new() -> Self {
75        Default::default()
76    }
77
78    /// Configures how `required` values should be validated.
79    pub fn with_required(mut self, mode: RequiredMode) -> Self {
80        self.required = mode;
81        self
82    }
83
84    /// If enabled the processor adds additional metadata to errors.
85    pub fn with_verbose_errors(mut self, verbose: bool) -> Self {
86        self.verbose_errors = verbose;
87        self
88    }
89}
90
91impl Processor for SchemaProcessor {
92    fn process_string(
93        &mut self,
94        value: &mut String,
95        meta: &mut Meta,
96        state: &ProcessingState<'_>,
97    ) -> ProcessingResult {
98        value_trim_whitespace(value, meta, state);
99        verify_value_nonempty_string(value, meta, state)?;
100        verify_value_characters(value, meta, state)?;
101        Ok(())
102    }
103
104    fn process_array<T>(
105        &mut self,
106        value: &mut Array<T>,
107        meta: &mut Meta,
108        state: &ProcessingState<'_>,
109    ) -> ProcessingResult
110    where
111        T: ProcessValue,
112    {
113        value.process_child_values(self, state)?;
114        verify_value_nonempty(value, meta, state)?;
115        Ok(())
116    }
117
118    fn process_object<T>(
119        &mut self,
120        value: &mut Object<T>,
121        meta: &mut Meta,
122        state: &ProcessingState<'_>,
123    ) -> ProcessingResult
124    where
125        T: ProcessValue,
126    {
127        value.process_child_values(self, state)?;
128        verify_value_nonempty(value, meta, state)?;
129        Ok(())
130    }
131
132    fn before_process<T: ProcessValue>(
133        &mut self,
134        value: Option<&T>,
135        meta: &mut Meta,
136        state: &ProcessingState<'_>,
137    ) -> ProcessingResult {
138        match self.required {
139            RequiredMode::DeleteParent => {
140                self.stack.push(SchemaState::default());
141            }
142            RequiredMode::DeleteValue => {
143                if value.is_none() && state.attrs().required && !meta.has_errors() {
144                    meta.add_error(ErrorKind::MissingAttribute);
145                }
146            }
147        }
148
149        Ok(())
150    }
151
152    fn after_process<T: ProcessValue>(
153        &mut self,
154        value: Option<&T>,
155        meta: &mut Meta,
156        state: &ProcessingState<'_>,
157    ) -> ProcessingResult {
158        if matches!(self.required, RequiredMode::DeleteValue) {
159            return Ok(());
160        }
161
162        let Some(mut current) = self.stack.pop() else {
163            debug_assert!(false, "processing stack should always have a value");
164            return Ok(());
165        };
166
167        // A local violation indicates that the current field violates a required requirement.
168        // In such a case the parent must be deleted.
169        let mut local_violation = None;
170        if state.attrs().required {
171            local_violation = current.required_violation.take();
172            if value.is_none() {
173                let violation = local_violation.get_or_insert_default();
174                if self.verbose_errors {
175                    violation.add(state);
176                }
177            }
178        }
179
180        if let Some(violation) = local_violation {
181            if let Some(parent) = self.stack.last_mut() {
182                // Just attaching the violation to the parent is enough,
183                // as the parent will delete itself and annotate the error.
184                match &mut parent.required_violation {
185                    p @ None => *p = Some(violation),
186                    Some(p) => p.merge_with(violation),
187                }
188            } else {
189                // There is no parent we can attach the error to, this must be the root element and
190                // we have to attach the violation to this node as an error.
191                meta.add_error(violation)
192            };
193            Err(ProcessingAction::DeleteValueHard)
194        } else if let Some(violation) = current.required_violation {
195            // A child violated a required requirement, but this node itself is not required,
196            // the parent does not need to be deleted and we can attach the violation information
197            // to the current node.
198            meta.add_error(violation);
199            Err(ProcessingAction::DeleteValueHard)
200        } else {
201            Ok(())
202        }
203    }
204}
205
206#[derive(Debug, Default)]
207struct SchemaState {
208    required_violation: Option<RequiredViolation>,
209}
210
211#[derive(Debug, Default, Clone)]
212struct RequiredViolation {
213    path: Vec<Annotated<Value>>,
214}
215
216impl RequiredViolation {
217    fn add(&mut self, state: &ProcessingState<'_>) {
218        self.path
219            .push(Annotated::new(state.path().to_string().into()));
220    }
221
222    fn merge_with(&mut self, other: Self) {
223        let Self { path } = other;
224        self.path.extend(path);
225    }
226}
227
228impl From<RequiredViolation> for Error {
229    fn from(value: RequiredViolation) -> Self {
230        let mut error: Error = ErrorKind::MissingAttribute.into();
231        if !value.path.is_empty() {
232            error.insert("path", value.path);
233        }
234        error
235    }
236}
237
238fn value_trim_whitespace(value: &mut String, _meta: &mut Meta, state: &ProcessingState<'_>) {
239    if state.attrs().trim_whitespace {
240        let new_value = value.trim().to_owned();
241        value.clear();
242        value.push_str(&new_value);
243    }
244}
245
246fn verify_value_nonempty<T>(
247    value: &T,
248    meta: &mut Meta,
249    state: &ProcessingState<'_>,
250) -> ProcessingResult
251where
252    T: Empty,
253{
254    if state.attrs().nonempty && value.is_empty() {
255        meta.add_error(Error::nonempty());
256        Err(ProcessingAction::DeleteValueHard)
257    } else {
258        Ok(())
259    }
260}
261
262fn verify_value_nonempty_string<T>(
263    value: &T,
264    meta: &mut Meta,
265    state: &ProcessingState<'_>,
266) -> ProcessingResult
267where
268    T: Empty,
269{
270    if state.attrs().nonempty && value.is_empty() {
271        meta.add_error(Error::nonempty_string());
272        Err(ProcessingAction::DeleteValueHard)
273    } else {
274        Ok(())
275    }
276}
277
278fn verify_value_characters(
279    value: &str,
280    meta: &mut Meta,
281    state: &ProcessingState<'_>,
282) -> ProcessingResult {
283    if let Some(ref character_set) = state.attrs().characters {
284        for c in value.chars() {
285            if !(character_set.char_is_valid)(c) {
286                meta.add_error(Error::invalid(format!("invalid character {c:?}")));
287                return Err(ProcessingAction::DeleteValueSoft);
288            }
289        }
290    }
291
292    Ok(())
293}
294
295#[cfg(test)]
296mod tests {
297    use relay_event_schema::processor;
298    use relay_event_schema::protocol::{
299        CError, ClientSdkInfo, Event, MachException, Mechanism, MechanismMeta, PosixSignal,
300        RawStacktrace, User,
301    };
302    use relay_protocol::{Annotated, FromValue, IntoValue, assert_annotated_snapshot};
303    use similar_asserts::assert_eq;
304
305    use super::*;
306
307    fn assert_nonempty_base<T>(expected_error: &str)
308    where
309        T: Default + PartialEq + ProcessValue,
310    {
311        #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
312        struct Foo<T> {
313            #[metastructure(required = true, nonempty = true)]
314            bar: Annotated<T>,
315            bar2: Annotated<T>,
316        }
317
318        let mut wrapper = Annotated::new(Foo {
319            bar: Annotated::new(T::default()),
320            bar2: Annotated::new(T::default()),
321        });
322        processor::process_value(
323            &mut wrapper,
324            &mut SchemaProcessor::new(),
325            ProcessingState::root(),
326        )
327        .unwrap();
328
329        assert_eq!(
330            wrapper,
331            Annotated::new(Foo {
332                bar: Annotated::from_error(Error::expected(expected_error), None),
333                bar2: Annotated::new(T::default())
334            })
335        );
336    }
337
338    #[test]
339    fn test_nonempty_string() {
340        assert_nonempty_base::<String>("a non-empty string");
341    }
342
343    #[test]
344    fn test_nonempty_array() {
345        assert_nonempty_base::<Array<u64>>("a non-empty value");
346    }
347
348    #[test]
349    fn test_nonempty_object() {
350        assert_nonempty_base::<Object<u64>>("a non-empty value");
351    }
352
353    #[test]
354    fn test_invalid_email() {
355        let mut user = Annotated::new(User {
356            email: Annotated::new("bananabread".to_owned()),
357            ..Default::default()
358        });
359
360        let expected = user.clone();
361        processor::process_value(
362            &mut user,
363            &mut SchemaProcessor::new(),
364            ProcessingState::root(),
365        )
366        .unwrap();
367
368        assert_eq!(user, expected);
369    }
370
371    #[test]
372    fn test_client_sdk_missing_attribute() {
373        let mut info = Annotated::new(ClientSdkInfo {
374            name: Annotated::new("sentry.rust".to_owned()),
375            ..Default::default()
376        });
377
378        processor::process_value(
379            &mut info,
380            &mut SchemaProcessor::new(),
381            ProcessingState::root(),
382        )
383        .unwrap();
384
385        let expected = Annotated::new(ClientSdkInfo {
386            name: Annotated::new("sentry.rust".to_owned()),
387            version: Annotated::from_error(ErrorKind::MissingAttribute, None),
388            ..Default::default()
389        });
390
391        assert_eq!(info, expected);
392    }
393
394    #[test]
395    fn test_mechanism_missing_attributes() {
396        let mut mechanism = Annotated::new(Mechanism {
397            ty: Annotated::new("mytype".to_owned()),
398            meta: Annotated::new(MechanismMeta {
399                errno: Annotated::new(CError {
400                    name: Annotated::new("ENOENT".to_owned()),
401                    ..Default::default()
402                }),
403                mach_exception: Annotated::new(MachException {
404                    name: Annotated::new("EXC_BAD_ACCESS".to_owned()),
405                    ..Default::default()
406                }),
407                signal: Annotated::new(PosixSignal {
408                    name: Annotated::new("SIGSEGV".to_owned()),
409                    ..Default::default()
410                }),
411                ..Default::default()
412            }),
413            ..Default::default()
414        });
415
416        processor::process_value(
417            &mut mechanism,
418            &mut SchemaProcessor::new(),
419            ProcessingState::root(),
420        )
421        .unwrap();
422
423        let expected = Annotated::new(Mechanism {
424            ty: Annotated::new("mytype".to_owned()),
425            meta: Annotated::new(MechanismMeta {
426                errno: Annotated::new(CError {
427                    number: Annotated::empty(),
428                    name: Annotated::new("ENOENT".to_owned()),
429                }),
430                mach_exception: Annotated::new(MachException {
431                    ty: Annotated::empty(),
432                    code: Annotated::empty(),
433                    subcode: Annotated::empty(),
434                    name: Annotated::new("EXC_BAD_ACCESS".to_owned()),
435                }),
436                signal: Annotated::new(PosixSignal {
437                    number: Annotated::empty(),
438                    code: Annotated::empty(),
439                    name: Annotated::new("SIGSEGV".to_owned()),
440                    code_name: Annotated::empty(),
441                }),
442                ..Default::default()
443            }),
444            ..Default::default()
445        });
446
447        assert_eq!(mechanism, expected);
448    }
449
450    #[test]
451    fn test_stacktrace_missing_attribute() {
452        let mut stack = Annotated::new(RawStacktrace::default());
453
454        processor::process_value(
455            &mut stack,
456            &mut SchemaProcessor::new(),
457            ProcessingState::root(),
458        )
459        .unwrap();
460
461        let expected = Annotated::new(RawStacktrace {
462            frames: Annotated::from_error(ErrorKind::MissingAttribute, None),
463            ..Default::default()
464        });
465
466        assert_eq!(stack, expected);
467    }
468
469    #[test]
470    fn test_newlines_release() {
471        let mut event = Annotated::new(Event {
472            release: Annotated::new("42\n".to_owned().into()),
473            ..Default::default()
474        });
475
476        processor::process_value(
477            &mut event,
478            &mut SchemaProcessor::new(),
479            ProcessingState::root(),
480        )
481        .unwrap();
482
483        let expected = Annotated::new(Event {
484            release: Annotated::new("42".to_owned().into()),
485            ..Default::default()
486        });
487
488        assert_eq!(expected, event);
489    }
490
491    #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
492    struct TestItem<T> {
493        #[metastructure(required = true, nonempty = true)]
494        req_non_empty: Annotated<T>,
495        #[metastructure(required = true)]
496        req: Annotated<T>,
497        other: Annotated<T>,
498    }
499
500    #[test]
501    fn test_required_delete_parent_top_level_nonempty() {
502        let mut item = Annotated::new(TestItem {
503            req_non_empty: Annotated::new("".to_owned()),
504            req: Annotated::new("something".to_owned()),
505            other: Annotated::new("something".to_owned()),
506        });
507
508        processor::process_value(
509            &mut item,
510            &mut SchemaProcessor::new()
511                .with_required(RequiredMode::DeleteParent)
512                .with_verbose_errors(true),
513            ProcessingState::root(),
514        )
515        .unwrap();
516
517        assert_annotated_snapshot!(item, @r#"
518        {
519          "_meta": {
520            "": {
521              "err": [
522                [
523                  "missing_attribute",
524                  {
525                    "path": [
526                      "req_non_empty"
527                    ]
528                  }
529                ]
530              ]
531            }
532          }
533        }
534        "#);
535    }
536
537    #[test]
538    fn test_required_delete_parent_top_level_nonempty_not_verbose() {
539        let mut item = Annotated::new(TestItem {
540            req_non_empty: Annotated::new("".to_owned()),
541            req: Annotated::new("something".to_owned()),
542            other: Annotated::new("something".to_owned()),
543        });
544
545        processor::process_value(
546            &mut item,
547            &mut SchemaProcessor::new().with_required(RequiredMode::DeleteParent),
548            ProcessingState::root(),
549        )
550        .unwrap();
551
552        assert_annotated_snapshot!(item, @r#"
553        {
554          "_meta": {
555            "": {
556              "err": [
557                "missing_attribute"
558              ]
559            }
560          }
561        }
562        "#);
563    }
564
565    #[test]
566    fn test_required_delete_parent_top_level_req() {
567        let mut item = Annotated::new(TestItem {
568            req_non_empty: Annotated::new("something".to_owned()),
569            req: Annotated::empty(),
570            other: Annotated::new("something".to_owned()),
571        });
572
573        processor::process_value(
574            &mut item,
575            &mut SchemaProcessor::new()
576                .with_required(RequiredMode::DeleteParent)
577                .with_verbose_errors(true),
578            ProcessingState::root(),
579        )
580        .unwrap();
581
582        assert_annotated_snapshot!(item, @r#"
583        {
584          "_meta": {
585            "": {
586              "err": [
587                [
588                  "missing_attribute",
589                  {
590                    "path": [
591                      "req"
592                    ]
593                  }
594                ]
595              ]
596            }
597          }
598        }
599        "#);
600    }
601
602    #[test]
603    fn test_required_delete_parent_top_level_req_error() {
604        let mut item = Annotated::new(TestItem {
605            req_non_empty: Annotated::new("something".to_owned()),
606            req: Annotated(None, Meta::from_error(Error::expected("something"))),
607            other: Annotated::new("something".to_owned()),
608        });
609
610        processor::process_value(
611            &mut item,
612            &mut SchemaProcessor::new()
613                .with_required(RequiredMode::DeleteParent)
614                .with_verbose_errors(true),
615            ProcessingState::root(),
616        )
617        .unwrap();
618
619        assert_annotated_snapshot!(item, @r#"
620        {
621          "_meta": {
622            "": {
623              "err": [
624                [
625                  "missing_attribute",
626                  {
627                    "path": [
628                      "req"
629                    ]
630                  }
631                ]
632              ]
633            }
634          }
635        }
636        "#);
637    }
638
639    #[test]
640    fn test_required_delete_parent_top_multiple_missing() {
641        let mut item = Annotated::new(TestItem {
642            req_non_empty: Annotated::new("".to_owned()),
643            req: Annotated::empty(),
644            other: Annotated::empty(),
645        });
646
647        processor::process_value(
648            &mut item,
649            &mut SchemaProcessor::new()
650                .with_required(RequiredMode::DeleteParent)
651                .with_verbose_errors(true),
652            ProcessingState::root(),
653        )
654        .unwrap();
655
656        assert_annotated_snapshot!(item, @r#"
657        {
658          "_meta": {
659            "": {
660              "err": [
661                [
662                  "missing_attribute",
663                  {
664                    "path": [
665                      "req_non_empty",
666                      "req"
667                    ]
668                  }
669                ]
670              ]
671            }
672          }
673        }
674        "#);
675    }
676
677    #[test]
678    fn test_required_delete_parent_top_level_okay() {
679        let mut item = Annotated::new(TestItem {
680            req_non_empty: Annotated::new("something".to_owned()),
681            req: Annotated::new("something".to_owned()),
682            other: Annotated::empty(),
683        });
684
685        processor::process_value(
686            &mut item,
687            &mut SchemaProcessor::new()
688                .with_required(RequiredMode::DeleteParent)
689                .with_verbose_errors(true),
690            ProcessingState::root(),
691        )
692        .unwrap();
693
694        assert_annotated_snapshot!(item, @r#"
695        {
696          "req_non_empty": "something",
697          "req": "something"
698        }
699        "#);
700    }
701
702    #[test]
703    fn test_required_delete_parent_nested_propagated() {
704        let mut item = Annotated::new(TestItem {
705            req_non_empty: Annotated::new(TestItem {
706                req_non_empty: Annotated::new("".to_owned()),
707                req: Annotated::new("something".to_owned()),
708                other: Annotated::new("something".to_owned()),
709            }),
710            req: Annotated::new(TestItem {
711                req_non_empty: Annotated::new("something".to_owned()),
712                req: Annotated::new("something".to_owned()),
713                other: Annotated::new("something".to_owned()),
714            }),
715            other: Annotated::empty(),
716        });
717
718        processor::process_value(
719            &mut item,
720            &mut SchemaProcessor::new()
721                .with_required(RequiredMode::DeleteParent)
722                .with_verbose_errors(true),
723            ProcessingState::root(),
724        )
725        .unwrap();
726
727        assert_annotated_snapshot!(item, @r#"
728        {
729          "_meta": {
730            "": {
731              "err": [
732                [
733                  "missing_attribute",
734                  {
735                    "path": [
736                      "req_non_empty.req_non_empty"
737                    ]
738                  }
739                ]
740              ]
741            }
742          }
743        }
744        "#);
745    }
746
747    #[test]
748    fn test_required_delete_nested_simple_all_the_way() {
749        #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
750        struct Foo {
751            #[metastructure(required = true)]
752            bar: Annotated<Bar>,
753            other: Annotated<String>,
754        }
755
756        #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
757        struct Bar {
758            #[metastructure(required = true, nonempty = true)]
759            value: Annotated<String>,
760        }
761
762        let mut item = Annotated::new(Foo {
763            bar: Annotated::new(Bar {
764                value: Annotated::new("".to_owned()),
765            }),
766            other: Annotated::new("something".to_owned()),
767        });
768
769        processor::process_value(
770            &mut item,
771            &mut SchemaProcessor::new()
772                .with_required(RequiredMode::DeleteParent)
773                .with_verbose_errors(true),
774            ProcessingState::root(),
775        )
776        .unwrap();
777
778        assert_annotated_snapshot!(item, @r#"
779        {
780          "_meta": {
781            "": {
782              "err": [
783                [
784                  "missing_attribute",
785                  {
786                    "path": [
787                      "bar.value"
788                    ]
789                  }
790                ]
791              ]
792            }
793          }
794        }
795        "#);
796    }
797
798    #[test]
799    fn test_required_delete_nested_simple_one_layer() {
800        #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
801        struct Foo {
802            bar: Annotated<Bar>,
803            other: Annotated<String>,
804        }
805
806        #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
807        struct Bar {
808            #[metastructure(required = true, nonempty = true)]
809            value: Annotated<String>,
810        }
811
812        let mut item = Annotated::new(Foo {
813            bar: Annotated::new(Bar {
814                value: Annotated::new("".to_owned()),
815            }),
816            other: Annotated::new("something".to_owned()),
817        });
818
819        processor::process_value(
820            &mut item,
821            &mut SchemaProcessor::new()
822                .with_required(RequiredMode::DeleteParent)
823                .with_verbose_errors(true),
824            ProcessingState::root(),
825        )
826        .unwrap();
827
828        assert_annotated_snapshot!(item, @r#"
829        {
830          "bar": null,
831          "other": "something",
832          "_meta": {
833            "bar": {
834              "": {
835                "err": [
836                  [
837                    "missing_attribute",
838                    {
839                      "path": [
840                        "bar.value"
841                      ]
842                    }
843                  ]
844                ]
845              }
846            }
847          }
848        }
849        "#);
850    }
851}