Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

experimental(rule-condition): Add loop operator inside Val #3794

Closed
wants to merge 3 commits into from
Closed
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
25 changes: 24 additions & 1 deletion relay-event-schema/src/protocol/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use std::str::FromStr;
use relay_common::time;
#[cfg(feature = "jsonschema")]
use relay_jsonschema_derive::JsonSchema;
use relay_protocol::{Annotated, Array, Empty, FromValue, Getter, IntoValue, Object, Val, Value};
use relay_protocol::{
Annotated, Arr, Array, Empty, FromValue, Getter, GetterIter, IntoValue, Object, Val, Value,
};
#[cfg(feature = "jsonschema")]
use schemars::{gen::SchemaGenerator, schema::Schema};
use sentry_release_parser::Release as ParsedRelease;
Expand Down Expand Up @@ -652,6 +654,10 @@ impl Getter for Event {
"logger" => self.logger.as_str()?.into(),
"platform" => self.platform.as_str().unwrap_or("other").into(),

"exceptions" => {
Val::Array(Arr::new_annotated(self.exceptions.value()?.values.value()?))
}

// Fields in top level structures (called "interfaces" in Sentry)
"user.email" => or_none(&self.user.value()?.email)?.into(),
"user.id" => or_none(&self.user.value()?.id)?.into(),
Expand Down Expand Up @@ -805,6 +811,23 @@ impl Getter for Event {
}
})
}

// fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
// Some(match path.strip_prefix("event.")? {
// "exceptions" => GetterIter::new_annotated(self.exceptions.value()?.values.value()?),
// _ => return None,
// })
// }
}

impl Getter for Exception {
fn get_value(&self, path: &str) -> Option<Val<'_>> {
Some(match path {
"ty" => self.ty.as_str()?.into(),
"value" => self.value.as_str()?.into(),
_ => return None,
})
}
}

#[cfg(test)]
Expand Down
107 changes: 107 additions & 0 deletions relay-protocol/src/condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use relay_common::glob3::GlobPatterns;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::format;

use crate::{Getter, Val};

Expand Down Expand Up @@ -307,6 +308,56 @@ impl NotCondition {
}
}

/// TODO: add comments.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AnyCondition {
/// Path of the field that should match the value.
pub name: String,
/// Inner rule to match on each element.
pub inner: Box<RuleCondition>,
}

impl AnyCondition {
fn supported(&self) -> bool {
self.inner.supported()
}
fn matches<T>(&self, instance: &T) -> bool
where
T: Getter + ?Sized,
{
let Some(Val::Array(arr)) = instance.get_value(self.name.as_str()) else {
return false;
};

arr.iter().any(|g| self.inner.matches(g))
}
}

/// TODO: add comments.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AllCondition {
/// Path of the field that should match the value.
pub name: String,
/// Inner rule to match on each element.
pub inner: Box<RuleCondition>,
}

impl AllCondition {
fn supported(&self) -> bool {
self.inner.supported()
}
fn matches<T>(&self, instance: &T) -> bool
where
T: Getter + ?Sized,
{
let Some(Val::Array(arr)) = instance.get_value(self.name.as_str()) else {
return false;
};

arr.iter().all(|g| self.inner.matches(g))
}
}

/// A condition that can be evaluated on structured data.
///
/// The basic conditions are [`eq`](Self::eq), [`glob`](Self::glob), and the comparison operators.
Expand Down Expand Up @@ -445,6 +496,26 @@ pub enum RuleCondition {
/// ```
Not(NotCondition),

/// Loops over an array field and returns true if at least one element matches
/// the inner condition.
///
/// # TODO: impl docs.
/// # Example
///
/// ```
/// ```
Any(AnyCondition),

/// Loops over an array field and returns true if at least one element matches
/// the inner condition.
///
/// # TODO: impl docs.
/// # Example
///
/// ```
/// ```
All(AllCondition),

/// An unsupported condition for future compatibility.
#[serde(other)]
Unsupported,
Expand Down Expand Up @@ -655,6 +726,8 @@ impl RuleCondition {
RuleCondition::And(rules) => rules.supported(),
RuleCondition::Or(rules) => rules.supported(),
RuleCondition::Not(rule) => rule.supported(),
RuleCondition::Any(rule) => rule.supported(),
RuleCondition::All(rule) => rule.supported(),
}
}

