relay_event_normalization/
schema.rs

1use relay_event_schema::processor::{
2    ProcessValue, ProcessingAction, ProcessingResult, ProcessingState, Processor,
3};
4use relay_protocol::{Array, Empty, Error, ErrorKind, Meta, Object};
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    stack: SmallVec<[SchemaState; 10]>,
69}
70
71impl SchemaProcessor {
72    /// Creates a new [`SchemaProcessor`].
73    pub fn new() -> Self {
74        Default::default()
75    }
76
77    /// Configures how `required` values should be validated.
78    pub fn with_required(mut self, mode: RequiredMode) -> Self {
79        self.required = mode;
80        self
81    }
82}
83
84impl Processor for SchemaProcessor {
85    fn process_string(
86        &mut self,
87        value: &mut String,
88        meta: &mut Meta,
89        state: &ProcessingState<'_>,
90    ) -> ProcessingResult {
91        value_trim_whitespace(value, meta, state);
92        verify_value_nonempty_string(value, meta, state)?;
93        verify_value_characters(value, meta, state)?;
94        Ok(())
95    }
96
97    fn process_array<T>(
98        &mut self,
99        value: &mut Array<T>,
100        meta: &mut Meta,
101        state: &ProcessingState<'_>,
102    ) -> ProcessingResult
103    where
104        T: ProcessValue,
105    {
106        value.process_child_values(self, state)?;
107        verify_value_nonempty(value, meta, state)?;
108        Ok(())
109    }
110
111    fn process_object<T>(
112        &mut self,
113        value: &mut Object<T>,
114        meta: &mut Meta,
115        state: &ProcessingState<'_>,
116    ) -> ProcessingResult
117    where
118        T: ProcessValue,
119    {
120        value.process_child_values(self, state)?;
121        verify_value_nonempty(value, meta, state)?;
122        Ok(())
123    }
124
125    fn before_process<T: ProcessValue>(
126        &mut self,
127        value: Option<&T>,
128        meta: &mut Meta,
129        state: &ProcessingState<'_>,
130    ) -> ProcessingResult {
131        match self.required {
132            RequiredMode::DeleteParent => {
133                self.stack.push(SchemaState::default());
134            }
135            RequiredMode::DeleteValue => {
136                if value.is_none() && state.attrs().required && !meta.has_errors() {
137                    meta.add_error(ErrorKind::MissingAttribute);
138                }
139            }
140        }
141
142        Ok(())
143    }
144
145    fn after_process<T: ProcessValue>(
146        &mut self,
147        value: Option<&T>,
148        meta: &mut Meta,
149        state: &ProcessingState<'_>,
150    ) -> ProcessingResult {
151        if matches!(self.required, RequiredMode::DeleteValue) {
152            return Ok(());
153        }
154
155        let Some(current) = self.stack.pop() else {
156            debug_assert!(false, "processing stack should always have a value");
157            return Ok(());
158        };
159
160        // There is a required validation if the field is required and the value is `None`, or
161        // the current object had any required violations already.
162        let is_required_violation =
163            state.attrs().required && (value.is_none() || current.has_required_violation);
164
165        // Propagate the violation to the parent container, to make sure the parent is deleted.
166        if is_required_violation && let Some(parent) = self.stack.last_mut() {
167            parent.has_required_violation = true;
168        }
169
170        // Delete the current value if it is a container containing a violation.
171        match current.has_required_violation {
172            true => {
173                meta.add_error(ErrorKind::MissingAttribute);
174                Err(ProcessingAction::DeleteValueHard)
175            }
176            false => Ok(()),
177        }
178    }
179}
180
181#[derive(Debug, Default)]
182struct SchemaState {
183    has_required_violation: bool,
184}
185
186fn value_trim_whitespace(value: &mut String, _meta: &mut Meta, state: &ProcessingState<'_>) {
187    if state.attrs().trim_whitespace {
188        let new_value = value.trim().to_owned();
189        value.clear();
190        value.push_str(&new_value);
191    }
192}
193
194fn verify_value_nonempty<T>(
195    value: &T,
196    meta: &mut Meta,
197    state: &ProcessingState<'_>,
198) -> ProcessingResult
199where
200    T: Empty,
201{
202    if state.attrs().nonempty && value.is_empty() {
203        meta.add_error(Error::nonempty());
204        Err(ProcessingAction::DeleteValueHard)
205    } else {
206        Ok(())
207    }
208}
209
210fn verify_value_nonempty_string<T>(
211    value: &T,
212    meta: &mut Meta,
213    state: &ProcessingState<'_>,
214) -> ProcessingResult
215where
216    T: Empty,
217{
218    if state.attrs().nonempty && value.is_empty() {
219        meta.add_error(Error::nonempty_string());
220        Err(ProcessingAction::DeleteValueHard)
221    } else {
222        Ok(())
223    }
224}
225
226fn verify_value_characters(
227    value: &str,
228    meta: &mut Meta,
229    state: &ProcessingState<'_>,
230) -> ProcessingResult {
231    if let Some(ref character_set) = state.attrs().characters {
232        for c in value.chars() {
233            if !(character_set.char_is_valid)(c) {
234                meta.add_error(Error::invalid(format!("invalid character {c:?}")));
235                return Err(ProcessingAction::DeleteValueSoft);
236            }
237        }
238    }
239
240    Ok(())
241}
242
243#[cfg(test)]
244mod tests {
245    use relay_event_schema::processor;
246    use relay_event_schema::protocol::{
247        CError, ClientSdkInfo, Event, MachException, Mechanism, MechanismMeta, PosixSignal,
248        RawStacktrace, User,
249    };
250    use relay_protocol::{Annotated, FromValue, IntoValue, assert_annotated_snapshot};
251    use similar_asserts::assert_eq;
252
253    use super::*;
254
255    fn assert_nonempty_base<T>(expected_error: &str)
256    where
257        T: Default + PartialEq + ProcessValue,
258    {
259        #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
260        struct Foo<T> {
261            #[metastructure(required = true, nonempty = true)]
262            bar: Annotated<T>,
263            bar2: Annotated<T>,
264        }
265
266        let mut wrapper = Annotated::new(Foo {
267            bar: Annotated::new(T::default()),
268            bar2: Annotated::new(T::default()),
269        });
270        processor::process_value(
271            &mut wrapper,
272            &mut SchemaProcessor::new(),
273            ProcessingState::root(),
274        )
275        .unwrap();
276
277        assert_eq!(
278            wrapper,
279            Annotated::new(Foo {
280                bar: Annotated::from_error(Error::expected(expected_error), None),
281                bar2: Annotated::new(T::default())
282            })
283        );
284    }
285
286    #[test]
287    fn test_nonempty_string() {
288        assert_nonempty_base::<String>("a non-empty string");
289    }
290
291    #[test]
292    fn test_nonempty_array() {
293        assert_nonempty_base::<Array<u64>>("a non-empty value");
294    }
295
296    #[test]
297    fn test_nonempty_object() {
298        assert_nonempty_base::<Object<u64>>("a non-empty value");
299    }
300
301    #[test]
302    fn test_invalid_email() {
303        let mut user = Annotated::new(User {
304            email: Annotated::new("bananabread".to_owned()),
305            ..Default::default()
306        });
307
308        let expected = user.clone();
309        processor::process_value(
310            &mut user,
311            &mut SchemaProcessor::new(),
312            ProcessingState::root(),
313        )
314        .unwrap();
315
316        assert_eq!(user, expected);
317    }
318
319    #[test]
320    fn test_client_sdk_missing_attribute() {
321        let mut info = Annotated::new(ClientSdkInfo {
322            name: Annotated::new("sentry.rust".to_owned()),
323            ..Default::default()
324        });
325
326        processor::process_value(
327            &mut info,
328            &mut SchemaProcessor::new(),
329            ProcessingState::root(),
330        )
331        .unwrap();
332
333        let expected = Annotated::new(ClientSdkInfo {
334            name: Annotated::new("sentry.rust".to_owned()),
335            version: Annotated::from_error(ErrorKind::MissingAttribute, None),
336            ..Default::default()
337        });
338
339        assert_eq!(info, expected);
340    }
341
342    #[test]
343    fn test_mechanism_missing_attributes() {
344        let mut mechanism = Annotated::new(Mechanism {
345            ty: Annotated::new("mytype".to_owned()),
346            meta: Annotated::new(MechanismMeta {
347                errno: Annotated::new(CError {
348                    name: Annotated::new("ENOENT".to_owned()),
349                    ..Default::default()
350                }),
351                mach_exception: Annotated::new(MachException {
352                    name: Annotated::new("EXC_BAD_ACCESS".to_owned()),
353                    ..Default::default()
354                }),
355                signal: Annotated::new(PosixSignal {
356                    name: Annotated::new("SIGSEGV".to_owned()),
357                    ..Default::default()
358                }),
359                ..Default::default()
360            }),
361            ..Default::default()
362        });
363
364        processor::process_value(
365            &mut mechanism,
366            &mut SchemaProcessor::new(),
367            ProcessingState::root(),
368        )
369        .unwrap();
370
371        let expected = Annotated::new(Mechanism {
372            ty: Annotated::new("mytype".to_owned()),
373            meta: Annotated::new(MechanismMeta {
374                errno: Annotated::new(CError {
375                    number: Annotated::empty(),
376                    name: Annotated::new("ENOENT".to_owned()),
377                }),
378                mach_exception: Annotated::new(MachException {
379                    ty: Annotated::empty(),
380                    code: Annotated::empty(),
381                    subcode: Annotated::empty(),
382                    name: Annotated::new("EXC_BAD_ACCESS".to_owned()),
383                }),
384                signal: Annotated::new(PosixSignal {
385                    number: Annotated::empty(),
386                    code: Annotated::empty(),
387                    name: Annotated::new("SIGSEGV".to_owned()),
388                    code_name: Annotated::empty(),
389                }),
390                ..Default::default()
391            }),
392            ..Default::default()
393        });
394
395        assert_eq!(mechanism, expected);
396    }
397
398    #[test]
399    fn test_stacktrace_missing_attribute() {
400        let mut stack = Annotated::new(RawStacktrace::default());
401
402        processor::process_value(
403            &mut stack,
404            &mut SchemaProcessor::new(),
405            ProcessingState::root(),
406        )
407        .unwrap();
408
409        let expected = Annotated::new(RawStacktrace {
410            frames: Annotated::from_error(ErrorKind::MissingAttribute, None),
411            ..Default::default()
412        });
413
414        assert_eq!(stack, expected);
415    }
416
417    #[test]
418    fn test_newlines_release() {
419        let mut event = Annotated::new(Event {
420            release: Annotated::new("42\n".to_owned().into()),
421            ..Default::default()
422        });
423
424        processor::process_value(
425            &mut event,
426            &mut SchemaProcessor::new(),
427            ProcessingState::root(),
428        )
429        .unwrap();
430
431        let expected = Annotated::new(Event {
432            release: Annotated::new("42".to_owned().into()),
433            ..Default::default()
434        });
435
436        assert_eq!(expected, event);
437    }
438
439    #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
440    struct TestItem<T> {
441        #[metastructure(required = true, nonempty = true)]
442        req_non_empty: Annotated<T>,
443        #[metastructure(required = true)]
444        req: Annotated<T>,
445        other: Annotated<T>,
446    }
447
448    #[test]
449    fn test_required_delete_parent_top_level_nonempty() {
450        let mut item = Annotated::new(TestItem {
451            req_non_empty: Annotated::new("".to_owned()),
452            req: Annotated::new("something".to_owned()),
453            other: Annotated::new("something".to_owned()),
454        });
455
456        processor::process_value(
457            &mut item,
458            &mut SchemaProcessor::new().with_required(RequiredMode::DeleteParent),
459            ProcessingState::root(),
460        )
461        .unwrap();
462
463        assert_annotated_snapshot!(item, @r#"
464        {
465          "_meta": {
466            "": {
467              "err": [
468                "missing_attribute"
469              ]
470            }
471          }
472        }
473        "#);
474    }
475
476    #[test]
477    fn test_required_delete_parent_top_level_req() {
478        let mut item = Annotated::new(TestItem {
479            req_non_empty: Annotated::new("something".to_owned()),
480            req: Annotated::empty(),
481            other: Annotated::new("something".to_owned()),
482        });
483
484        processor::process_value(
485            &mut item,
486            &mut SchemaProcessor::new().with_required(RequiredMode::DeleteParent),
487            ProcessingState::root(),
488        )
489        .unwrap();
490
491        assert_annotated_snapshot!(item, @r#"
492        {
493          "_meta": {
494            "": {
495              "err": [
496                "missing_attribute"
497              ]
498            }
499          }
500        }
501        "#);
502    }
503
504    #[test]
505    fn test_required_delete_parent_top_level_req_error() {
506        let mut item = Annotated::new(TestItem {
507            req_non_empty: Annotated::new("something".to_owned()),
508            req: Annotated(None, Meta::from_error(Error::expected("something"))),
509            other: Annotated::new("something".to_owned()),
510        });
511
512        processor::process_value(
513            &mut item,
514            &mut SchemaProcessor::new().with_required(RequiredMode::DeleteParent),
515            ProcessingState::root(),
516        )
517        .unwrap();
518
519        assert_annotated_snapshot!(item, @r#"
520        {
521          "_meta": {
522            "": {
523              "err": [
524                "missing_attribute"
525              ]
526            }
527          }
528        }
529        "#);
530    }
531
532    #[test]
533    fn test_required_delete_parent_top_level_okay() {
534        let mut item = Annotated::new(TestItem {
535            req_non_empty: Annotated::new("something".to_owned()),
536            req: Annotated::new("something".to_owned()),
537            other: Annotated::empty(),
538        });
539
540        processor::process_value(
541            &mut item,
542            &mut SchemaProcessor::new().with_required(RequiredMode::DeleteParent),
543            ProcessingState::root(),
544        )
545        .unwrap();
546
547        assert_annotated_snapshot!(item, @r#"
548        {
549          "req_non_empty": "something",
550          "req": "something"
551        }
552        "#);
553    }
554
555    #[test]
556    fn test_required_delete_parent_nested_propagated() {
557        let mut item = Annotated::new(TestItem {
558            req_non_empty: 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            req: Annotated::new(TestItem {
564                req_non_empty: Annotated::new("something".to_owned()),
565                req: Annotated::new("something".to_owned()),
566                other: Annotated::new("something".to_owned()),
567            }),
568            other: Annotated::empty(),
569        });
570
571        processor::process_value(
572            &mut item,
573            &mut SchemaProcessor::new().with_required(RequiredMode::DeleteParent),
574            ProcessingState::root(),
575        )
576        .unwrap();
577
578        assert_annotated_snapshot!(item, @r#"
579        {
580          "_meta": {
581            "": {
582              "err": [
583                "missing_attribute"
584              ]
585            }
586          }
587        }
588        "#);
589    }
590
591    #[test]
592    fn test_required_delete_nested_simple_all_the_way() {
593        #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
594        struct Foo {
595            #[metastructure(required = true)]
596            bar: Annotated<Bar>,
597            other: Annotated<String>,
598        }
599
600        #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
601        struct Bar {
602            #[metastructure(required = true, nonempty = true)]
603            value: Annotated<String>,
604        }
605
606        let mut item = Annotated::new(Foo {
607            bar: Annotated::new(Bar {
608                value: Annotated::new("".to_owned()),
609            }),
610            other: Annotated::new("something".to_owned()),
611        });
612
613        processor::process_value(
614            &mut item,
615            &mut SchemaProcessor::new().with_required(RequiredMode::DeleteParent),
616            ProcessingState::root(),
617        )
618        .unwrap();
619
620        assert_annotated_snapshot!(item, @r#"
621        {
622          "_meta": {
623            "": {
624              "err": [
625                "missing_attribute"
626              ]
627            }
628          }
629        }
630        "#);
631    }
632
633    #[test]
634    fn test_required_delete_nested_simple_one_layer() {
635        #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
636        struct Foo {
637            bar: Annotated<Bar>,
638            other: Annotated<String>,
639        }
640
641        #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
642        struct Bar {
643            #[metastructure(required = true, nonempty = true)]
644            value: Annotated<String>,
645        }
646
647        let mut item = Annotated::new(Foo {
648            bar: Annotated::new(Bar {
649                value: Annotated::new("".to_owned()),
650            }),
651            other: Annotated::new("something".to_owned()),
652        });
653
654        processor::process_value(
655            &mut item,
656            &mut SchemaProcessor::new().with_required(RequiredMode::DeleteParent),
657            ProcessingState::root(),
658        )
659        .unwrap();
660
661        assert_annotated_snapshot!(item, @r#"
662        {
663          "bar": null,
664          "other": "something",
665          "_meta": {
666            "bar": {
667              "": {
668                "err": [
669                  "missing_attribute"
670                ]
671              }
672            }
673          }
674        }
675        "#);
676    }
677}