Skip to main content

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 = "value_or_meta", 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
110impl From<Vec<String>> for AttributeValue {
111    fn from(value: Vec<String>) -> Self {
112        let values = value
113            .into_iter()
114            .map(|value| Annotated::new(Value::String(value)))
115            .collect();
116
117        Self {
118            ty: Annotated::new(AttributeType::Array),
119            value: Annotated::new(Value::Array(values)),
120        }
121    }
122}
123
124/// Determines the `Pii` value for an attribute (or, more exactly, the
125/// attribute's `value` field) by looking it up in `relay-conventions`.
126///
127/// The attribute's value may be addressed by `<key>.value` (for advanced
128/// scrubbing rules) or by just `<key>` (for default scrubbing rules). Therefore,
129/// we iterate backwards through the processing state's segments and try to look
130/// up each in the conventions.
131///
132/// If the attribute is not found in the conventions, this returns `Pii::True`
133/// as a precaution.
134pub fn attribute_pii_from_conventions(state: &ProcessingState) -> Pii {
135    for key in state.keys() {
136        let Some(info) = relay_conventions::attribute_info(key) else {
137            continue;
138        };
139
140        return match info.apply_scrubbing {
141            relay_conventions::ApplyScrubbing::Auto => Pii::True,
142            relay_conventions::ApplyScrubbing::Never => Pii::False,
143            relay_conventions::ApplyScrubbing::Manual => Pii::Maybe,
144        };
145    }
146
147    Pii::True
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum AttributeType {
152    /// A boolean.
153    ///
154    /// The respective value must be of type [`Value::Bool`].
155    Boolean,
156    /// An integer type.
157    ///
158    /// The respective value must be of type [`Value::I64`] or [`Value::U64`].
159    Integer,
160    /// A floating point/double type.
161    ///
162    /// The respective value must be of type [`Value::F64`].
163    Double,
164    /// A string type.
165    ///
166    /// The respective value must be of type [`Value::String`].
167    String,
168    /// A string type.
169    ///
170    /// The respective value must be of type [`Value::Array`].
171    Array,
172    /// An unknown type.
173    ///
174    /// Kept for forward compatibility.
175    Unknown(String),
176}
177
178impl ProcessValue for AttributeType {}
179
180impl AttributeType {
181    pub fn as_str(&self) -> &str {
182        match self {
183            Self::Boolean => "boolean",
184            Self::Integer => "integer",
185            Self::Double => "double",
186            Self::String => "string",
187            Self::Array => "array",
188            Self::Unknown(value) => value,
189        }
190    }
191
192    pub fn unknown_string() -> String {
193        "unknown".to_owned()
194    }
195}
196
197impl fmt::Display for AttributeType {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        write!(f, "{}", self.as_str())
200    }
201}
202
203impl From<String> for AttributeType {
204    fn from(value: String) -> Self {
205        match value.as_str() {
206            "boolean" => Self::Boolean,
207            "integer" => Self::Integer,
208            "double" => Self::Double,
209            "string" => Self::String,
210            "array" => Self::Array,
211            _ => Self::Unknown(value),
212        }
213    }
214}
215
216impl Empty for AttributeType {
217    #[inline]
218    fn is_empty(&self) -> bool {
219        false
220    }
221}
222
223impl FromValue for AttributeType {
224    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
225        match String::from_value(value) {
226            Annotated(Some(value), meta) => Annotated(Some(value.into()), meta),
227            Annotated(None, meta) => Annotated(None, meta),
228        }
229    }
230}
231
232impl IntoValue for AttributeType {
233    fn into_value(self) -> Value
234    where
235        Self: Sized,
236    {
237        Value::String(match self {
238            Self::Unknown(s) => s,
239            s => s.to_string(),
240        })
241    }
242
243    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
244    where
245        Self: Sized,
246        S: serde::Serializer,
247    {
248        serde::ser::Serialize::serialize(self.as_str(), s)
249    }
250}
251
252/// Wrapper struct around a collection of attributes with some convenience methods.
253#[derive(Debug, Clone, Default, PartialEq, Empty, FromValue, IntoValue)]
254pub struct Attributes(pub Object<Attribute>);
255
256impl Attributes {
257    /// Creates an empty collection of attributes.
258    pub fn new() -> Self {
259        Self(Object::new())
260    }
261
262    /// Returns the value of the attribute with the given key.
263    pub fn get_value<Q>(&self, key: &Q) -> Option<&Value>
264    where
265        String: Borrow<Q>,
266        Q: Ord + ?Sized,
267    {
268        self.get_annotated_value(key)?.value()
269    }
270
271    /// Returns the attribute with the given key.
272    pub fn get_attribute<Q>(&self, key: &Q) -> Option<&Attribute>
273    where
274        String: Borrow<Q>,
275        Q: Ord + ?Sized,
276    {
277        self.0.get(key)?.value()
278    }
279
280    /// Returns the attribute value as annotated.
281    pub fn get_annotated_value<Q>(&self, key: &Q) -> Option<&Annotated<Value>>
282    where
283        String: Borrow<Q>,
284        Q: Ord + ?Sized,
285    {
286        Some(&self.0.get(key)?.value()?.value.value)
287    }
288
289    /// Returns the mutable attribute value as annotated.
290    pub fn get_annotated_value_mut<Q>(&mut self, key: &Q) -> Option<&mut Annotated<Value>>
291    where
292        String: Borrow<Q>,
293        Q: Ord + ?Sized,
294    {
295        Some(&mut self.0.get_mut(key)?.value_mut().as_mut()?.value.value)
296    }
297
298    /// Inserts an attribute with the given value into this collection.
299    pub fn insert<K: Into<String>, V: Into<AttributeValue>>(&mut self, key: K, value: V) {
300        fn inner(slf: &mut Attributes, key: String, value: AttributeValue) {
301            let attribute = Annotated::new(Attribute {
302                value,
303                other: Default::default(),
304            });
305            slf.0.insert(key, attribute);
306        }
307        let value = value.into();
308        if !value.value.is_empty() {
309            inner(self, key.into(), value);
310        }
311    }
312
313    /// Inserts an attribute with the given value if it was not already present.
314    pub fn insert_if_missing<F, V>(&mut self, key: &str, value: F)
315    where
316        F: FnOnce() -> V,
317        V: Into<AttributeValue>,
318    {
319        if !self.0.contains_key(key) {
320            self.insert(key.to_owned(), value());
321        }
322    }
323
324    /// Checks whether this collection contains an attribute with the given key.
325    pub fn contains_key<Q>(&self, key: &Q) -> bool
326    where
327        String: Borrow<Q>,
328        Q: Ord + ?Sized,
329    {
330        self.0.contains_key(key)
331    }
332
333    /// Removes an attribute from this collection.
334    pub fn remove<Q>(&mut self, key: &Q) -> Option<Annotated<Attribute>>
335    where
336        String: Borrow<Q>,
337        Q: Ord + ?Sized,
338    {
339        self.0.remove(key)
340    }
341}
342
343impl IntoIterator for Attributes {
344    type Item = (String, Annotated<Attribute>);
345
346    type IntoIter = std::collections::btree_map::IntoIter<String, Annotated<Attribute>>;
347
348    fn into_iter(self) -> Self::IntoIter {
349        self.0.into_iter()
350    }
351}
352
353impl FromIterator<(String, Annotated<Attribute>)> for Attributes {
354    fn from_iter<T: IntoIterator<Item = (String, Annotated<Attribute>)>>(iter: T) -> Self {
355        Self(Object::from_iter(iter))
356    }
357}
358
359impl<const N: usize> From<[(String, Annotated<Attribute>); N]> for Attributes {
360    fn from(value: [(String, Annotated<Attribute>); N]) -> Self {
361        value.into_iter().collect()
362    }
363}
364
365// We need to manually implement `ProcessValue` for `Attributes`.
366// Deriving it, even with `process_func`, causes both `process_value`
367// and `process_child_values` to be called because it's a newtype struct.
368impl ProcessValue for Attributes {
369    #[inline]
370    fn value_type(&self) -> EnumSet<ValueType> {
371        EnumSet::only(ValueType::Object)
372    }
373
374    #[inline]
375    fn process_value<P>(
376        &mut self,
377        meta: &mut Meta,
378        processor: &mut P,
379        state: &ProcessingState<'_>,
380    ) -> ProcessingResult
381    where
382        P: Processor,
383    {
384        processor.process_attributes(self, meta, state)
385    }
386
387    fn process_child_values<P>(
388        &mut self,
389        processor: &mut P,
390        state: &ProcessingState<'_>,
391    ) -> ProcessingResult
392    where
393        P: Processor,
394    {
395        let enter_state = state.enter_nothing(Some(Cow::Borrowed(state.attrs())));
396        self.0.process_child_values(processor, &enter_state)
397    }
398}