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