From 9167f3e264b32283332c3ac857d1f4ee780c2d22 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Fri, 6 Feb 2026 12:16:00 +0100 Subject: [PATCH 1/3] feat: Introduce builder for `ProcessingState` Setting attributes on the a root `ProcessingState` is quite cumbersome. This PR introduces a `ProcessingStateBuilder` that makes it more ergonomic. To facilitate this it also changes the type signatures of some methods on `FieldAttrs`. --- relay-event-normalization/src/eap/trimming.rs | 9 +- relay-event-schema/src/processor/attrs.rs | 82 ++++++++++++++++++- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/relay-event-normalization/src/eap/trimming.rs b/relay-event-normalization/src/eap/trimming.rs index b5eac1b8e3..6c47756a12 100644 --- a/relay-event-normalization/src/eap/trimming.rs +++ b/relay-event-normalization/src/eap/trimming.rs @@ -716,8 +716,7 @@ mod tests { // That leaves 9B for the string's value. // Note that the `number` field doesn't take up any size. // The `"footer"` is removed because it comes after the attributes and there's no space left. - let state = - ProcessingState::new_root(Some(Cow::Owned(FieldAttrs::default().max_bytes(50))), []); + let state = ProcessingState::root_builder().max_bytes(Some(50)).build(); processor::process_value(&mut value, &mut processor, &state).unwrap(); insta::assert_json_snapshot!(SerializableAnnotated(&value), @r###" @@ -883,8 +882,7 @@ mod tests { }); let mut processor = TrimmingProcessor::new(100); - let state = - ProcessingState::new_root(Some(Cow::Owned(FieldAttrs::default().max_bytes(30))), []); + let state = ProcessingState::root_builder().max_bytes(Some(30)).build(); processor::process_value(&mut value, &mut processor, &state).unwrap(); insta::assert_json_snapshot!(SerializableAnnotated(&value), @r###" @@ -937,8 +935,7 @@ mod tests { let mut attributes = Annotated::new(attributes); - let state = - ProcessingState::new_root(Some(Cow::Owned(FieldAttrs::default().max_bytes(40))), []); + let state = ProcessingState::root_builder().max_bytes(Some(40)).build(); processor::process_value(&mut attributes, &mut TrimmingProcessor::new(20), &state).unwrap(); let attributes_after_trimming = attributes.clone(); processor::process_value(&mut attributes, &mut TrimmingProcessor::new(20), &state).unwrap(); diff --git a/relay-event-schema/src/processor/attrs.rs b/relay-event-schema/src/processor/attrs.rs index 7b55aa4f6d..10055586ab 100644 --- a/relay-event-schema/src/processor/attrs.rs +++ b/relay-event-schema/src/processor/attrs.rs @@ -239,8 +239,8 @@ impl FieldAttrs { } /// Sets the maximum number of characters allowed in the field. - pub const fn max_chars(mut self, max_chars: usize) -> Self { - self.max_chars = SizeMode::Static(Some(max_chars)); + pub const fn max_chars(mut self, max_chars: Option) -> Self { + self.max_chars = SizeMode::Static(max_chars); self } @@ -254,8 +254,8 @@ impl FieldAttrs { } /// Sets the maximum number of bytes allowed in the field. - pub const fn max_bytes(mut self, max_bytes: usize) -> Self { - self.max_bytes = SizeMode::Static(Some(max_bytes)); + pub const fn max_bytes(mut self, max_bytes: Option) -> Self { + self.max_bytes = SizeMode::Static(max_bytes); self } @@ -355,6 +355,76 @@ impl Deref for BoxCow<'_, T> { } } +#[derive(Debug, Clone, Default)] +pub struct ProcessingStateBuilder { + attrs: Option, + value_type: EnumSet, +} + +impl ProcessingStateBuilder { + pub fn attrs FieldAttrs>(mut self, f: F) -> Self { + let attrs = self.attrs.take().unwrap_or_default(); + self.attrs = Some(f(attrs)); + self + } + + pub fn required(self, required: bool) -> Self { + self.attrs(|attrs| attrs.required(required)) + } + + pub fn nonempty(self, nonempty: bool) -> Self { + self.attrs(|attrs| attrs.nonempty(nonempty)) + } + + pub fn trim_whitespace(self, trim_whitespace: bool) -> Self { + self.attrs(|attrs| attrs.trim_whitespace(trim_whitespace)) + } + + pub fn pii(self, pii: Pii) -> Self { + self.attrs(|attrs| attrs.pii(pii)) + } + + pub fn pii_dynamic(self, pii: fn(&ProcessingState) -> Pii) -> Self { + self.attrs(|attrs| attrs.pii_dynamic(pii)) + } + + pub fn max_chars(self, max_chars: Option) -> Self { + self.attrs(|attrs| attrs.max_chars(max_chars)) + } + + pub fn max_chars_dynamic(self, max_chars: fn(&ProcessingState) -> Option) -> Self { + self.attrs(|attrs| attrs.max_chars_dynamic(max_chars)) + } + + pub fn max_bytes(self, max_bytes: Option) -> Self { + self.attrs(|attrs| attrs.max_bytes(max_bytes)) + } + + pub fn max_bytes_dynamic(self, max_bytes: fn(&ProcessingState) -> Option) -> Self { + self.attrs(|attrs| attrs.max_bytes_dynamic(max_bytes)) + } + + pub fn retain(self, retain: bool) -> Self { + self.attrs(|attrs| attrs.retain(retain)) + } + + pub fn value_type(mut self, value_type: EnumSet) -> Self { + self.value_type = value_type; + self + } + + pub fn build(self) -> ProcessingState<'static> { + let Self { attrs, value_type } = self; + ProcessingState { + parent: None, + path_item: None, + attrs: attrs.map(Cow::Owned), + value_type, + depth: 0, + } + } +} + /// An event's processing state. /// /// The processing state describes an item in an event which is being processed, an example @@ -405,6 +475,10 @@ impl<'a> ProcessingState<'a> { } } + pub fn root_builder() -> ProcessingStateBuilder { + ProcessingStateBuilder::default() + } + /// Derives a processing state by entering a borrowed key. pub fn enter_borrowed( &'a self, From 8ec395a7613aacada829c0088e50f5e1d2026265 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Fri, 6 Feb 2026 12:42:44 +0100 Subject: [PATCH 2/3] Use impl Into> --- relay-event-normalization/src/eap/trimming.rs | 6 +++--- relay-event-schema/src/processor/attrs.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/relay-event-normalization/src/eap/trimming.rs b/relay-event-normalization/src/eap/trimming.rs index 6c47756a12..58bde9d05c 100644 --- a/relay-event-normalization/src/eap/trimming.rs +++ b/relay-event-normalization/src/eap/trimming.rs @@ -716,7 +716,7 @@ mod tests { // That leaves 9B for the string's value. // Note that the `number` field doesn't take up any size. // The `"footer"` is removed because it comes after the attributes and there's no space left. - let state = ProcessingState::root_builder().max_bytes(Some(50)).build(); + let state = ProcessingState::root_builder().max_bytes(50).build(); processor::process_value(&mut value, &mut processor, &state).unwrap(); insta::assert_json_snapshot!(SerializableAnnotated(&value), @r###" @@ -882,7 +882,7 @@ mod tests { }); let mut processor = TrimmingProcessor::new(100); - let state = ProcessingState::root_builder().max_bytes(Some(30)).build(); + let state = ProcessingState::root_builder().max_bytes(30).build(); processor::process_value(&mut value, &mut processor, &state).unwrap(); insta::assert_json_snapshot!(SerializableAnnotated(&value), @r###" @@ -935,7 +935,7 @@ mod tests { let mut attributes = Annotated::new(attributes); - let state = ProcessingState::root_builder().max_bytes(Some(40)).build(); + let state = ProcessingState::root_builder().max_bytes(40).build(); processor::process_value(&mut attributes, &mut TrimmingProcessor::new(20), &state).unwrap(); let attributes_after_trimming = attributes.clone(); processor::process_value(&mut attributes, &mut TrimmingProcessor::new(20), &state).unwrap(); diff --git a/relay-event-schema/src/processor/attrs.rs b/relay-event-schema/src/processor/attrs.rs index 10055586ab..4dd73c4e45 100644 --- a/relay-event-schema/src/processor/attrs.rs +++ b/relay-event-schema/src/processor/attrs.rs @@ -388,16 +388,16 @@ impl ProcessingStateBuilder { self.attrs(|attrs| attrs.pii_dynamic(pii)) } - pub fn max_chars(self, max_chars: Option) -> Self { - self.attrs(|attrs| attrs.max_chars(max_chars)) + pub fn max_chars(self, max_chars: impl Into>) -> Self { + self.attrs(|attrs| attrs.max_chars(max_chars.into())) } pub fn max_chars_dynamic(self, max_chars: fn(&ProcessingState) -> Option) -> Self { self.attrs(|attrs| attrs.max_chars_dynamic(max_chars)) } - pub fn max_bytes(self, max_bytes: Option) -> Self { - self.attrs(|attrs| attrs.max_bytes(max_bytes)) + pub fn max_bytes(self, max_bytes: impl Into>) -> Self { + self.attrs(|attrs| attrs.max_bytes(max_bytes.into())) } pub fn max_bytes_dynamic(self, max_bytes: fn(&ProcessingState) -> Option) -> Self { From 9912a6026bffc6ba91704ecdf879e0c7b52eaac2 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Fri, 6 Feb 2026 13:00:16 +0100 Subject: [PATCH 3/3] Docs, slight ref --- relay-event-schema/src/processor/attrs.rs | 41 +++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/relay-event-schema/src/processor/attrs.rs b/relay-event-schema/src/processor/attrs.rs index 4dd73c4e45..fec2145a20 100644 --- a/relay-event-schema/src/processor/attrs.rs +++ b/relay-event-schema/src/processor/attrs.rs @@ -211,7 +211,7 @@ impl FieldAttrs { self } - /// Sets whether this field can have an empty value. + /// Sets whether this field's value must be nonempty. /// /// This is distinct from `required`. An empty string (`""`) passes the "required" check but not the /// "nonempty" one. @@ -355,64 +355,84 @@ impl Deref for BoxCow<'_, T> { } } -#[derive(Debug, Clone, Default)] +/// A builder for root [`ProcessingStates`](ProcessingState). +/// +/// This is created by [`ProcessingState::root_builder`]. +#[derive(Debug, Clone)] pub struct ProcessingStateBuilder { attrs: Option, value_type: EnumSet, } impl ProcessingStateBuilder { + /// Modifies the attributes of the root field. pub fn attrs FieldAttrs>(mut self, f: F) -> Self { let attrs = self.attrs.take().unwrap_or_default(); self.attrs = Some(f(attrs)); self } + /// Sets whether a value in the root field is required. pub fn required(self, required: bool) -> Self { self.attrs(|attrs| attrs.required(required)) } + /// Sets whether the root field's value must be nonempty. + /// + /// This is distinct from `required`. An empty string (`""`) passes the "required" check but not the + /// "nonempty" one. pub fn nonempty(self, nonempty: bool) -> Self { self.attrs(|attrs| attrs.nonempty(nonempty)) } + /// Sets whether whitespace should be trimmed on the root field before validation. pub fn trim_whitespace(self, trim_whitespace: bool) -> Self { self.attrs(|attrs| attrs.trim_whitespace(trim_whitespace)) } + /// Sets whether the root field contains PII. pub fn pii(self, pii: Pii) -> Self { self.attrs(|attrs| attrs.pii(pii)) } + /// Sets whether the root field contains PII dynamically based on the current state. pub fn pii_dynamic(self, pii: fn(&ProcessingState) -> Pii) -> Self { self.attrs(|attrs| attrs.pii_dynamic(pii)) } + /// Sets the maximum number of chars allowed in the root field. pub fn max_chars(self, max_chars: impl Into>) -> Self { self.attrs(|attrs| attrs.max_chars(max_chars.into())) } + /// Sets the maximum number of characters allowed in the root field dynamically based on the current state. pub fn max_chars_dynamic(self, max_chars: fn(&ProcessingState) -> Option) -> Self { self.attrs(|attrs| attrs.max_chars_dynamic(max_chars)) } + /// Sets the maximum number of bytes allowed in the root field. pub fn max_bytes(self, max_bytes: impl Into>) -> Self { self.attrs(|attrs| attrs.max_bytes(max_bytes.into())) } + /// Sets the maximum number of bytes allowed in the root field dynamically based on the current state. pub fn max_bytes_dynamic(self, max_bytes: fn(&ProcessingState) -> Option) -> Self { self.attrs(|attrs| attrs.max_bytes_dynamic(max_bytes)) } + /// Sets whether additional properties should be retained during normalization. pub fn retain(self, retain: bool) -> Self { self.attrs(|attrs| attrs.retain(retain)) } + /// Sets the value type for the root state. pub fn value_type(mut self, value_type: EnumSet) -> Self { self.value_type = value_type; self } + /// Consumes the builder and returns a root [`ProcessingState`] with + /// the configured attributes and value type. pub fn build(self) -> ProcessingState<'static> { let Self { attrs, value_type } = self; ProcessingState { @@ -475,8 +495,23 @@ impl<'a> ProcessingState<'a> { } } + /// Creates a builder that can be used to easily create + /// a custom root state. + /// + /// # Example + /// ``` + /// use relay_event_schema::processor::ProcessingState; + /// + /// let root = ProcessingState::root_builder() + /// .max_bytes(50) + /// .retain(true) + /// .build(); + /// ``` pub fn root_builder() -> ProcessingStateBuilder { - ProcessingStateBuilder::default() + ProcessingStateBuilder { + attrs: None, + value_type: EnumSet::empty(), + } } /// Derives a processing state by entering a borrowed key.