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