Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 62 additions & 30 deletions relay-event-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, quote};
use syn::meta::ParseNestedMeta;
use syn::parse::ParseStream;
use syn::{ExprPath, Ident, Lit, LitBool, LitInt, LitStr};
use synstructure::decl_derive;

Expand Down Expand Up @@ -284,8 +284,7 @@ fn parse_type_attributes(s: &synstructure::Structure<'_>) -> syn::Result<TypeAtt
let s = meta.value()?.parse::<LitBool>()?;
rv.trim = Some(s.value());
} else if ident == "pii" {
let s = meta.value()?.parse::<LitStr>()?;
rv.pii = parse_pii_value(s, &meta)?;
rv.pii = Some(meta.value()?.parse()?);
} else {
// Ignore other attributes used by `relay-protocol-derive`.
if !meta.input.peek(syn::Token![,]) {
Expand All @@ -308,19 +307,66 @@ enum Pii {
Dynamic(ExprPath),
}

impl syn::parse::Parse for Pii {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just an improvement over the ad-hoc function, or did you need it for something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more a stylistic thing; I saw there's a trait in syn for exactly this use case and decided to implement it.

fn parse(input: ParseStream) -> syn::Result<Self> {
let head = input.fork();
let value = input.parse::<LitStr>()?;
let pii = match value.value().as_str() {
"true" => Self::True,
"false" => Self::False,
"maybe" => Self::Maybe,
_ => Self::Dynamic(value.parse().map_err(|_| {
head.error("Expected one of `true`, `false`, `maybe`, or a function name")
})?),
};
Ok(pii)
}
}

impl Pii {
fn as_tokens(&self) -> TokenStream {
match self {
Pii::True => quote!(::relay_event_schema::processor::PiiMode::Static(
Self::True => quote!(::relay_event_schema::processor::PiiMode::Static(
::relay_event_schema::processor::Pii::True
)),
Pii::False => quote!(::relay_event_schema::processor::PiiMode::Static(
Self::False => quote!(::relay_event_schema::processor::PiiMode::Static(
::relay_event_schema::processor::Pii::False
)),
Pii::Maybe => quote!(::relay_event_schema::processor::PiiMode::Static(
Self::Maybe => quote!(::relay_event_schema::processor::PiiMode::Static(
::relay_event_schema::processor::Pii::Maybe
)),
Pii::Dynamic(fun) => quote!(::relay_event_schema::processor::PiiMode::Dynamic(#fun)),
Self::Dynamic(fun) => quote!(::relay_event_schema::processor::PiiMode::Dynamic(#fun)),
}
}
}

#[derive(Clone, Debug)]
enum Size {
Static(usize),
Dynamic(ExprPath),
}

impl syn::parse::Parse for Size {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.peek(LitInt) {
let lit = input.parse::<LitInt>()?;
return Ok(Self::Static(lit.base10_parse()?));
}

let head = input.fork();
let path = input
.parse::<LitStr>()?
.parse()
.map_err(|_| head.error("Expected a function name"))?;
Ok(Self::Dynamic(path))
}
}

impl Size {
fn as_tokens(&self) -> TokenStream {
match self {
Self::Static(n) => quote!(::relay_event_schema::processor::SizeMode::Static(Some(#n))),
Self::Dynamic(fun) => quote!(::relay_event_schema::processor::SizeMode::Dynamic(#fun)),
}
}
}
Expand All @@ -337,10 +383,10 @@ struct FieldAttrs {
pii: Option<Pii>,
retain: bool,
characters: Option<TokenStream>,
max_chars: Option<TokenStream>,
max_chars: Option<Size>,
max_chars_allowance: Option<TokenStream>,
max_depth: Option<TokenStream>,
max_bytes: Option<TokenStream>,
max_bytes: Option<Size>,
trim: Option<bool>,
}

Expand Down Expand Up @@ -402,11 +448,11 @@ impl FieldAttrs {
let retain = self.retain;

let max_chars = if let Some(ref max_chars) = self.max_chars {
quote!(Some(#max_chars))
max_chars.as_tokens()
} else if let Some(ref parent_attrs) = inherit_from_field_attrs {
quote!(#parent_attrs.max_chars)
} else {
quote!(None)
quote!(::relay_event_schema::processor::SizeMode::Static(None))
};

let max_chars_allowance = if let Some(ref max_chars_allowance) = self.max_chars_allowance {
Expand All @@ -426,11 +472,11 @@ impl FieldAttrs {
};

let max_bytes = if let Some(ref max_bytes) = self.max_bytes {
quote!(Some(#max_bytes))
max_bytes.as_tokens()
} else if let Some(ref parent_attrs) = inherit_from_field_attrs {
quote!(#parent_attrs.max_bytes)
} else {
quote!(None)
quote!(::relay_event_schema::processor::SizeMode::Static(None))
};

let characters = if let Some(ref characters) = self.characters {
Expand Down Expand Up @@ -513,20 +559,17 @@ fn parse_field_attributes(
let s = meta.value()?.parse::<LitStr>()?;
rv.characters = Some(parse_character_set(ident, &s.value()));
} else if ident == "max_chars" {
let s = meta.value()?.parse::<LitInt>()?;
rv.max_chars = Some(quote!(#s));
rv.max_chars = Some(meta.value()?.parse()?);
} else if ident == "max_chars_allowance" {
let s = meta.value()?.parse::<LitInt>()?;
rv.max_chars_allowance = Some(quote!(#s));
} else if ident == "max_depth" {
let s = meta.value()?.parse::<LitInt>()?;
rv.max_depth = Some(quote!(#s));
} else if ident == "max_bytes" {
let s = meta.value()?.parse::<LitInt>()?;
rv.max_bytes = Some(quote!(#s));
rv.max_bytes = Some(meta.value()?.parse()?);
} else if ident == "pii" {
let s = meta.value()?.parse::<LitStr>()?;
rv.pii = parse_pii_value(s, &meta)?;
rv.pii = Some(meta.value()?.parse()?);
} else if ident == "retain" {
let s = meta.value()?.parse::<LitBool>()?;
rv.retain = s.value();
Expand Down Expand Up @@ -600,14 +643,3 @@ fn parse_character_set(ident: &Ident, value: &str) -> TokenStream {
}
}
}

fn parse_pii_value(value: LitStr, meta: &ParseNestedMeta) -> syn::Result<Option<Pii>> {
Ok(Some(match value.value().as_str() {
"true" => Pii::True,
"false" => Pii::False,
"maybe" => Pii::Maybe,
_ => Pii::Dynamic(value.parse().map_err(|_| {
meta.error("Expected one of `true`, `false`, `maybe`, or a function name")
})?),
}))
}
6 changes: 3 additions & 3 deletions relay-event-normalization/src/trimming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ impl Processor for TrimmingProcessor {
// If we encounter a max_bytes or max_depth attribute it
// resets the size and depth that is permitted below it.
// XXX(iker): test setting only one of the two attributes.
if state.attrs().max_bytes.is_some() || state.attrs().max_depth.is_some() {
if state.max_bytes().is_some() || state.attrs().max_depth.is_some() {
self.size_state.push(SizeState {
size_remaining: state.attrs().max_bytes,
size_remaining: state.max_bytes(),
encountered_at_depth: state.depth(),
max_depth: state.attrs().max_depth,
});
Expand Down Expand Up @@ -128,7 +128,7 @@ impl Processor for TrimmingProcessor {
meta: &mut Meta,
state: &ProcessingState<'_>,
) -> ProcessingResult {
if let Some(max_chars) = state.attrs().max_chars {
if let Some(max_chars) = state.max_chars() {
trim_string(value, meta, max_chars, state.attrs().max_chars_allowance);
}

Expand Down
108 changes: 99 additions & 9 deletions relay-event-schema/src/processor/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ pub enum PiiMode {
Dynamic(fn(&ProcessingState) -> Pii),
}

/// A static or dynamic Option<`usize`> value.
///
/// Used for the fields `max_chars` and `max_bytes`.
#[derive(Debug, Clone, Copy)]
pub enum SizeMode {
Static(Option<usize>),
Dynamic(fn(&ProcessingState) -> Option<usize>),
}

/// Meta information about a field.
#[derive(Debug, Clone, Copy)]
pub struct FieldAttrs {
Expand All @@ -131,13 +140,13 @@ pub struct FieldAttrs {
/// A set of allowed or denied character ranges for this string.
pub characters: Option<CharacterSet>,
/// The maximum char length of this field.
pub max_chars: Option<usize>,
pub max_chars: SizeMode,
/// The extra char length allowance on top of max_chars.
pub max_chars_allowance: usize,
/// The maximum depth of this field.
pub max_depth: Option<usize>,
/// The maximum number of bytes of this field.
pub max_bytes: Option<usize>,
pub max_bytes: SizeMode,
/// The type of PII on the field.
pub pii: PiiMode,
/// Whether additional properties should be retained during normalization.
Expand Down Expand Up @@ -177,10 +186,10 @@ impl FieldAttrs {
nonempty: false,
trim_whitespace: false,
characters: None,
max_chars: None,
max_chars: SizeMode::Static(None),
max_chars_allowance: 0,
max_depth: None,
max_bytes: None,
max_bytes: SizeMode::Static(None),
pii: PiiMode::Static(Pii::False),
retain: false,
trim: true,
Expand Down Expand Up @@ -222,7 +231,16 @@ 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 = Some(max_chars);
self.max_chars = SizeMode::Static(Some(max_chars));
self
}

/// Sets the maximum number of characters allowed in the field dynamically based on the current state.
pub const fn max_chars_dynamic(
mut self,
max_chars: fn(&ProcessingState) -> Option<usize>,
) -> Self {
self.max_chars = SizeMode::Dynamic(max_chars);
self
}

Expand Down Expand Up @@ -462,6 +480,30 @@ impl<'a> ProcessingState<'a> {
}
}

/// Returns the max bytes for this state.
///
/// If the state's `FieldAttrs` contain a fixed `max_bytes` value,
/// it is returned. If they contain a dynamic `max_bytes` value (a function),
/// it is applied to this state and the output returned.
pub fn max_bytes(&self) -> Option<usize> {
match self.attrs().max_bytes {
SizeMode::Static(n) => n,
SizeMode::Dynamic(max_bytes_fn) => max_bytes_fn(self),
}
}

/// Returns the max chars for this state.
///
/// If the state's `FieldAttrs` contain a fixed `max_chars` value,
/// it is returned. If they contain a dynamic `max_chars` value (a function),
/// it is applied to this state and the output returned.
pub fn max_chars(&self) -> Option<usize> {
match self.attrs().max_chars {
SizeMode::Static(n) => n,
SizeMode::Dynamic(max_chars_fn) => max_chars_fn(self),
}
}

/// Iterates through this state and all its ancestors up the hierarchy.
///
/// This starts at the top of the stack of processing states and ends at the root. Thus
Expand Down Expand Up @@ -628,13 +670,21 @@ mod tests {
}
}

fn max_chars_from_item_name(state: &ProcessingState) -> Option<usize> {
match state.path_item().and_then(|p| p.key()) {
Some("short_item") => Some(10),
Some("long_item") => Some(20),
_ => None,
}
}

#[derive(Debug, Clone, Empty, IntoValue, FromValue, ProcessValue)]
#[metastructure(pii = "pii_from_item_name")]
struct TestValue(String);
struct TestValue(#[metastructure(max_chars = "max_chars_from_item_name")] String);

struct TestProcessor;
struct TestPiiProcessor;

impl Processor for TestProcessor {
impl Processor for TestPiiProcessor {
fn process_string(
&mut self,
value: &mut String,
Expand All @@ -650,6 +700,22 @@ mod tests {
}
}

struct TestTrimmingProcessor;

impl Processor for TestTrimmingProcessor {
fn process_string(
&mut self,
value: &mut String,
_meta: &mut relay_protocol::Meta,
state: &ProcessingState<'_>,
) -> crate::processor::ProcessingResult where {
if let Some(n) = state.max_chars() {
value.truncate(n);
}
Ok(())
}
}

#[test]
fn test_dynamic_pii() {
let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
Expand All @@ -663,7 +729,7 @@ mod tests {
)
.unwrap();

process_value(&mut object, &mut TestProcessor, &ROOT_STATE).unwrap();
process_value(&mut object, &mut TestPiiProcessor, &ROOT_STATE).unwrap();

insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
{
Expand All @@ -673,4 +739,28 @@ mod tests {
}
"###);
}

#[test]
fn test_dynamic_max_chars() {
let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
r#"
{
"short_item": "Should be shortened to 10",
"long_item": "Should be shortened to 20",
"other_item": "Should not be shortened at all"
}
"#,
)
.unwrap();

process_value(&mut object, &mut TestTrimmingProcessor, &ROOT_STATE).unwrap();

insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
{
"long_item": "Should be shortened ",
"other_item": "Should not be shortened at all",
"short_item": "Should be "
}
"###);
}
}
Loading