1pub mod consts;
10
11pub use consts::*;
12
13include!(concat!(env!("OUT_DIR"), "/attribute_map.rs"));
14include!(concat!(env!("OUT_DIR"), "/name_fn.rs"));
15
16#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
18pub enum Pii {
19 True,
21 False,
23 Maybe,
26}
27
28#[derive(Debug, Clone, Copy)]
30pub enum WriteBehavior {
31 CurrentName,
35 NewName(&'static str),
37 BothNames(&'static str),
40}
41
42#[derive(Debug, Clone)]
44pub struct AttributeInfo {
45 pub write_behavior: WriteBehavior,
47 pub pii: Pii,
49 pub aliases: &'static [&'static str],
51}
52
53pub fn attribute_info(key: &str) -> Option<&'static AttributeInfo> {
55 ATTRIBUTES.find(key)
56}
57
58const PLACEHOLDER_SEGMENT: &str = "<key>";
60
61struct Node<T: 'static> {
62 info: Option<T>,
63 children: phf::Map<&'static str, Node<T>>,
64}
65
66impl<T> Node<T> {
67 fn find(&self, key: &str) -> Option<&T> {
68 if key.is_empty() {
69 return self.info.as_ref();
70 }
71 let (prefix, suffix) = key.split_once('.').unwrap_or((key, ""));
72 for candidate in [prefix, PLACEHOLDER_SEGMENT] {
73 if let Some(info) = self
74 .children
75 .get(candidate)
76 .and_then(|child| child.find(suffix))
77 {
78 return Some(info);
79 }
80 }
81 None
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use std::collections::HashMap;
88
89 use phf::phf_map;
90 use relay_protocol::{Getter, Val};
91
92 use super::*;
93
94 #[test]
95 fn test_http_response_content_length() {
96 let info = attribute_info("http.response_content_length").unwrap();
97
98 insta::assert_debug_snapshot!(info, @r###"
99 AttributeInfo {
100 write_behavior: BothNames(
101 "http.response.body.size",
102 ),
103 pii: False,
104 aliases: [
105 "http.response.body.size",
106 "http.response.header.content-length",
107 ],
108 }
109 "###);
110 }
111
112 #[test]
113 fn test_url_path_parameter() {
114 let info = attribute_info("url.path.parameter.'id=123'").unwrap();
116
117 insta::assert_debug_snapshot!(info, @r###"
118 AttributeInfo {
119 write_behavior: CurrentName,
120 pii: Maybe,
121 aliases: [
122 "params.<key>",
123 ],
124 }
125 "###);
126 }
127
128 const ROOT: Node<u8> = Node {
129 info: None,
130 children: phf_map! {
131 "foo" => Node {
132 info: Some(1),
133 children: phf_map!{}
134 },
135 "<key>" => Node {
136 info: None,
137 children: phf_map!{
138 "bar" => Node {
139 info: Some(2),
140 children: phf_map! {}
141 }
142 }
143 }
144 },
145 };
146
147 #[test]
148 fn test_hypothetical() {
149 assert_eq!(ROOT.find("foo.bar"), Some(&2));
150 }
151
152 struct GetterMap<'a>(HashMap<&'a str, Val<'a>>);
153
154 impl Getter for GetterMap<'_> {
155 fn get_value(&self, path: &str) -> Option<Val<'_>> {
156 self.0.get(path).copied()
157 }
158 }
159
160 mod test_name_fn {
161 include!(concat!(env!("OUT_DIR"), "/test_name_fn.rs"));
162 }
163 use test_name_fn::name_for_op_and_attributes;
164
165 #[test]
166 fn only_literal_template() {
167 let attributes = GetterMap(HashMap::new());
168 assert_eq!(
169 name_for_op_and_attributes("op_with_literal_name", &attributes,),
170 "literal name"
171 );
172 }
173
174 #[test]
175 fn multiple_ops_same_template() {
176 let attributes = GetterMap(HashMap::from([("attr1", Val::String("foo"))]));
177 assert_eq!(
178 name_for_op_and_attributes("op_with_attributes_1", &attributes),
179 "foo"
180 );
181 assert_eq!(
182 name_for_op_and_attributes("op_with_attributes_2", &attributes),
183 "foo"
184 );
185 }
186
187 #[test]
188 fn skips_templates_when_attrs_are_missing() {
189 let attributes = GetterMap(HashMap::from([
190 ("attr2", Val::String("bar")),
191 ("attr3", Val::String("baz")),
192 ]));
193 assert_eq!(
194 name_for_op_and_attributes("op_with_attributes_1", &attributes),
195 "bar baz"
196 );
197 }
198
199 #[test]
200 fn handles_literal_prefixes_and_suffixes() {
201 let attributes = GetterMap(HashMap::from([("attr3", Val::String("baz"))]));
202 assert_eq!(
203 name_for_op_and_attributes("op_with_attributes_1", &attributes),
204 "prefix baz suffix",
205 );
206 }
207
208 #[test]
209 fn considers_multiple_files() {
210 let attributes = GetterMap(HashMap::new());
211 assert_eq!(
212 name_for_op_and_attributes("op_in_second_name_file", &attributes),
213 "second file literal name",
214 );
215 }
216
217 #[test]
218 fn falls_back_to_op_for_unknown_ops() {
219 let attributes = GetterMap(HashMap::new());
220 assert_eq!(
221 name_for_op_and_attributes("unknown_op", &attributes),
222 "unknown_op",
223 );
224 }
225
226 #[test]
227 fn handles_multiple_value_types() {
228 let attributes = GetterMap(HashMap::from([("attr1", Val::Bool(true))]));
229 assert_eq!(
230 name_for_op_and_attributes("op_with_attributes_1", &attributes),
231 "true",
232 );
233
234 let attributes = GetterMap(HashMap::from([("attr1", Val::I64(123))]));
235 assert_eq!(
236 name_for_op_and_attributes("op_with_attributes_1", &attributes),
237 "123",
238 );
239
240 let attributes = GetterMap(HashMap::from([("attr1", Val::U64(123))]));
241 assert_eq!(
242 name_for_op_and_attributes("op_with_attributes_1", &attributes),
243 "123",
244 );
245
246 let attributes = GetterMap(HashMap::from([("attr1", Val::F64(1.23))]));
247 assert_eq!(
248 name_for_op_and_attributes("op_with_attributes_1", &attributes),
249 "1.23",
250 );
251 }
252}