Skip to main content

vane_core/preset/
mod.rs

1//! Preset expansion: `{"preset": ..., ...}` โ†’ `Vec<RawRule>`.
2//!
3//! Presets are opinionated compile-stage expansions that turn high-level
4//! intent into raw-rule bundles. The four built-in presets are
5//! `reverse_proxy`, `port_forward`, `static_site`, and `redirect_https`.
6//!
7//! See [`spec/crates/core.md` ยง _Compile pipeline_](../../../../spec/crates/core.md#compile-pipeline).
8
9mod port_forward;
10mod redirect_https;
11mod reverse_proxy;
12mod static_site;
13
14use serde_json::Value;
15
16use crate::error::Error;
17use crate::rule::{ListenSpec, RawRule, SourceInfo, TlsConfig};
18
19/// User-authored preset invocation. The `preset` field discriminates
20/// which expander runs; `args` is opaque at parse time and validated
21/// inside the expander.
22#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
23pub struct PresetInvocation {
24	/// Base name; the expander prefixes synth rules (`<name>.main`,
25	/// `<name>.ws`, `<name>.ws-allow`, `<name>.ws-deny`).
26	pub name: String,
27	/// Discriminator. One of `reverse_proxy` / `port_forward` /
28	/// `static_site` / `redirect_https`.
29	pub preset: String,
30	pub listen: Vec<ListenSpec>,
31	#[serde(default)]
32	pub args: Value,
33	/// Optional TLS termination config โ€” same shape as `RawRule.tls`.
34	/// Each preset's `expand()` propagates this to every emitted
35	/// rule on the listener, so a `reverse_proxy` preset that emits
36	/// `<name>.ws` + `<name>.main` carries the same TLS config on
37	/// both rules and `lower_port`'s consistency check passes.
38	#[serde(default)]
39	pub tls: Option<TlsConfig>,
40	#[serde(default)]
41	pub source: SourceInfo,
42}
43
44/// File-level entry: either a hand-written raw rule or a preset
45/// invocation that expands to one or more raw rules. Discrimination is
46/// by presence of the top-level `preset` key โ€” the custom `Deserialize`
47/// peeks at the JSON before routing to the right variant so a malformed
48/// preset payload produces a pointed error instead of falling through to
49/// `RawRule` parsing and surfacing a confusing "missing terminate" error.
50#[derive(Debug, Clone, serde::Serialize)]
51#[serde(untagged)]
52pub enum RuleEntry {
53	Preset(PresetInvocation),
54	Raw(RawRule),
55}
56
57impl<'de> serde::Deserialize<'de> for RuleEntry {
58	fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
59		let v = Value::deserialize(d)?;
60		if v.get("preset").is_some() {
61			let inv: PresetInvocation = serde_json::from_value(v).map_err(serde::de::Error::custom)?;
62			Ok(Self::Preset(inv))
63		} else {
64			let r: RawRule = serde_json::from_value(v).map_err(serde::de::Error::custom)?;
65			Ok(Self::Raw(r))
66		}
67	}
68}
69
70/// Dispatch on `inv.preset` to the appropriate expander.
71///
72/// # Errors
73/// Returns [`Error::compile`] when `inv.preset` names an unknown preset,
74/// or when the dispatched expander rejects `inv.args`.
75pub fn expand_invocation(inv: PresetInvocation) -> Result<Vec<RawRule>, Error> {
76	match inv.preset.as_str() {
77		"reverse_proxy" => reverse_proxy::expand(inv),
78		"port_forward" => port_forward::expand(inv),
79		"static_site" => static_site::expand(inv),
80		"redirect_https" => redirect_https::expand(inv),
81		other => Err(Error::compile(format!(
82			"unknown preset {other:?}; supported: reverse_proxy / port_forward / static_site / redirect_https"
83		))),
84	}
85}
86
87#[cfg(test)]
88mod tests {
89	use super::*;
90
91	#[test]
92	fn unknown_preset_name_yields_compile_error() {
93		let inv = PresetInvocation {
94			name: "x".into(),
95			preset: "no_such_preset".into(),
96			listen: vec![":443".into()],
97			args: Value::Null,
98			tls: None,
99			source: SourceInfo::default(),
100		};
101		let err = expand_invocation(inv).expect_err("unknown preset must fail");
102		let msg = err.to_string();
103		assert!(msg.contains("no_such_preset"), "error names the offending preset: {msg}");
104		assert!(msg.contains("reverse_proxy"), "error lists supported presets: {msg}");
105	}
106
107	#[test]
108	fn rule_entry_deserializes_preset_when_preset_key_present() {
109		let raw = serde_json::json!({
110			"preset": "port_forward",
111			"name": "ssh",
112			"listen": [":2222"],
113			"args": { "upstream": "10.0.0.5:22" }
114		});
115		let entry: RuleEntry = serde_json::from_value(raw).expect("parse preset entry");
116		match entry {
117			RuleEntry::Preset(inv) => {
118				assert_eq!(inv.preset, "port_forward");
119				assert_eq!(inv.name, "ssh");
120				assert_eq!(inv.listen, vec![":2222".to_string()]);
121			}
122			RuleEntry::Raw(_) => panic!("preset key must route to Preset variant"),
123		}
124	}
125
126	#[test]
127	fn rule_entry_deserializes_raw_when_no_preset_key() {
128		let raw = serde_json::json!({
129			"name": "r",
130			"listen": [":443"],
131			"terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" }
132		});
133		let entry: RuleEntry = serde_json::from_value(raw).expect("parse raw entry");
134		match entry {
135			RuleEntry::Raw(r) => assert_eq!(r.name, "r"),
136			RuleEntry::Preset(_) => panic!("no preset key must route to Raw variant"),
137		}
138	}
139}