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#[derive(Debug, Default)]
9pub enum RequiredMode {
10 #[default]
36 DeleteValue,
37 DeleteParent,
62}
63
64#[derive(Debug, Default)]
66pub struct SchemaProcessor {
67 required: RequiredMode,
68 verbose_errors: bool,
69 stack: SmallVec<[SchemaState; 10]>,
70}
71
72impl SchemaProcessor {
73 pub fn new() -> Self {
75 Default::default()
76 }
77
78 pub fn with_required(mut self, mode: RequiredMode) -> Self {
80 self.required = mode;
81 self
82 }
83
84 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 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 match &mut parent.required_violation {
204 p @ None => *p = Some(violation),
205 Some(p) => p.merge_with(violation),
206 }
207 } else {
208 meta.add_error(violation)
211 };
212 Err(ProcessingAction::DeleteValueHard)
213 } else if let Some(violation) = current.required_violation {
214 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}