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#[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 stack: SmallVec<[SchemaState; 10]>,
69}
70
71impl SchemaProcessor {
72 pub fn new() -> Self {
74 Default::default()
75 }
76
77 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 let is_required_violation =
163 state.attrs().required && (value.is_none() || current.has_required_violation);
164
165 if is_required_violation && let Some(parent) = self.stack.last_mut() {
167 parent.has_required_violation = true;
168 }
169
170 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}