Expand All @@ -673,6 +746,8 @@ impl RuleCondition {
RuleCondition::And(conditions) => conditions.matches(value),
RuleCondition::Or(conditions) => conditions.matches(value),
RuleCondition::Not(condition) => condition.matches(value),
RuleCondition::Any(condition) => condition.matches(value),
RuleCondition::All(condition) => condition.matches(value),
RuleCondition::Unsupported => false,
}
}
Expand Down Expand Up @@ -705,12 +780,28 @@ impl std::ops::Not for RuleCondition {
#[cfg(test)]
mod tests {
use super::*;
use crate::{Annotated, Arr, Array, GetterIter};

#[derive(Debug)]
struct Exception {
name: String,
}

impl Getter for Exception {
fn get_value(&self, path: &str) -> Option<Val<'_>> {
Some(match path {
"name" => self.name.as_str().into(),
_ => return None,
})
}
}

struct MockDSC {
transaction: String,
release: String,
environment: String,
user_segment: String,
exceptions: Vec<Exception>,
}

impl Getter for MockDSC {
Expand All @@ -720,6 +811,7 @@ mod tests {
"release" => self.release.as_str().into(),
"environment" => self.environment.as_str().into(),
"user.segment" => self.user_segment.as_str().into(),
"exceptions" => Val::Array(Arr::new(self.exceptions.iter())),
_ => {
return None;
}
Expand All @@ -733,6 +825,9 @@ mod tests {
release: "1.1.1".to_string(),
environment: "debug".to_string(),
user_segment: "vip".to_string(),
exceptions: vec![Exception {
name: "NullPointerException".to_string(),
}],
}
}

Expand Down Expand Up @@ -1093,4 +1188,16 @@ mod tests {
assert!(!condition.matches(&dsc), "{failure_name}");
}
}

#[test]
fn test_loop_condition() {
let condition = RuleCondition::Any(AnyCondition {
name: "trace.exceptions".to_string(),
inner: Box::new(RuleCondition::glob("name", "*Exception")),
});

let dsc = mock_dsc();

assert_eq!(condition.matches(&dsc), true);
}
}
6 changes: 5 additions & 1 deletion relay-protocol/src/traits.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::fmt::{Debug, Formatter};

use crate::annotated::{Annotated, MetaMap, MetaTree};
use crate::value::{Val, Value};
Expand Down Expand Up @@ -195,4 +195,8 @@ pub trait IntoValue: Debug + Empty {
pub trait Getter {
/// Returns the serialized value of a field pointed to by a `path`.
fn get_value(&self, path: &str) -> Option<Val<'_>>;

// fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
// None
// }
}
81 changes: 71 additions & 10 deletions relay-protocol/src/value.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
use std::collections::BTreeMap;
use std::{fmt, str};

#[cfg(feature = "jsonschema")]
use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema};
use serde::de::{Deserialize, MapAccess, SeqAccess, Visitor};
use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::{fmt, str};
use uuid::Uuid;

use crate::annotated::Annotated;
use crate::meta::Meta;
use crate::Getter;

/// Alias for typed arrays.
pub type Array<T> = Vec<Annotated<T>>;
Expand Down Expand Up @@ -351,20 +353,81 @@ where
serde_json::to_value(value).map(Value::from_json)
}

pub trait GetterTrait<'a>: Iterator<Item = &'a dyn Getter> + Debug {
fn clone_2(&self) -> Box<dyn GetterTrait<'a> + 'a>;
}

impl<'a, T> GetterTrait<'a> for T
where
T: Iterator<Item = &'a dyn Getter> + Clone + Debug + 'a,
{
fn clone_2(&self) -> Box<dyn GetterTrait<'a> + 'a> {
Box::new(self.clone())
}
}

#[derive(Debug)]
pub struct GetterIter<'a> {
iter: Box<dyn GetterTrait<'a> + 'a>,
}

impl<'a> Clone for GetterIter<'a> {
fn clone(&self) -> Self {
Self {
iter: self.clone_2(),
}
}
}

impl<'a> Iterator for GetterIter<'a> {
type Item = &'a dyn Getter;

fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
}

/// Borrowed version of [`Array`].
#[derive(Debug, Clone, Copy)]
#[derive(Debug)]
pub struct Arr<'a> {
_phantom: std::marker::PhantomData<&'a ()>,
iter: GetterIter<'a>,
}

impl<'a> Arr<'a> {
pub fn new<I, T>(iterator: I) -> Self
where
I: Iterator<Item = &'a T> + Clone + Debug + 'a,
T: 'a + Getter,
{
Self {
iter: GetterIter {
iter: Box::new(iterator.map(|v| v as &dyn Getter)),
},
}
}

pub fn new_annotated<I, T>(iterator: I) -> Self
where
I: IntoIterator<Item = &'a Annotated<T>>,
I::IntoIter: Clone + Debug + 'a,
T: 'static + Getter,
{
Self::new(iterator.into_iter().filter_map(Annotated::value))
}

pub fn iter(&self) -> GetterIter<'a> {
self.iter.clone()
}
}

/// Borrowed version of [`Object`].
#[derive(Debug, Clone, Copy)]
#[derive(Debug)]
pub struct Obj<'a> {
_phantom: std::marker::PhantomData<&'a ()>,
}

/// Borrowed version of [`Value`].
#[derive(Debug, Clone, Copy)]
#[derive(Debug)]
pub enum Val<'a> {
/// A boolean value.
Bool(bool),
Expand Down Expand Up @@ -493,9 +556,7 @@ impl<'a> From<&'a Value> for Val<'a> {
Value::U64(value) => Self::U64(*value),
Value::F64(value) => Self::F64(*value),
Value::String(value) => Self::String(value),
Value::Array(_) => Self::Array(Arr {
_phantom: Default::default(),
}),
Value::Array(value) => Self::Array(Arr::new_annotated(value)),
Value::Object(_) => Self::Object(Obj {
_phantom: Default::default(),
}),
Expand Down
Loading