1use serde::Serialize;
7
8#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
10#[serde(rename_all = "lowercase")]
11pub enum VarKind {
12 Static,
13 Family,
14}
15
16#[derive(Debug, Serialize)]
19pub struct VarEntry {
20 pub name: &'static str,
21 pub kind: VarKind,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub family_source: Option<&'static str>,
24 pub description: &'static str,
25 pub example: &'static str,
26 pub path: &'static str,
29}
30
31const S: VarKind = VarKind::Static;
32const F: VarKind = VarKind::Family;
33
34fn e(
35 name: &'static str,
36 kind: VarKind,
37 family_source: Option<&'static str>,
38 description: &'static str,
39 example: &'static str,
40 path: &'static str,
41) -> VarEntry {
42 VarEntry {
43 name,
44 kind,
45 family_source,
46 description,
47 example,
48 path,
49 }
50}
51
52pub fn var_catalog() -> Vec<VarEntry> {
54 vec![
55 e(
56 "uri",
57 S,
58 None,
59 "Request path (no query string)",
60 "$uri",
61 "request.path",
62 ),
63 e(
64 "request_uri",
65 S,
66 None,
67 "Path plus ?query when query params exist",
68 "$request_uri",
69 "",
70 ),
71 e(
72 "method",
73 S,
74 None,
75 "HTTP method (alias: request_method)",
76 "$method",
77 "request.method",
78 ),
79 e(
80 "request_method",
81 S,
82 None,
83 "HTTP method (alias of method)",
84 "$request_method",
85 "request.method",
86 ),
87 e("host", S, None, "Request Host", "$host", "request.host"),
88 e(
89 "scheme",
90 S,
91 None,
92 "http or https",
93 "$scheme",
94 "request.scheme",
95 ),
96 e(
97 "protocol",
98 S,
99 None,
100 "HTTP protocol version (http1, http2, ...)",
101 "$protocol",
102 "",
103 ),
104 e(
105 "remote_addr",
106 S,
107 None,
108 "Client IP without port",
109 "$remote_addr",
110 "client.ip",
111 ),
112 e(
113 "remote_port",
114 S,
115 None,
116 "Client port",
117 "$remote_port",
118 "client.port",
119 ),
120 e(
121 "query_string",
122 S,
123 None,
124 "Full query string, rebuilt and sorted",
125 "$query_string",
126 "",
127 ),
128 e(
129 "status",
130 S,
131 None,
132 "Response status code",
133 "$status",
134 "response.status",
135 ),
136 e(
137 "resp_body",
138 S,
139 None,
140 "Response body (lossy UTF-8)",
141 "$resp_body",
142 "response.body",
143 ),
144 e(
145 "request_body",
146 S,
147 None,
148 "Request body (lossy UTF-8)",
149 "$request_body",
150 "request.body",
151 ),
152 e(
153 "consumer_name",
154 S,
155 None,
156 "Authenticated consumer name (set by auth plugins)",
157 "$consumer_name",
158 "message.consumer.name",
159 ),
160 e(
161 "consumer_group_id",
162 S,
163 None,
164 "Authenticated consumer group",
165 "$consumer_group_id",
166 "message.consumer.group",
167 ),
168 e(
169 "arg_*",
170 F,
171 Some("query_params"),
172 "First value of a query parameter",
173 "$arg_page",
174 "request.query.*",
175 ),
176 e(
177 "http_*",
178 F,
179 Some("request_headers"),
180 "First value of a request header (underscores map to dashes)",
181 "$http_user_agent",
182 "request.headers.*",
183 ),
184 e(
185 "cookie_*",
186 F,
187 Some("cookies"),
188 "Value from the Cookie request header",
189 "$cookie_session",
190 "request.cookies.*",
191 ),
192 e(
193 "post_arg_*",
194 F,
195 Some("form_body"),
196 "Form field from an application/x-www-form-urlencoded body",
197 "$post_arg_username",
198 "",
199 ),
200 e(
201 "msg_*",
202 F,
203 Some("message"),
204 "Any context.message key, stringified; dotted keys need ${msg_key.with.dots}",
205 "${msg_consumer.name}",
206 "message.*",
207 ),
208 e(
209 "sent_http_*",
210 F,
211 Some("response_headers"),
212 "First value of a response header (underscores map to dashes)",
213 "$sent_http_content_type",
214 "response.headers.*",
215 ),
216 ]
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use std::collections::BTreeSet;
223
224 #[test]
229 fn test_catalog_matches_resolver() {
230 let src = include_str!("mod.rs");
231 let start = src.find("pub fn resolve").expect("resolve fn present");
234 let rest = &src[start..];
235 let end = rest[10..]
236 .find("pub fn ")
237 .map(|i| i + 10)
238 .unwrap_or(rest.len());
239 let body = &rest[..end];
240
241 let mut from_source: BTreeSet<String> = BTreeSet::new();
242 for line in body.lines() {
243 let t = line.trim();
244 if t.contains("=>") {
245 let mut s = t;
247 while let Some(open) = s.find('"') {
248 let after = &s[open + 1..];
249 if let Some(close) = after.find('"') {
250 let name = &after[..close];
251 if !name.is_empty()
252 && name.chars().all(|c| c.is_ascii_lowercase() || c == '_')
253 {
254 from_source.insert(name.to_string());
255 }
256 s = &after[close + 1..];
257 } else {
258 break;
259 }
260 }
261 }
262 if let Some(idx) = t.find("strip_prefix(\"") {
263 let after = &t[idx + 14..];
264 if let Some(close) = after.find('"') {
265 from_source.insert(format!("{}*", &after[..close]));
266 }
267 }
268 }
269 from_source.remove("consumer.name");
273 from_source.remove("consumer.group");
274
275 let from_catalog: BTreeSet<String> =
276 var_catalog().iter().map(|v| v.name.to_string()).collect();
277
278 assert_eq!(from_catalog, from_source, "catalog drifted from resolve()");
279 }
280
281 #[test]
282 fn test_catalog_families_have_sources_and_statics_do_not() {
283 for v in var_catalog() {
284 match v.kind {
285 VarKind::Family => {
286 assert!(
287 v.family_source.is_some(),
288 "{} missing family_source",
289 v.name
290 );
291 assert!(
292 v.name.ends_with("_*"),
293 "{} family name must end in _*",
294 v.name
295 );
296 }
297 VarKind::Static => assert!(v.family_source.is_none(), "{}", v.name),
298 }
299 }
300 }
301}