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.pii() {
598            Pii::True => Some(Cow::Borrowed(&PII_TRUE_FIELD_ATTRS)),
599            Pii::False => None,
600            Pii::Maybe => Some(Cow::Borrowed(&PII_MAYBE_FIELD_ATTRS)),
601        }
602    }
603
604    /// Returns the PII status for this state.
605    ///
606    /// If the state's `FieldAttrs` contain a fixed PII status,
607    /// it is returned. If they contain a dynamic PII status (a function),
608    /// it is applied to this state and the output returned.
609    pub fn pii(&self) -> Pii {
610        match self.attrs().pii {
611            PiiMode::Static(pii) => pii,
612            PiiMode::Dynamic(pii_fn) => pii_fn(self),
613        }
614    }
615
616    /// Returns the max bytes for this state.
617    ///
618    /// If the state's `FieldAttrs` contain a fixed `max_bytes` value,
619    /// it is returned. If they contain a dynamic `max_bytes` value (a function),
620    /// it is applied to this state and the output returned.
621    pub fn max_bytes(&self) -> Option<usize> {
622        match self.attrs().max_bytes {
623            SizeMode::Static(n) => n,
624            SizeMode::Dynamic(max_bytes_fn) => max_bytes_fn(self),
625        }
626    }
627
628    /// Returns the bytes size for this state.
629    ///
630    /// If the state's `FieldAttrs` contain a fixed `bytes_size` value,
631    /// it is returned. If they contain a dynamic `bytes_size` value (a function),
632    /// it is applied to this state and the output returned.
633    pub fn bytes_size(&self) -> Option<usize> {
634        match self.attrs().bytes_size {
635            SizeMode::Static(n) => n,
636            SizeMode::Dynamic(bytes_size_fn) => bytes_size_fn(self),
637        }
638    }
639
640    /// Returns the max chars for this state.
641    ///
642    /// If the state's `FieldAttrs` contain a fixed `max_chars` value,
643    /// it is returned. If they contain a dynamic `max_chars` value (a function),
644    /// it is applied to this state and the output returned.
645    pub fn max_chars(&self) -> Option<usize> {
646        match self.attrs().max_chars {
647            SizeMode::Static(n) => n,
648            SizeMode::Dynamic(max_chars_fn) => max_chars_fn(self),
649        }
650    }
651
652    /// Iterates through this state and all its ancestors up the hierarchy.
653    ///
654    /// This starts at the top of the stack of processing states and ends at the root.  Thus
655    /// the first item returned is the currently visited leaf of the event structure.
656    pub fn iter(&'a self) -> ProcessingStateIter<'a> {
657        ProcessingStateIter {
658            state: Some(self),
659            size: self.depth,
660        }
661    }
662
663    /// Returns the contained parent state.
664    ///
665    /// - Returns `Ok(None)` if the current state is the root.
666    /// - Returns `Err(self)` if the current state does not own the parent state.
667    #[expect(
668        clippy::result_large_err,
669        reason = "this method returns `self` in the error case"
670    )]
671    pub fn try_into_parent(self) -> Result<Option<Self>, Self> {
672        match self.parent {
673            Some(BoxCow::Borrowed(_)) => Err(self),
674            Some(BoxCow::Owned(parent)) => Ok(Some(*parent)),
675            None => Ok(None),
676        }
677    }
678
679    /// Return the depth (~ indentation level) of the currently processed value.
680    pub fn depth(&'a self) -> usize {
681        self.depth
682    }
683
684    /// Return whether the depth changed between parent and self.
685    ///
686    /// This is `false` when we entered a newtype struct.
687    pub fn entered_anything(&'a self) -> bool {
688        if let Some(parent) = &self.parent {
689            parent.depth() != self.depth()
690        } else {
691            true
692        }
693    }
694
695    /// Returns an iterator over the "keys" in this state,
696    /// in order from right to left (or innermost state to outermost).
697    pub fn keys(&self) -> impl Iterator<Item = &str> {
698        self.iter()
699            .filter_map(|state| state.path_item.as_ref())
700            .flat_map(|item| item.key())
701    }
702
703    /// Returns the last path item if there is one. Skips over "dummy" path segments that exist
704    /// because of newtypes.
705    #[inline]
706    fn path_item(&self) -> Option<&PathItem<'_>> {
707        for state in self.iter() {
708            if let Some(ref path_item) = state.path_item {
709                return Some(path_item);
710            }
711        }
712        None
713    }
714}
715
716pub struct ProcessingStateIter<'a> {
717    state: Option<&'a ProcessingState<'a>>,
718    size: usize,
719}
720
721impl<'a> Iterator for ProcessingStateIter<'a> {
722    type Item = &'a ProcessingState<'a>;
723
724    fn next(&mut self) -> Option<Self::Item> {
725        let current = self.state?;
726        self.state = current.parent.as_deref();
727        Some(current)
728    }
729
730    fn size_hint(&self) -> (usize, Option<usize>) {
731        (self.size, Some(self.size))
732    }
733}
734
735impl ExactSizeIterator for ProcessingStateIter<'_> {}
736
737impl Default for ProcessingState<'_> {
738    fn default() -> Self {
739        ProcessingState::root().clone()
740    }
741}
742
743/// Represents the [`ProcessingState`] as a path.
744///
745/// This is a view of a [`ProcessingState`] which treats the stack of states as a path.
746#[derive(Debug)]
747pub struct Path<'a>(&'a ProcessingState<'a>);
748
749impl Path<'_> {
750    /// Returns the current key if there is one
751    #[inline]
752    pub fn key(&self) -> Option<&str> {
753        PathItem::key(self.0.path_item()?)
754    }
755
756    /// Returns the current index if there is one
757    #[inline]
758    pub fn index(&self) -> Option<usize> {
759        PathItem::index(self.0.path_item()?)
760    }
761
762    /// Return the depth (~ indentation level) of the currently processed value.
763    pub fn depth(&self) -> usize {
764        self.0.depth()
765    }
766
767    /// Returns the field attributes of the current path item.
768    pub fn attrs(&self) -> &FieldAttrs {
769        self.0.attrs()
770    }
771
772    /// Returns the PII status for this path.
773    pub fn pii(&self) -> Pii {
774        self.0.pii()
775    }
776
777    /// Iterates through the states in this path.
778    pub fn iter(&self) -> ProcessingStateIter<'_> {
779        self.0.iter()
780    }
781}
782
783impl fmt::Display for Path<'_> {
784    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
785        let mut items = Vec::with_capacity(self.0.depth);
786        for state in self.0.iter() {
787            if let Some(ref path_item) = state.path_item {
788                items.push(path_item)
789            }
790        }
791
792        for (idx, item) in items.into_iter().rev().enumerate() {
793            if idx > 0 {
794                write!(f, ".")?;
795            }
796            write!(f, "{item}")?;
797        }
798        Ok(())
799    }
800}
801
802#[cfg(test)]
803mod tests {
804
805    use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, SerializableAnnotated};
806
807    use crate::processor::attrs::ROOT_STATE;
808    use crate::processor::{Pii, ProcessValue, ProcessingState, Processor, process_value};
809
810    fn pii_from_item_name(state: &ProcessingState) -> Pii {
811        match state.path_item().and_then(|p| p.key()) {
812            Some("true_item") => Pii::True,
813            Some("false_item") => Pii::False,
814            _ => Pii::Maybe,
815        }
816    }
817
818    fn max_chars_from_item_name(state: &ProcessingState) -> Option<usize> {
819        match state.path_item().and_then(|p| p.key()) {
820            Some("short_item") => Some(10),
821            Some("long_item") => Some(20),
822            _ => None,
823        }
824    }
825
826    #[derive(Debug, Clone, Empty, IntoValue, FromValue, ProcessValue)]
827    #[metastructure(pii = "pii_from_item_name")]
828    struct TestValue(#[metastructure(max_chars = "max_chars_from_item_name")] String);
829
830    struct TestPiiProcessor;
831
832    impl Processor for TestPiiProcessor {
833        fn process_string(
834            &mut self,
835            value: &mut String,
836            _meta: &mut relay_protocol::Meta,
837            state: &ProcessingState<'_>,
838        ) -> crate::processor::ProcessingResult where {
839            match state.pii() {
840                Pii::True => *value = "true".to_owned(),
841                Pii::False => *value = "false".to_owned(),
842                Pii::Maybe => *value = "maybe".to_owned(),
843            }
844            Ok(())
845        }
846    }
847
848    struct TestTrimmingProcessor;
849
850    impl Processor for TestTrimmingProcessor {
851        fn process_string(
852            &mut self,
853            value: &mut String,
854            _meta: &mut relay_protocol::Meta,
855            state: &ProcessingState<'_>,
856        ) -> crate::processor::ProcessingResult where {
857            if let Some(n) = state.max_chars() {
858                value.truncate(n);
859            }
860            Ok(())
861        }
862    }
863
864    #[test]
865    fn test_dynamic_pii() {
866        let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
867            r#"
868        {
869          "false_item": "replace me",
870          "other_item": "replace me",
871          "true_item": "replace me"
872        }
873        "#,
874        )
875        .unwrap();
876
877        process_value(&mut object, &mut TestPiiProcessor, &ROOT_STATE).unwrap();
878
879        insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
880        {
881          "false_item": "false",
882          "other_item": "maybe",
883          "true_item": "true"
884        }
885        "###);
886    }
887
888    #[test]
889    fn test_dynamic_max_chars() {
890        let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
891            r#"
892        {
893          "short_item": "Should be shortened to 10",
894          "long_item": "Should be shortened to 20",
895          "other_item": "Should not be shortened at all"
896        }
897        "#,
898        )
899        .unwrap();
900
901        process_value(&mut object, &mut TestTrimmingProcessor, &ROOT_STATE).unwrap();
902
903        insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
904        {
905          "long_item": "Should be shortened ",
906          "other_item": "Should not be shortened at all",
907          "short_item": "Should be "
908        }
909        "###);
910    }
911}