Skip to main content

relay_event_normalization/
schema.rs

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