1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde_json::Value;
5
6use crate::fetch::FetchKind;
7use crate::predicate::Predicate;
8
9pub type ListenSpec = String;
10
11#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
12pub struct RawRule {
13 pub name: String,
14 pub listen: Vec<ListenSpec>,
15 #[serde(default, rename = "match")]
16 pub match_predicate: Option<Predicate>,
17 #[serde(default)]
18 pub middleware_chain: Vec<MiddlewareRef>,
19 pub terminate: TerminateSpec,
20 #[serde(default)]
21 pub source: SourceInfo,
22}
23
24#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
25pub struct MiddlewareRef {
26 #[serde(rename = "use")]
27 pub name: String,
28 #[serde(default)]
29 pub args: Value,
30 #[serde(default)]
31 pub on_error: Option<OnErrorSpec>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
35pub enum OnErrorSpec {
36 Close,
37 Response(SynthResponse),
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
41pub struct SynthResponse {
42 pub status: u16,
43 #[serde(default)]
44 pub headers: Option<BTreeMap<String, String>>,
45 #[serde(default)]
46 pub body: Option<String>,
47}
48
49impl<'de> serde::Deserialize<'de> for OnErrorSpec {
50 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
51 #[derive(serde::Deserialize)]
52 #[serde(untagged)]
53 enum Raw {
54 Literal(String),
55 Response { response: SynthResponse },
56 }
57 match Raw::deserialize(de)? {
58 Raw::Literal(s) if s == "close" => Ok(Self::Close),
59 Raw::Literal(other) => Err(serde::de::Error::unknown_variant(&other, &["close"])),
60 Raw::Response { response } => Ok(Self::Response(response)),
61 }
62 }
63}
64
65#[derive(Debug, Clone, serde::Serialize)]
66pub struct TerminateSpec {
67 pub kind: FetchKind,
68 pub args: Value,
69}
70
71impl<'de> serde::Deserialize<'de> for TerminateSpec {
72 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
73 let mut v = Value::deserialize(de)?;
74 let obj = v
75 .as_object_mut()
76 .ok_or_else(|| serde::de::Error::custom("`terminate` must be a JSON object"))?;
77 let type_val = obj.remove("type").ok_or_else(|| serde::de::Error::missing_field("type"))?;
78 let Value::String(alias) = type_val else {
79 return Err(serde::de::Error::custom("`terminate.type` must be a string"));
80 };
81 let kind = fetch_kind_from_alias(&alias)
82 .ok_or_else(|| serde::de::Error::custom(format!("unknown terminate type: {alias:?}")))?;
83 Ok(Self { kind, args: v })
84 }
85}
86
87fn fetch_kind_from_alias(alias: &str) -> Option<FetchKind> {
88 match alias {
89 "tcp_forward" | "udp_forward" => Some(FetchKind::L4Forward),
90 "http_proxy" | "http1_proxy" | "http2_proxy" | "http3_proxy" | "unix_proxy" | "cgi" => {
91 Some(FetchKind::HttpProxy)
92 }
93 "websocket" => Some(FetchKind::WebSocketUpgrade),
94 "static" | "redirect_https" => Some(FetchKind::HttpSynthesize),
95 _ => None,
96 }
97}
98
99#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
100pub struct SourceInfo {
101 #[serde(default)]
102 pub file: PathBuf,
103 #[serde(default)]
104 pub line: u32,
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::predicate::{CheckMap, FieldPath, Operator, Predicate, Value as PredValue};
111
112 #[test]
113 fn raw_rule_minimal_parses_with_defaults() {
114 let raw = serde_json::json!({
115 "name": "r",
116 "listen": [":443"],
117 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
118 });
119 let rule: RawRule = serde_json::from_value(raw).expect("parse minimal rule");
120 assert_eq!(rule.name, "r");
121 assert_eq!(rule.listen, vec![":443".to_string()]);
122 assert!(rule.match_predicate.is_none());
123 assert!(rule.middleware_chain.is_empty());
124 assert_eq!(rule.terminate.kind, FetchKind::HttpProxy);
125 assert_eq!(rule.terminate.args, serde_json::json!({ "upstream": "127.0.0.1:8080" }));
126 assert_eq!(rule.source.file, PathBuf::new());
127 assert_eq!(rule.source.line, 0);
128 }
129
130 #[test]
131 fn raw_rule_full_populates_every_field() {
132 let raw = serde_json::json!({
133 "name": "api",
134 "listen": [":443", "0.0.0.0:80"],
135 "match": { "tls.sni": { "equals": "api.example.com" } },
136 "middleware_chain": [
137 { "use": "rate_limit", "args": { "rate": 100 } },
138 { "use": "jwt", "args": { "secret": "x" }, "on_error": "close" },
139 ],
140 "terminate": {
141 "type": "http_proxy",
142 "upstream": "127.0.0.1:8080",
143 "timeouts": { "connect": "5s" }
144 },
145 "source": { "file": "rules/30-api.json", "line": 14 },
146 });
147 let rule: RawRule = serde_json::from_value(raw).expect("parse full rule");
148 assert_eq!(rule.name, "api");
149 assert_eq!(rule.listen.len(), 2);
150 let check = match rule.match_predicate.as_ref().expect("match present") {
151 Predicate::Check(c) => c,
152 other => panic!("expected Check, got {other:?}"),
153 };
154 assert_eq!(check.path, FieldPath::TlsSni);
155 match &check.op {
156 Operator::Equals(PredValue::Str(s)) => assert_eq!(s, "api.example.com"),
157 other => panic!("unexpected op: {other:?}"),
158 }
159 assert_eq!(rule.middleware_chain.len(), 2);
160 assert_eq!(rule.middleware_chain[1].on_error, Some(OnErrorSpec::Close));
161 assert_eq!(rule.terminate.kind, FetchKind::HttpProxy);
162 assert_eq!(
163 rule.terminate.args,
164 serde_json::json!({
165 "upstream": "127.0.0.1:8080",
166 "timeouts": { "connect": "5s" }
167 }),
168 );
169 assert_eq!(rule.source.file, PathBuf::from("rules/30-api.json"));
170 assert_eq!(rule.source.line, 14);
171 }
172
173 #[test]
174 fn middleware_ref_flat_form_parses_name_and_args() {
175 let raw = serde_json::json!({ "use": "rate_limit", "args": { "rate": 100 } });
176 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
177 assert_eq!(m.name, "rate_limit");
178 assert_eq!(m.args, serde_json::json!({ "rate": 100 }));
179 assert!(m.on_error.is_none());
180 }
181
182 #[test]
183 fn middleware_ref_on_error_close_form() {
184 let raw = serde_json::json!({ "use": "jwt", "args": { "secret": "x" }, "on_error": "close" });
185 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
186 assert_eq!(m.on_error, Some(OnErrorSpec::Close));
187 }
188
189 #[test]
190 fn middleware_ref_on_error_response_object_form() {
191 let raw = serde_json::json!({
192 "use": "jwt",
193 "on_error": { "response": { "status": 503, "body": "maintenance" } },
194 });
195 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
196 assert_eq!(m.name, "jwt");
197 assert_eq!(m.args, Value::Null);
198 let resp = match m.on_error.expect("on_error present") {
199 OnErrorSpec::Response(r) => r,
200 OnErrorSpec::Close => panic!("expected Response"),
201 };
202 assert_eq!(resp.status, 503);
203 assert_eq!(resp.body.as_deref(), Some("maintenance"));
204 assert!(resp.headers.is_none());
205 }
206
207 #[test]
208 fn middleware_ref_args_defaults_to_null_when_omitted() {
209 let raw = serde_json::json!({ "use": "tag" });
210 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
211 assert_eq!(m.args, Value::Null);
212 }
213
214 #[test]
215 fn middleware_ref_requires_use_key() {
216 let raw = serde_json::json!({});
217 let err = serde_json::from_value::<MiddlewareRef>(raw).expect_err("missing `use` must fail");
218 let _ = err.to_string();
219 }
220
221 #[test]
222 fn on_error_spec_string_invalid_variant_rejected() {
223 let raw = serde_json::json!("crash");
224 let err = serde_json::from_value::<OnErrorSpec>(raw).expect_err("non-`close` literal rejected");
225 let msg = err.to_string();
226 assert!(msg.contains("close"), "error names the only valid literal: {msg}");
227 }
228
229 #[test]
230 fn on_error_spec_malformed_response_object_rejected() {
231 let raw = serde_json::json!({ "response": null });
232 let err = serde_json::from_value::<OnErrorSpec>(raw).expect_err("null response rejected");
233 let _ = err.to_string();
234 }
235
236 #[test]
237 fn on_error_spec_close_literal_parses() {
238 let raw = serde_json::json!("close");
239 let s: OnErrorSpec = serde_json::from_value(raw).expect("close literal parses");
240 assert_eq!(s, OnErrorSpec::Close);
241 }
242
243 #[test]
244 fn on_error_spec_response_object_parses() {
245 let raw = serde_json::json!({
246 "response": { "status": 503, "body": "maintenance" },
247 });
248 let s: OnErrorSpec = serde_json::from_value(raw).expect("response object parses");
249 match s {
250 OnErrorSpec::Response(r) => {
251 assert_eq!(r.status, 503);
252 assert_eq!(r.body.as_deref(), Some("maintenance"));
253 assert!(r.headers.is_none());
254 }
255 OnErrorSpec::Close => panic!("expected Response"),
256 }
257 }
258
259 #[test]
260 fn synth_response_minimal_status_only() {
261 let raw = serde_json::json!({ "status": 200 });
262 let r: SynthResponse = serde_json::from_value(raw).expect("parse status-only synth");
263 assert_eq!(r.status, 200);
264 assert!(r.headers.is_none());
265 assert!(r.body.is_none());
266 }
267
268 #[test]
269 fn synth_response_full_status_headers_body() {
270 let raw = serde_json::json!({
271 "status": 404,
272 "headers": { "content-type": "text/plain" },
273 "body": "not found",
274 });
275 let r: SynthResponse = serde_json::from_value(raw).expect("parse full synth");
276 assert_eq!(r.status, 404);
277 let headers = r.headers.as_ref().expect("headers present");
278 assert_eq!(headers.get("content-type").map(String::as_str), Some("text/plain"));
279 assert_eq!(r.body.as_deref(), Some("not found"));
280 }
281
282 #[test]
283 fn terminate_spec_alias_table_maps_to_fetch_kind() {
284 let cases: &[(&str, FetchKind)] = &[
286 ("tcp_forward", FetchKind::L4Forward),
287 ("udp_forward", FetchKind::L4Forward),
288 ("http_proxy", FetchKind::HttpProxy),
289 ("http1_proxy", FetchKind::HttpProxy),
290 ("http2_proxy", FetchKind::HttpProxy),
291 ("http3_proxy", FetchKind::HttpProxy),
292 ("unix_proxy", FetchKind::HttpProxy),
293 ("cgi", FetchKind::HttpProxy),
294 ("websocket", FetchKind::WebSocketUpgrade),
295 ("static", FetchKind::HttpSynthesize),
296 ("redirect_https", FetchKind::HttpSynthesize),
297 ];
298 for (alias, expected) in cases {
299 let raw = serde_json::json!({ "type": alias });
300 let t: TerminateSpec =
301 serde_json::from_value(raw).unwrap_or_else(|e| panic!("alias {alias} must parse: {e}"));
302 assert_eq!(t.kind, *expected, "alias {alias} must map to {expected:?}");
303 }
304 }
305
306 #[test]
307 fn terminate_spec_args_preserves_all_non_type_keys_verbatim() {
308 let raw = serde_json::json!({
311 "type": "http_proxy",
312 "upstream": "127.0.0.1:8080",
313 "timeouts": { "connect": "5s", "total": "60s" },
314 });
315 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
316 assert_eq!(t.kind, FetchKind::HttpProxy);
317 assert_eq!(
318 t.args,
319 serde_json::json!({
320 "upstream": "127.0.0.1:8080",
321 "timeouts": { "connect": "5s", "total": "60s" },
322 }),
323 );
324 }
325
326 #[test]
327 fn terminate_spec_alias_only_yields_empty_object_not_null() {
328 let raw = serde_json::json!({ "type": "http_proxy" });
332 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
333 assert_eq!(t.kind, FetchKind::HttpProxy);
334 assert_eq!(t.args, serde_json::Value::Object(serde_json::Map::new()));
335 assert!(t.args.is_object(), "args must be an object, got {:?}", t.args);
336 }
337
338 #[test]
339 fn terminate_spec_unknown_type_rejected_and_names_alias() {
340 let raw = serde_json::json!({ "type": "bogus" });
341 let err = serde_json::from_value::<TerminateSpec>(raw).expect_err("unknown alias rejected");
342 assert!(err.to_string().contains("bogus"), "error must name the offending alias: {err}");
343 }
344
345 #[test]
346 fn terminate_spec_missing_type_rejected_and_names_field() {
347 let raw = serde_json::json!({ "upstream": "127.0.0.1:8080" });
348 let err = serde_json::from_value::<TerminateSpec>(raw).expect_err("missing type rejected");
349 assert!(err.to_string().contains("type"), "error must name the missing field: {err}");
350 }
351
352 #[test]
353 fn source_info_default_is_empty_path_and_zero_line() {
354 let s = SourceInfo::default();
355 assert_eq!(s.file, PathBuf::new());
356 assert_eq!(s.line, 0);
357 }
358
359 #[test]
360 fn source_info_round_trip_via_json() {
361 let raw = serde_json::json!({ "file": "rules/a.json", "line": 7 });
362 let s: SourceInfo = serde_json::from_value(raw).expect("parse source info");
363 assert_eq!(s.file, PathBuf::from("rules/a.json"));
364 assert_eq!(s.line, 7);
365 }
366
367 #[test]
368 fn middleware_chain_defaults_to_empty_when_omitted() {
369 let raw = serde_json::json!({
370 "name": "r",
371 "listen": [":443"],
372 "terminate": { "type": "http_proxy" },
373 });
374 let rule: RawRule = serde_json::from_value(raw).expect("parse");
375 assert!(rule.middleware_chain.is_empty());
376 }
377
378 #[test]
379 fn middleware_ref_chain_mixes_on_error_forms() {
380 let raw = serde_json::json!({
381 "name": "r",
382 "listen": [":443"],
383 "middleware_chain": [
384 { "use": "a" },
385 { "use": "b", "on_error": "close" },
386 { "use": "c", "on_error": { "response": { "status": 500 } } },
387 ],
388 "terminate": { "type": "http_proxy" },
389 });
390 let rule: RawRule = serde_json::from_value(raw).expect("parse");
391 assert_eq!(rule.middleware_chain.len(), 3);
392 assert!(rule.middleware_chain[0].on_error.is_none());
393 assert_eq!(rule.middleware_chain[1].on_error, Some(OnErrorSpec::Close));
394 match rule.middleware_chain[2].on_error.as_ref().expect("on_error[2]") {
395 OnErrorSpec::Response(r) => {
396 assert_eq!(r.status, 500);
397 assert!(r.body.is_none());
398 assert!(r.headers.is_none());
399 }
400 OnErrorSpec::Close => panic!("expected Response at index 2"),
401 }
402 }
403
404 #[test]
405 fn raw_rule_accepts_top_level_check_predicate() {
406 let raw = serde_json::json!({
407 "name": "r",
408 "listen": [":80"],
409 "match": { "http.uri.path": { "prefix": "/api" } },
410 "terminate": { "type": "http_proxy" },
411 });
412 let rule: RawRule = serde_json::from_value(raw).expect("parse");
413 let Some(Predicate::Check(CheckMap { path, op })) = rule.match_predicate else {
414 panic!("expected Check predicate");
415 };
416 assert_eq!(path, FieldPath::HttpUriPath);
417 match op {
418 Operator::Prefix(PredValue::Str(s)) => assert_eq!(s, "/api"),
419 other => panic!("unexpected op: {other:?}"),
420 }
421 }
422}