Skip to main content

vane_core/
rule.rs

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		// Every row of 05-terminator.md § _Variant ergonomics in config_.
285		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		// 14-presets.md § _RawRule shape_: "every other key goes into `args`
309		// verbatim". Covers top-level scalars AND nested objects.
310		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		// 14-presets.md § _RawRule shape_: the custom Deserialize removes `type`
329		// from a JSON object and keeps the rest. An alias-only terminate leaves
330		// an empty object behind — NOT Value::Null.
331		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}