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#[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 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 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 match &mut parent.required_violation {
185 p @ None => *p = Some(violation),
186 Some(p) => p.merge_with(violation),
187 }
188 } else {
189 meta.add_error(violation)
192 };
193 Err(ProcessingAction::DeleteValueHard)
194 } else if let Some(violation) = current.required_violation {
195 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}