relay_event_schema/protocol/
tags.rs1use 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#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
47pub struct Tags(pub PairList<TagEntry>);
48
49impl Tags {
50 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}