Skip to main content

relay_event_schema/processor/
attrs.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::ops::{Deref, RangeInclusive};
4
5use enumset::{EnumSet, EnumSetType};
6use relay_protocol::Annotated;
7
8use crate::processor::ProcessValue;
9
10/// Error for unknown value types.
11#[derive(Debug)]
12pub struct UnknownValueTypeError;
13
14impl fmt::Display for UnknownValueTypeError {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        write!(f, "unknown value type")
17    }
18}
19
20impl std::error::Error for UnknownValueTypeError {}
21
22/// The (simplified) type of a value.
23#[derive(Debug, Ord, PartialOrd, EnumSetType)]
24pub enum ValueType {
25    // Basic types
26    String,
27    Binary,
28    Number,
29    Boolean,
30    DateTime,
31    Array,
32    Object,
33
34    // Roots
35    Event,
36    Attachments,
37    Replay,
38
39    // Protocol types
40    Exception,
41    Stacktrace,
42    Frame,
43    Request,
44    User,
45    LogEntry,
46    Message,
47    Thread,
48    Breadcrumb,
49    OurLog,
50    TraceMetric,
51    Span,
52    ClientSdkInfo,
53
54    // Attachments and Contents
55    Minidump,
56    HeapMemory,
57    StackMemory,
58}
59
60impl ValueType {
61    pub fn for_field<T: ProcessValue>(field: &Annotated<T>) -> EnumSet<Self> {
62        field
63            .value()
64            .map(ProcessValue::value_type)
65            .unwrap_or_else(EnumSet::empty)
66    }
67}
68
69relay_common::derive_fromstr_and_display!(ValueType, UnknownValueTypeError, {
70    ValueType::String => "string",
71    ValueType::Binary => "binary",
72    ValueType::Number => "number",
73    ValueType::Boolean => "boolean" | "bool",
74    ValueType::DateTime => "datetime",
75    ValueType::Array => "array" | "list",
76    ValueType::Object => "object",
77    ValueType::Event => "event",
78    ValueType::Attachments => "attachments",
79    ValueType::Replay => "replay",
80    ValueType::Exception => "error" | "exception",
81    ValueType::Stacktrace => "stack" | "stacktrace",
82    ValueType::Frame => "frame",
83    ValueType::Request => "http" | "request",
84    ValueType::User => "user",
85    ValueType::LogEntry => "logentry",
86    ValueType::Message => "message",
87    ValueType::Thread => "thread",
88    ValueType::Breadcrumb => "breadcrumb",
89    ValueType::OurLog => "log",
90    ValueType::TraceMetric => "trace_metric",
91
92    ValueType::Span => "span",
93    ValueType::ClientSdkInfo => "sdk",
94    ValueType::Minidump => "minidump",
95    ValueType::HeapMemory => "heap_memory",
96    ValueType::StackMemory => "stack_memory",
97});
98
99/// Whether an attribute should be PII-strippable/should be subject to datascrubbers
100#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
101pub enum Pii {
102    /// The field will be stripped by default
103    True,
104    /// The field cannot be stripped at all
105    False,
106    /// The field will only be stripped when addressed with a specific path selector, but generic
107    /// selectors such as `$string` do not apply.
108    Maybe,
109}
110
111/// A static or dynamic `Pii` value.
112#[derive(Debug, Clone, Copy)]
113pub enum PiiMode {
114    /// A static value.
115    Static(Pii),
116    /// A dynamic value, computed based on a `ProcessingState`.
117    Dynamic(fn(&ProcessingState) -> Pii),
118}
119
120/// A static or dynamic Option<`usize`> value.
121///
122/// Used for the fields `max_chars` and `max_bytes`.
123#[derive(Debug, Clone, Copy)]
124pub enum SizeMode {
125    Static(Option<usize>),
126    Dynamic(fn(&ProcessingState) -> Option<usize>),
127}
128
129/// Whether a field must be present.
130#[derive(Debug, Clone, Copy)]
131pub enum Required {
132    /// The field is not required.
133    False,
134    /// The field requires a value or metadata.
135    ValueOrMeta,
136    /// The field requires a value.
137    Value,
138}
139
140/// Meta information about a field.
141#[derive(Debug, Clone, Copy)]
142pub struct FieldAttrs {
143    /// Optionally the name of the field.
144    pub name: Option<&'static str>,
145    /// If the field is required.
146    pub required: Required,
147    /// If the field should be non-empty.
148    pub nonempty: bool,
149    /// Whether to trim whitespace from this string.
150    pub trim_whitespace: bool,
151    /// A set of allowed or denied character ranges for this string.
152    pub characters: Option<CharacterSet>,
153    /// The maximum char length of this field.
154    pub max_chars: SizeMode,
155    /// The extra char length allowance on top of max_chars.
156    pub max_chars_allowance: usize,
157    /// The maximum depth of this field.
158    pub max_depth: Option<usize>,
159    /// The maximum number of bytes of this field.
160    pub max_bytes: SizeMode,
161    /// How this item's size is computed.
162    ///
163    /// There are two axes to this:
164    /// * `Static`/`Dynamic` denotes whether the value is fixed or computed based
165    ///   on the `ProcessingState`;
166    /// * `None` means a processor should use its default method to compute/estimate the size,
167    ///   `Some(size)` means the item should count as `size` bytes.
168    pub bytes_size: SizeMode,
169    /// The type of PII on the field.
170    pub pii: PiiMode,
171    /// Whether additional properties should be retained during normalization.
172    pub retain: bool,
173    /// Whether the trimming processor is allowed to shorten or drop this field.
174    pub trim: bool,
175}
176
177/// A set of characters allowed or denied for a (string) field.
178///
179/// Note that this field is generated in the derive, it can't be constructed easily in tests.
180#[derive(Clone, Copy)]
181pub struct CharacterSet {
182    /// Generated in derive for performance. Can be left out when set is created manually.
183    pub char_is_valid: fn(char) -> bool,
184    /// A set of ranges that are allowed/denied within the character set
185    pub ranges: &'static [RangeInclusive<char>],
186    /// Whether the character set is inverted
187    pub is_negative: bool,
188}
189
190impl fmt::Debug for CharacterSet {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        f.debug_struct("CharacterSet")
193            .field("ranges", &self.ranges)
194            .field("is_negative", &self.is_negative)
195            .finish()
196    }
197}
198
199impl FieldAttrs {
200    /// Creates default `FieldAttrs`.
201    pub const fn new() -> Self {
202        FieldAttrs {
203            name: None,
204            required: Required::False,
205            nonempty: false,
206            trim_whitespace: false,
207            characters: None,
208            max_chars: SizeMode::Static(None),
209            max_chars_allowance: 0,
210            max_depth: None,
211            max_bytes: SizeMode::Static(None),
212            pii: PiiMode::Static(Pii::False),
213            retain: false,
214            trim: true,
215            bytes_size: SizeMode::Static(None),
216        }
217    }
218
219    /// Sets whether a value in this field is required.
220    pub const fn required(mut self, required: Required) -> Self {
221        self.required = required;
222        self
223    }
224
225    /// Sets whether this field's value must be nonempty.
226    ///
227    /// This is distinct from `required`. An empty string (`""`) passes the "required" check but not the
228    /// "nonempty" one.
229    pub const fn nonempty(mut self, nonempty: bool) -> Self {
230        self.nonempty = nonempty;
231        self
232    }
233
234    /// Sets whether whitespace should be trimmed before validation.
235    pub const fn trim_whitespace(mut self, trim_whitespace: bool) -> Self {
236        self.trim_whitespace = trim_whitespace;
237        self
238    }
239
240    /// Sets whether this field contains PII.
241    pub const fn pii(mut self, pii: Pii) -> Self {
242        self.pii = PiiMode::Static(pii);
243        self
244    }
245
246    /// Sets whether this field contains PII dynamically based on the current state.
247    pub const fn pii_dynamic(mut self, pii: fn(&ProcessingState) -> Pii) -> Self {
248        self.pii = PiiMode::Dynamic(pii);
249        self
250    }
251
252    /// Sets whether this field should be trimmed.
253    pub const fn trim(mut self, trim: bool) -> Self {
254        self.trim = trim;
255        self
256    }
257
258    /// Sets the maximum number of characters allowed in the field.
259    pub const fn max_chars(mut self, max_chars: Option<usize>) -> Self {
260        self.max_chars = SizeMode::Static(max_chars);
261        self
262    }
263
264    /// Sets the maximum number of characters allowed in the field dynamically based on the current state.
265    pub const fn max_chars_dynamic(
266        mut self,
267        max_chars: fn(&ProcessingState) -> Option<usize>,
268    ) -> Self {
269        self.max_chars = SizeMode::Dynamic(max_chars);
270        self
271    }
272
273    /// Sets the maximum number of bytes allowed in the field.
274    pub const fn max_bytes(mut self, max_bytes: Option<usize>) -> Self {
275        self.max_bytes = SizeMode::Static(max_bytes);
276        self
277    }
278
279    /// Sets the maximum number of bytes allowed in the field dynamically based on the current state.
280    pub const fn max_bytes_dynamic(
281        mut self,
282        max_bytes: fn(&ProcessingState) -> Option<usize>,
283    ) -> Self {
284        self.max_bytes = SizeMode::Dynamic(max_bytes);
285        self
286    }
287
288    /// Sets whether additional properties should be retained during normalization.
289    pub const fn retain(mut self, retain: bool) -> Self {
290        self.retain = retain;
291        self
292    }
293}
294
295static DEFAULT_FIELD_ATTRS: FieldAttrs = FieldAttrs::new();
296
297impl Default for FieldAttrs {
298    fn default() -> Self {
299        Self::new()
300    }
301}
302
303#[derive(Debug, Clone, Eq, Ord, PartialOrd)]
304enum PathItem<'a> {
305    StaticKey(&'a str),
306    OwnedKey(String),
307    Index(usize),
308}
309
310impl<'a> PartialEq for PathItem<'a> {
311    fn eq(&self, other: &PathItem<'a>) -> bool {
312        self.key() == other.key() && self.index() == other.index()
313    }
314}
315
316impl PathItem<'_> {
317    /// Returns the key if there is one
318    #[inline]
319    pub fn key(&self) -> Option<&str> {
320        match self {
321            PathItem::StaticKey(s) => Some(s),
322            PathItem::OwnedKey(s) => Some(s.as_str()),
323            PathItem::Index(_) => None,
324        }
325    }
326
327    /// Returns the index if there is one
328    #[inline]
329    pub fn index(&self) -> Option<usize> {
330        match self {
331            PathItem::StaticKey(_) | PathItem::OwnedKey(_) => None,
332            PathItem::Index(idx) => Some(*idx),
333        }
334    }
335}
336
337impl fmt::Display for PathItem<'_> {
338    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339        match self {
340            PathItem::StaticKey(s) => f.pad(s),
341            PathItem::OwnedKey(s) => f.pad(s.as_str()),
342            PathItem::Index(val) => write!(f, "{val}"),
343        }
344    }
345}
346
347/// Like [`std::borrow::Cow`], but with a boxed value.
348///
349/// This is useful for types that contain themselves, where otherwise the layout of the type
350/// cannot be computed, for example
351///
352/// ```rust,ignore
353/// struct Foo<'a>(Cow<'a, Foo<'a>>); // will not compile
354/// struct Bar<'a>(BoxCow<'a, Bar<'a>>); // will compile
355/// ```
356#[derive(Debug, Clone)]
357enum BoxCow<'a, T> {
358    Borrowed(&'a T),
359    Owned(Box<T>),
360}
361
362impl<T> Deref for BoxCow<'_, T> {
363    type Target = T;
364
365    fn deref(&self) -> &Self::Target {
366        match self {
367            BoxCow::Borrowed(inner) => inner,
368            BoxCow::Owned(inner) => inner.deref(),
369        }
370    }
371}
372
373/// A builder for root [`ProcessingStates`](ProcessingState).
374///
375/// This is created by [`ProcessingState::root_builder`].
376#[derive(Debug, Clone)]
377pub struct ProcessingStateBuilder {
378    attrs: Option<FieldAttrs>,
379    value_type: EnumSet<ValueType>,
380}
381
382impl ProcessingStateBuilder {
383    /// Modifies the attributes of the root field.
384    pub fn attrs<F: FnOnce(FieldAttrs) -> FieldAttrs>(mut self, f: F) -> Self {
385        let attrs = self.attrs.take().unwrap_or_default();
386        self.attrs = Some(f(attrs));
387        self
388    }
389
390    /// Sets whether a value in the root field is required.
391    pub fn required(self, required: Required) -> Self {
392        self.attrs(|attrs| attrs.required(required))
393    }
394
395    /// Sets whether the root field's value must be nonempty.
396    ///
397    /// This is distinct from `required`. An empty string (`""`) passes the "required" check but not the
398    /// "nonempty" one.
399    pub fn nonempty(self, nonempty: bool) -> Self {
400        self.attrs(|attrs| attrs.nonempty(nonempty))
401    }
402
403    /// Sets whether whitespace should be trimmed on the root field before validation.
404    pub fn trim_whitespace(self, trim_whitespace: bool) -> Self {
405        self.attrs(|attrs| attrs.trim_whitespace(trim_whitespace))
406    }
407
408    /// Sets whether the root field contains PII.
409    pub fn pii(self, pii: Pii) -> Self {
410        self.attrs(|attrs| attrs.pii(pii))
411    }
412
413    /// Sets whether the root field contains PII dynamically based on the current state.
414    pub fn pii_dynamic(self, pii: fn(&ProcessingState) -> Pii) -> Self {
415        self.attrs(|attrs| attrs.pii_dynamic(pii))
416    }
417
418    /// Sets the maximum number of chars allowed in the root field.
419    pub fn max_chars(self, max_chars: impl Into<Option<usize>>) -> Self {
420        self.attrs(|attrs| attrs.max_chars(max_chars.into()))
421    }
422
423    /// Sets the maximum number of characters allowed in the root field dynamically based on the current state.
424    pub fn max_chars_dynamic(self, max_chars: fn(&ProcessingState) -> Option<usize>) -> Self {
425        self.attrs(|attrs| attrs.max_chars_dynamic(max_chars))
426    }
427
428    /// Sets the maximum number of bytes allowed in the root field.
429    pub fn max_bytes(self, max_bytes: impl Into<Option<usize>>) -> Self {
430        self.attrs(|attrs| attrs.max_bytes(max_bytes.into()))
431    }
432
433    /// Sets the maximum number of bytes allowed in the root field dynamically based on the current state.
434    pub fn max_bytes_dynamic(self, max_bytes: fn(&ProcessingState) -> Option<usize>) -> Self {
435        self.attrs(|attrs| attrs.max_bytes_dynamic(max_bytes))
436    }
437
438    /// Sets whether additional properties should be retained during normalization.
439    pub fn retain(self, retain: bool) -> Self {
440        self.attrs(|attrs| attrs.retain(retain))
441    }
442
443    /// Sets the value type for the root state.
444    pub fn value_type(mut self, value_type: EnumSet<ValueType>) -> Self {
445        self.value_type = value_type;
446        self
447    }
448
449    /// Consumes the builder and returns a root [`ProcessingState`] with
450    /// the configured attributes and value type.
451    pub fn build(self) -> ProcessingState<'static> {
452        let Self { attrs, value_type } = self;
453        ProcessingState {
454            parent: None,
455            path_item: None,
456            attrs: attrs.map(Cow::Owned),
457            value_type,
458            depth: 0,
459        }
460    }
461}
462
463/// An event's processing state.
464///
465/// The processing state describes an item in an event which is being processed, an example
466/// of processing might be scrubbing the event for PII.  The processing state itself
467/// describes the current item and it's parent, which allows you to follow all the items up
468/// to the root item.  You can think of processing an event as a visitor pattern visiting
469/// all items in the event and the processing state is a stack describing the currently
470/// visited item and all it's parents.
471#[derive(Debug, Clone)]
472pub struct ProcessingState<'a> {
473    // In event scrubbing, every state holds a reference to its parent.
474    // In Replay scrubbing, we do not call `process_*` recursively,
475    // but instead hold a single `ProcessingState` that represents the current item.
476    // This item owns its parent (plus ancestors) exclusively, which is why we use `BoxCow` here
477    // rather than `Rc` / `Arc`.
478    parent: Option<BoxCow<'a, ProcessingState<'a>>>,
479    path_item: Option<PathItem<'a>>,
480    attrs: Option<Cow<'a, FieldAttrs>>,
481    value_type: EnumSet<ValueType>,
482    depth: usize,
483}
484
485static ROOT_STATE: ProcessingState = ProcessingState {
486    parent: None,
487    path_item: None,
488    attrs: None,
489    value_type: enumset::enum_set!(),
490    depth: 0,
491};
492
493impl<'a> ProcessingState<'a> {
494    /// Returns the root processing state.
495    pub fn root() -> &'static ProcessingState<'static> {
496        &ROOT_STATE
497    }
498
499    /// Creates a new root state.
500    pub fn new_root(
501        attrs: Option<Cow<'static, FieldAttrs>>,
502        value_type: impl IntoIterator<Item = ValueType>,
503    ) -> ProcessingState<'static> {
504        ProcessingState {
505            parent: None,
506            path_item: None,
507            attrs,
508            value_type: value_type.into_iter().collect(),
509            depth: 0,
510        }
511    }
512
513    /// Creates a builder that can be used to easily create
514    /// a custom root state.
515    ///
516    /// # Example
517    /// ```
518    /// use relay_event_schema::processor::ProcessingState;
519    ///
520    /// let root = ProcessingState::root_builder()
521    ///   .max_bytes(50)
522    ///   .retain(true)
523    ///   .build();
524    /// ```
525    pub fn root_builder() -> ProcessingStateBuilder {
526        ProcessingStateBuilder {
527            attrs: None,
528            value_type: EnumSet::empty(),
529        }
530    }
531
532    /// Derives a processing state by entering a borrowed key.
533    pub fn enter_borrowed(
534        &'a self,
535        key: &'a str,
536        attrs: Option<Cow<'a, FieldAttrs>>,
537        value_type: impl IntoIterator<Item = ValueType>,
538    ) -> Self {
539        ProcessingState {
540            parent: Some(BoxCow::Borrowed(self)),
541            path_item: Some(PathItem::StaticKey(key)),
542            attrs,
543            value_type: value_type.into_iter().collect(),
544            depth: self.depth + 1,
545        }
546    }
547
548    /// Derives a processing state by entering an owned key.
549    ///
550    /// The new (child) state takes ownership of the current (parent) state.
551    pub fn enter_owned(
552        self,
553        key: String,
554        attrs: Option<Cow<'a, FieldAttrs>>,
555        value_type: impl IntoIterator<Item = ValueType>,
556    ) -> Self {
557        let depth = self.depth + 1;
558        ProcessingState {
559            parent: Some(BoxCow::Owned(self.into())),
560            path_item: Some(PathItem::OwnedKey(key)),
561            attrs,
562            value_type: value_type.into_iter().collect(),
563            depth,
564        }
565    }
566
567    /// Derives a processing state by entering an index.
568    pub fn enter_index(
569        &'a self,
570        idx: usize,
571        attrs: Option<Cow<'a, FieldAttrs>>,
572        value_type: impl IntoIterator<Item = ValueType>,
573    ) -> Self {
574        ProcessingState {
575            parent: Some(BoxCow::Borrowed(self)),
576            path_item: Some(PathItem::Index(idx)),
577            attrs,
578            value_type: value_type.into_iter().collect(),
579            depth: self.depth + 1,
580        }
581    }
582
583    /// Derives a processing state without adding a path segment. Useful in newtype structs.
584    pub fn enter_nothing(&'a self, attrs: Option<Cow<'a, FieldAttrs>>) -> Self {
585        ProcessingState {
586            attrs,
587            path_item: None,
588            parent: Some(BoxCow::Borrowed(self)),
589            ..self.clone()
590        }
591    }
592
593    /// Returns the path in the processing state.
594    pub fn path(&'a self) -> Path<'a> {
595        Path(self)
596    }
597
598    pub fn value_type(&self) -> EnumSet<ValueType> {
599        self.value_type
600    }
601
602    /// Returns the field attributes.
603    pub fn attrs(&self) -> &FieldAttrs {
604        match self.attrs {
605            Some(ref cow) => cow,
606            None => &DEFAULT_FIELD_ATTRS,
607        }
608    }
609
610    /// Derives the attrs for recursion.
611    pub fn inner_attrs(&self) -> Option<Cow<'_, FieldAttrs>> {
612        let current_attrs = self.attrs();
613        match (current_attrs.pii, current_attrs.trim) {
614            // Both are default -> None.
615            (PiiMode::Static(Pii::False), true) => None,
616            (PiiMode::Static(Pii::True), true) => {
617                static ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::True).trim(true);
618                Some(Cow::Borrowed(&ATTRS))
619            }
620            (PiiMode::Static(Pii::Maybe), true) => {
621                static ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::Maybe).trim(true);
622                Some(Cow::Borrowed(&ATTRS))
623            }
624            (PiiMode::Static(Pii::True), false) => {
625                static ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::True).trim(false);
626                Some(Cow::Borrowed(&ATTRS))
627            }
628            (PiiMode::Static(Pii::False), false) => {
629                static ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::False).trim(false);
630                Some(Cow::Borrowed(&ATTRS))
631            }
632            (PiiMode::Static(Pii::Maybe), false) => {
633                static ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::Maybe).trim(false);
634                Some(Cow::Borrowed(&ATTRS))
635            }
636            (PiiMode::Dynamic(f), trim) => {
637                Some(Cow::Owned(FieldAttrs::new().pii_dynamic(f).trim(trim)))
638            }
639        }
640    }
641
642    /// Returns the PII status for this state.
643    ///
644    /// If the state's `FieldAttrs` contain a fixed PII status,
645    /// it is returned. If they contain a dynamic PII status (a function),
646    /// it is applied to this state and the output returned.
647    pub fn pii(&self) -> Pii {
648        match self.attrs().pii {
649            PiiMode::Static(pii) => pii,
650            PiiMode::Dynamic(pii_fn) => pii_fn(self),
651        }
652    }
653
654    /// Returns the max bytes for this state.
655    ///
656    /// If the state's `FieldAttrs` contain a fixed `max_bytes` value,
657    /// it is returned. If they contain a dynamic `max_bytes` value (a function),
658    /// it is applied to this state and the output returned.
659    pub fn max_bytes(&self) -> Option<usize> {
660        match self.attrs().max_bytes {
661            SizeMode::Static(n) => n,
662            SizeMode::Dynamic(max_bytes_fn) => max_bytes_fn(self),
663        }
664    }
665
666    /// Returns the bytes size for this state.
667    ///
668    /// If the state's `FieldAttrs` contain a fixed `bytes_size` value,
669    /// it is returned. If they contain a dynamic `bytes_size` value (a function),
670    /// it is applied to this state and the output returned.
671    pub fn bytes_size(&self) -> Option<usize> {
672        match self.attrs().bytes_size {
673            SizeMode::Static(n) => n,
674            SizeMode::Dynamic(bytes_size_fn) => bytes_size_fn(self),
675        }
676    }
677
678    /// Returns the max chars for this state.
679    ///
680    /// If the state's `FieldAttrs` contain a fixed `max_chars` value,
681    /// it is returned. If they contain a dynamic `max_chars` value (a function),
682    /// it is applied to this state and the output returned.
683    pub fn max_chars(&self) -> Option<usize> {
684        match self.attrs().max_chars {
685            SizeMode::Static(n) => n,
686            SizeMode::Dynamic(max_chars_fn) => max_chars_fn(self),
687        }
688    }
689
690    /// Iterates through this state and all its ancestors up the hierarchy.
691    ///
692    /// This starts at the top of the stack of processing states and ends at the root.  Thus
693    /// the first item returned is the currently visited leaf of the event structure.
694    pub fn iter(&'a self) -> ProcessingStateIter<'a> {
695        ProcessingStateIter {
696            state: Some(self),
697            size: self.depth,
698        }
699    }
700
701    /// Returns the contained parent state.
702    ///
703    /// - Returns `Ok(None)` if the current state is the root.
704    /// - Returns `Err(self)` if the current state does not own the parent state.
705    #[expect(
706        clippy::result_large_err,
707        reason = "this method returns `self` in the error case"
708    )]
709    pub fn try_into_parent(self) -> Result<Option<Self>, Self> {
710        match self.parent {
711            Some(BoxCow::Borrowed(_)) => Err(self),
712            Some(BoxCow::Owned(parent)) => Ok(Some(*parent)),
713            None => Ok(None),
714        }
715    }
716
717    /// Return the depth (~ indentation level) of the currently processed value.
718    pub fn depth(&'a self) -> usize {
719        self.depth
720    }
721
722    /// Return whether the depth changed between parent and self.
723    ///
724    /// This is `false` when we entered a newtype struct.
725    pub fn entered_anything(&'a self) -> bool {
726        if let Some(parent) = &self.parent {
727            parent.depth() != self.depth()
728        } else {
729            true
730        }
731    }
732
733    /// Returns an iterator over the "keys" in this state,
734    /// in order from right to left (or innermost state to outermost).
735    pub fn keys(&self) -> impl Iterator<Item = &str> {
736        self.iter()
737            .filter_map(|state| state.path_item.as_ref())
738            .flat_map(|item| item.key())
739    }
740
741    /// Returns the last path item if there is one. Skips over "dummy" path segments that exist
742    /// because of newtypes.
743    #[inline]
744    fn path_item(&self) -> Option<&PathItem<'_>> {
745        for state in self.iter() {
746            if let Some(ref path_item) = state.path_item {
747                return Some(path_item);
748            }
749        }
750        None
751    }
752}
753
754pub struct ProcessingStateIter<'a> {
755    state: Option<&'a ProcessingState<'a>>,
756    size: usize,
757}
758
759impl<'a> Iterator for ProcessingStateIter<'a> {
760    type Item = &'a ProcessingState<'a>;
761
762    fn next(&mut self) -> Option<Self::Item> {
763        let current = self.state?;
764        self.state = current.parent.as_deref();
765        Some(current)
766    }
767
768    fn size_hint(&self) -> (usize, Option<usize>) {
769        (self.size, Some(self.size))
770    }
771}
772
773impl ExactSizeIterator for ProcessingStateIter<'_> {}
774
775impl Default for ProcessingState<'_> {
776    fn default() -> Self {
777        ProcessingState::root().clone()
778    }
779}
780
781/// Represents the [`ProcessingState`] as a path.
782///
783/// This is a view of a [`ProcessingState`] which treats the stack of states as a path.
784#[derive(Debug)]
785pub struct Path<'a>(&'a ProcessingState<'a>);
786
787impl Path<'_> {
788    /// Returns the current key if there is one
789    #[inline]
790    pub fn key(&self) -> Option<&str> {
791        PathItem::key(self.0.path_item()?)
792    }
793
794    /// Returns the current index if there is one
795    #[inline]
796    pub fn index(&self) -> Option<usize> {
797        PathItem::index(self.0.path_item()?)
798    }
799
800    /// Return the depth (~ indentation level) of the currently processed value.
801    pub fn depth(&self) -> usize {
802        self.0.depth()
803    }
804
805    /// Returns the field attributes of the current path item.
806    pub fn attrs(&self) -> &FieldAttrs {
807        self.0.attrs()
808    }
809
810    /// Returns the PII status for this path.
811    pub fn pii(&self) -> Pii {
812        self.0.pii()
813    }
814
815    /// Iterates through the states in this path.
816    pub fn iter(&self) -> ProcessingStateIter<'_> {
817        self.0.iter()
818    }
819}
820
821impl fmt::Display for Path<'_> {
822    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
823        let mut items = Vec::with_capacity(self.0.depth);
824        for state in self.0.iter() {
825            if let Some(ref path_item) = state.path_item {
826                items.push(path_item)
827            }
828        }
829
830        for (idx, item) in items.into_iter().rev().enumerate() {
831            if idx > 0 {
832                write!(f, ".")?;
833            }
834            write!(f, "{item}")?;
835        }
836        Ok(())
837    }
838}
839
840#[cfg(test)]
841mod tests {
842
843    use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, SerializableAnnotated};
844
845    use crate::processor::attrs::ROOT_STATE;
846    use crate::processor::{Pii, ProcessValue, ProcessingState, Processor, process_value};
847
848    fn pii_from_item_name(state: &ProcessingState) -> Pii {
849        match state.path_item().and_then(|p| p.key()) {
850            Some("true_item") => Pii::True,
851            Some("false_item") => Pii::False,
852            _ => Pii::Maybe,
853        }
854    }
855
856    fn max_chars_from_item_name(state: &ProcessingState) -> Option<usize> {
857        match state.path_item().and_then(|p| p.key()) {
858            Some("short_item") => Some(10),
859            Some("long_item") => Some(20),
860            _ => None,
861        }
862    }
863
864    #[derive(Debug, Clone, Empty, IntoValue, FromValue, ProcessValue)]
865    #[metastructure(pii = "pii_from_item_name")]
866    struct TestValue(#[metastructure(max_chars = "max_chars_from_item_name")] String);
867
868    struct TestPiiProcessor;
869
870    impl Processor for TestPiiProcessor {
871        fn process_string(
872            &mut self,
873            value: &mut String,
874            _meta: &mut relay_protocol::Meta,
875            state: &ProcessingState<'_>,
876        ) -> crate::processor::ProcessingResult where {
877            match state.pii() {
878                Pii::True => *value = "true".to_owned(),
879                Pii::False => *value = "false".to_owned(),
880                Pii::Maybe => *value = "maybe".to_owned(),
881            }
882            Ok(())
883        }
884    }
885
886    struct TestTrimmingProcessor;
887
888    impl Processor for TestTrimmingProcessor {
889        fn process_string(
890            &mut self,
891            value: &mut String,
892            _meta: &mut relay_protocol::Meta,
893            state: &ProcessingState<'_>,
894        ) -> crate::processor::ProcessingResult where {
895            if let Some(n) = state.max_chars() {
896                value.truncate(n);
897            }
898            Ok(())
899        }
900    }
901
902    #[test]
903    fn test_dynamic_pii() {
904        let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
905            r#"
906        {
907          "false_item": "replace me",
908          "other_item": "replace me",
909          "true_item": "replace me"
910        }
911        "#,
912        )
913        .unwrap();
914
915        process_value(&mut object, &mut TestPiiProcessor, &ROOT_STATE).unwrap();
916
917        insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
918        {
919          "false_item": "false",
920          "other_item": "maybe",
921          "true_item": "true"
922        }
923        "###);
924    }
925
926    #[test]
927    fn test_dynamic_max_chars() {
928        let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
929            r#"
930        {
931          "short_item": "Should be shortened to 10",
932          "long_item": "Should be shortened to 20",
933          "other_item": "Should not be shortened at all"
934        }
935        "#,
936        )
937        .unwrap();
938
939        process_value(&mut object, &mut TestTrimmingProcessor, &ROOT_STATE).unwrap();
940
941        insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
942        {
943          "long_item": "Should be shortened ",
944          "other_item": "Should not be shortened at all",
945          "short_item": "Should be "
946        }
947        "###);
948    }
949}