relay_event_schema/protocol/
attributes.rs

1use std::borrow::{Borrow, Cow};
2use std::fmt;
3
4use enumset::EnumSet;
5use relay_protocol::{
6    Annotated, Empty, FromValue, IntoValue, Meta, Object, SkipSerialization, Value,
7};
8
9use crate::processor::{
10    Pii, ProcessValue, ProcessingResult, ProcessingState, Processor, ValueType,
11};
12
13#[derive(Clone, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
14pub struct Attribute {
15    #[metastructure(flatten)]
16    pub value: AttributeValue,
17
18    /// Additional arbitrary fields for forwards compatibility.
19    #[metastructure(additional_properties, trim = false)]
20    pub other: Object<Value>,
21}
22
23impl Attribute {
24    pub fn new(attribute_type: AttributeType, value: Value) -> Self {
25        Self {
26            value: AttributeValue {
27                ty: Annotated::new(attribute_type),
28                value: Annotated::new(value),
29            },
30            other: Object::new(),
31        }
32    }
33
34    /// Returns the string value of this attribute.
35    pub fn into_string(self) -> Option<String> {
36        self.value.value.into_value()?.into_string()
37    }
38}
39
40impl fmt::Debug for Attribute {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        f.debug_struct("Attribute")
43            .field("value", &self.value.value)
44            .field("type", &self.value.ty)
45            .field("other", &self.other)
46            .finish()
47    }
48}
49
50impl<T> From<T> for Attribute
51where
52    AttributeValue: From<T>,
53{
54    fn from(value: T) -> Self {
55        let value = value.into();
56        Self {
57            value,
58            other: Default::default(),
59        }
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
64pub struct AttributeValue {
65    #[metastructure(field = "type", required = true, trim = false, pii = "false")]
66    pub ty: Annotated<AttributeType>,
67    #[metastructure(required = true, pii = "attribute_pii_from_conventions")]
68    pub value: Annotated<Value>,
69}
70
71macro_rules! impl_from {
72    ($ty:ident, $aty: expr) => {
73        impl From<Annotated<$ty>> for AttributeValue {
74            fn from(value: Annotated<$ty>) -> Self {
75                AttributeValue {
76                    ty: Annotated::new($aty),
77                    value: value.map_value(Into::into),
78                }
79            }
80        }
81
82        impl From<$ty> for AttributeValue {
83            fn from(value: $ty) -> Self {
84                AttributeValue::from(Annotated::new(value))
85            }
86        }
87    };
88}
89
90impl_from!(String, AttributeType::String);
91impl_from!(i64, AttributeType::Integer);
92impl_from!(f64, AttributeType::Double);
93impl_from!(bool, AttributeType::Boolean);
94
95impl From<Annotated<&str>> for AttributeValue {
96    fn from(value: Annotated<&str>) -> Self {
97        Self {
98            ty: Annotated::new(AttributeType::String),
99            value: value.map_value(Into::into),
100        }
101    }
102}
103
104impl From<&str> for AttributeValue {
105    fn from(value: &str) -> Self {
106        Self::from(Annotated::new(value))
107    }
108}
109
110/// Determines the `Pii` value for an attribute (or, more exactly, the
111/// attribute's `value` field) by looking it up in `relay-conventions`.
112///
113/// The attribute's value may be addressed by `<key>.value` (for advanced
114/// scrubbing rules) or by just `<key>` (for default scrubbing rules). Therefore,
115/// we iterate backwards through the processing state's segments and try to look
116/// up each in the conventions.
117///
118/// If the attribute is not found in the conventions, this returns `Pii::True`
119/// as a precaution.
120pub fn attribute_pii_from_conventions(state: &ProcessingState) -> Pii {
121    for key in state.keys() {
122        let Some(info) = relay_conventions::attribute_info(key) else {
123            continue;
124        };
125
126        return match info.pii {
127            relay_conventions::Pii::True => Pii::True,
128            relay_conventions::Pii::False => Pii::False,
129            relay_conventions::Pii::Maybe => Pii::Maybe,
130        };
131    }
132
133    Pii::True
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum AttributeType {
138    /// A boolean.
139    ///
140    /// The respective value must be of type [`Value::Bool`].
141    Boolean,
142    /// An integer type.
143    ///
144    /// The respective value must be of type [`Value::I64`] or [`Value::U64`].
145    Integer,
146    /// A floating point/double type.
147    ///
148    /// The respective value must be of type [`Value::F64`].
149    Double,
150    /// A string type.
151    ///
152    /// The respective value must be of type [`Value::String`].
153    String,
154    /// A string type.
155    ///
156    /// The respective value must be of type [`Value::Array`].
157    Array,
158    /// An unknown type.
159    ///
160    /// Kept for forward compatibility.
161    Unknown(String),
162}
163
164impl ProcessValue for AttributeType {}
165
166impl AttributeType {
167    pub fn as_str(&self) -> &str {
168        match self {
169            Self::Boolean => "boolean",
170            Self::Integer => "integer",
171            Self::Double => "double",
172            Self::String => "string",
173            Self::Array => "array",
174            Self::Unknown(value) => value,
175        }
176    }
177
178    pub fn unknown_string() -> String {
179        "unknown".to_owned()
180    }
181}
182
183impl fmt::Display for AttributeType {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        write!(f, "{}", self.as_str())
186    }
187}
188
189impl From<String> for AttributeType {
190    fn from(value: String) -> Self {
191        match value.as_str() {
192            "boolean" => Self::Boolean,
193            "integer" => Self::Integer,
194            "double" => Self::Double,
195            "string" => Self::String,
196            "array" => Self::Array,
197            _ => Self::Unknown(value),
198        }
199    }
200}
201
202impl Empty for AttributeType {
203    #[inline]
204    fn is_empty(&self) -> bool {
205        false
206    }
207}
208
209impl FromValue for AttributeType {
210    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
211        match String::from_value(value) {
212            Annotated(Some(value), meta) => Annotated(Some(value.into()), meta),
213            Annotated(None, meta) => Annotated(None, meta),
214        }
215    }
216}
217
218impl IntoValue for AttributeType {
219    fn into_value(self) -> Value
220    where
221        Self: Sized,
222    {
223        Value::String(match self {
224            Self::Unknown(s) => s,
225            s => s.to_string(),
226        })
227    }
228
229    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
230    where
231        Self: Sized,
232        S: serde::Serializer,
233    {
234        serde::ser::Serialize::serialize(self.as_str(), s)
235    }
236}
237
238/// Wrapper struct around a collection of attributes with some convenience methods.
239#[derive(Debug, Clone, Default, PartialEq, Empty, FromValue, IntoValue)]
240pub struct Attributes(pub Object<Attribute>);
241
242impl Attributes {
243    /// Creates an empty collection of attributes.
244    pub fn new() -> Self {
245        Self(Object::new())
246    }
247
248    /// Returns the value of the attribute with the given key.
249    pub fn get_value<Q>(&self, key: &Q) -> Option<&Value>
250    where
251        String: Borrow<Q>,
252        Q: Ord + ?Sized,
253    {
254        self.get_annotated_value(key)?.value()
255    }
256
257    /// Returns the attribute with the given key.
258    pub fn get_attribute<Q>(&self, key: &Q) -> Option<&Attribute>
259    where
260        String: Borrow<Q>,
261        Q: Ord + ?Sized,
262    {
263        self.0.get(key)?.value()
264    }
265
266    /// Returns the attribute value as annotated.
267    pub fn get_annotated_value<Q>(&self, key: &Q) -> Option<&Annotated<Value>>
268    where
269        String: Borrow<Q>,
270        Q: Ord + ?Sized,
271    {
272        Some(&self.0.get(key)?.value()?.value.value)
273    }
274
275    /// Inserts an attribute with the given value into this collection.
276    pub fn insert<K: Into<String>, V: Into<AttributeValue>>(&mut self, key: K, value: V) {
277        fn inner(slf: &mut Attributes, key: String, value: AttributeValue) {
278            let attribute = Annotated::new(Attribute {
279                value,
280                other: Default::default(),
281            });
282            slf.0.insert(key, attribute);
283        }
284        let value = value.into();
285        if !value.value.is_empty() {
286            inner(self, key.into(), value);
287        }
288    }
289
290    /// Inserts an attribute with the given value if it was not already present.
291    pub fn insert_if_missing<F, V>(&mut self, key: &str, value: F)
292    where
293        F: FnOnce() -> V,
294        V: Into<AttributeValue>,
295    {
296        if !self.0.contains_key(key) {
297            self.insert(key.to_owned(), value());
298        }
299    }
300
301    /// Checks whether this collection contains an attribute with the given key.
302    pub fn contains_key<Q>(&self, key: &Q) -> bool
303    where
304        String: Borrow<Q>,
305        Q: Ord + ?Sized,
306    {
307        self.0.contains_key(key)
308    }
309
310    /// Removes an attribute from this collection.
311    pub fn remove<Q>(&mut self, key: &Q) -> Option<Annotated<Attribute>>
312    where
313        String: Borrow<Q>,
314        Q: Ord + ?Sized,
315    {
316        self.0.remove(key)
317    }
318}
319
320impl IntoIterator for Attributes {
321    type Item = (String, Annotated<Attribute>);
322
323    type IntoIter = std::collections::btree_map::IntoIter<String, Annotated<Attribute>>;
324
325    fn into_iter(self) -> Self::IntoIter {
326        self.0.into_iter()
327    }
328}
329
330impl FromIterator<(String, Annotated<Attribute>)> for Attributes {
331    fn from_iter<T: IntoIterator<Item = (String, Annotated<Attribute>)>>(iter: T) -> Self {
332        Self(Object::from_iter(iter))
333    }
334}
335
336impl<const N: usize> From<[(String, Annotated<Attribute>); N]> for Attributes {
337    fn from(value: [(String, Annotated<Attribute>); N]) -> Self {
338        value.into_iter().collect()
339    }
340}
341
342// We need to manually implement `ProcessValue` for `Attributes`.
343// Deriving it, even with `process_func`, causes both `process_value`
344// and `process_child_values` to be called because it's a newtype struct.
345impl ProcessValue for Attributes {
346    #[inline]
347    fn value_type(&self) -> EnumSet<ValueType> {
348        EnumSet::only(ValueType::Object)
349    }
350
351    #[inline]
352    fn process_value<P>(
353        &mut self,
354        meta: &mut Meta,
355        processor: &mut P,
356        state: &ProcessingState<'_>,
357    ) -> ProcessingResult
358    where
359        P: Processor,
360    {
361        processor.process_attributes(self, meta, state)
362    }
363
364    fn process_child_values<P>(
365        &mut self,
366        processor: &mut P,
367        state: &ProcessingState<'_>,
368    ) -> ProcessingResult
369    where
370        P: Processor,
371    {
372        let enter_state = state.enter_nothing(Some(Cow::Borrowed(state.attrs())));
373        self.0.process_child_values(processor, &enter_state)
374    }
375}