relay_event_schema/protocol/
tags.rs

1use relay_protocol::{Annotated, Array, Empty, FromValue, IntoValue, Object, Value};
2
3use crate::processor::ProcessValue;
4use crate::protocol::{AsPair, JsonLenientString, LenientString, PairList};
5
6#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
7pub struct TagEntry(
8    #[metastructure(max_chars = 200, allow_chars = "a-zA-Z0-9_.:-")] pub Annotated<String>,
9    #[metastructure(max_chars = 200, deny_chars = "\n")] pub Annotated<String>,
10);
11
12impl AsPair for TagEntry {
13    type Key = String;
14    type Value = String;
15
16    fn from_pair(pair: (Annotated<Self::Key>, Annotated<Self::Value>)) -> Self {
17        TagEntry(pair.0, pair.1)
18    }
19
20    fn into_pair(self) -> (Annotated<Self::Key>, Annotated<Self::Value>) {
21        (self.0, self.1)
22    }
23
24    fn as_pair(&self) -> (&Annotated<Self::Key>, &Annotated<Self::Value>) {
25        (&self.0, &self.1)
26    }
27
28    fn as_pair_mut(&mut self) -> (&mut Annotated<Self::Key>, &mut Annotated<Self::Value>) {
29        (&mut self.0, &mut self.1)
30    }
31}
32
33impl FromValue for TagEntry {
34    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
35        type TagTuple = (Annotated<LenientString>, Annotated<LenientString>);
36        TagTuple::from_value(value).map_value(|(key, value)| {
37            TagEntry(
38                key.map_value(|x| x.into_inner().replace(' ', "-").trim().to_string()),
39                value.map_value(|x| x.into_inner().trim().to_string()),
40            )
41        })
42    }
43}
44
45/// Manual key/value tag pairs.
46#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
47pub struct Tags(pub PairList<TagEntry>);
48
49impl Tags {
50    /// Returns a reference to the value of the tag, if it exists.
51    ///
52    /// If the tag with the specified key exists multiple times, the first instance is returned. If
53    /// no tag with the given key exists or the tag entry is erroneous, `None` is returned`.
54    ///
55    /// To retrieve the [`Annotated`] wrapper of the tag value, use [`PairList::get`] instead.
56    pub fn get(&self, key: &str) -> Option<&str> {
57        self.0.get_value(key).map(String::as_str)
58    }
59}
60
61impl std::ops::Deref for Tags {
62    type Target = Array<TagEntry>;
63
64    fn deref(&self) -> &Self::Target {
65        &self.0
66    }
67}
68
69impl std::ops::DerefMut for Tags {
70    fn deref_mut(&mut self) -> &mut Self::Target {
71        &mut self.0
72    }
73}
74
75impl<T: Into<String>> From<Object<T>> for Tags {
76    fn from(value: Object<T>) -> Self {
77        Self(PairList(
78            value
79                .into_iter()
80                .map(|(k, v)| TagEntry(k.into(), v.map_value(|s| s.into())))
81                .map(Annotated::new)
82                .collect(),
83        ))
84    }
85}
86
87impl From<Tags> for Object<JsonLenientString> {
88    fn from(value: Tags) -> Self {
89        value
90            .0
91            .0
92            .into_iter()
93            .flat_map(Annotated::into_value)
94            .flat_map(|p| Some((p.0.into_value()?, p.1.map_value(Into::into))))
95            .collect()
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use similar_asserts::assert_eq;
102
103    use super::*;
104    use crate::protocol::Event;
105
106    #[test]
107    fn test_tags_from_object() {
108        let json = r#"{
109  "blah": "blub",
110  "bool": true,
111  "foo bar": "baz",
112  "non string": 42,
113  "bam": null
114}"#;
115
116        let arr = vec![
117            Annotated::new(TagEntry(
118                Annotated::new("bam".to_string()),
119                Annotated::empty(),
120            )),
121            Annotated::new(TagEntry(
122                Annotated::new("blah".to_string()),
123                Annotated::new("blub".to_string()),
124            )),
125            Annotated::new(TagEntry(
126                Annotated::new("bool".to_string()),
127                Annotated::new("True".to_string()),
128            )),
129            Annotated::new(TagEntry(
130                Annotated::new("foo-bar".to_string()),
131                Annotated::new("baz".to_string()),
132            )),
133            Annotated::new(TagEntry(
134                Annotated::new("non-string".to_string()),
135                Annotated::new("42".to_string()),
136            )),
137        ];
138        let tags = Annotated::new(Tags(arr.into()));
139        assert_eq!(tags, Annotated::from_json(json).unwrap());
140    }
141
142    #[test]
143    fn test_tags_from_array() {
144        let input = r#"{
145  "tags": [
146    [
147      "bool",
148      true
149    ],
150    [
151      "foo bar",
152      "baz"
153    ],
154    [
155      23,
156      42
157    ],
158    [
159      "blah",
160      "blub"
161    ],
162    [
163      "bam",
164      null
165    ]
166  ]
167}"#;
168
169        let output = r#"{
170  "tags": [
171    [
172      "bool",
173      "True"
174    ],
175    [
176      "foo-bar",
177      "baz"
178    ],
179    [
180      "23",
181      "42"
182    ],
183    [
184      "blah",
185      "blub"
186    ],
187    [
188      "bam",
189      null
190    ]
191  ]
192}"#;
193
194        let arr = vec![
195            Annotated::new(TagEntry(
196                Annotated::new("bool".to_string()),
197                Annotated::new("True".to_string()),
198            )),
199            Annotated::new(TagEntry(
200                Annotated::new("foo-bar".to_string()),
201                Annotated::new("baz".to_string()),
202            )),
203            Annotated::new(TagEntry(
204                Annotated::new("23".to_string()),
205                Annotated::new("42".to_string()),
206            )),
207            Annotated::new(TagEntry(
208                Annotated::new("blah".to_string()),
209                Annotated::new("blub".to_string()),
210            )),
211            Annotated::new(TagEntry(
212                Annotated::new("bam".to_string()),
213                Annotated::empty(),
214            )),
215        ];
216
217        let tags = Annotated::new(Tags(arr.into()));
218        let event = Annotated::new(Event {
219            tags,
220            ..Default::default()
221        });
222
223        assert_eq!(event, Annotated::from_json(input).unwrap());
224        assert_eq!(event.to_json_pretty().unwrap(), output);
225    }
226}