Skip to main content

vane_core/compile/
expand.rs

1use std::collections::HashSet;
2
3use crate::compile::merge::MergedConfig;
4use crate::error::Error;
5use crate::preset::{RuleEntry, expand_invocation};
6use crate::rule::RawRule;
7
8#[derive(Debug, Clone)]
9pub struct RawRuleSet {
10	pub rules: Vec<RawRule>,
11	pub source_files: Vec<std::path::PathBuf>,
12}
13
14/// Preset expansion. Walks the merged `RuleEntry` list, dispatching
15/// each `Preset(inv)` to its expander and passing `Raw(r)` through
16/// verbatim. After concatenation, enforces uniqueness across the full
17/// post-expansion `RawRule` name set — presets can synthesise names
18/// (`<base>.main`, `<base>.ws-allow`, etc.) that only become visible
19/// here, so this is the right layer for the dup check.
20///
21/// # Errors
22/// Returns [`Error::compile`] for unknown preset names, preset arg
23/// validation failures, or duplicate rule names after expansion.
24pub fn expand(merged: MergedConfig) -> Result<RawRuleSet, Error> {
25	let mut rules: Vec<RawRule> = Vec::new();
26	for entry in merged.rules {
27		match entry {
28			RuleEntry::Raw(r) => rules.push(r),
29			RuleEntry::Preset(inv) => rules.extend(expand_invocation(inv)?),
30		}
31	}
32
33	let mut seen: HashSet<&str> = HashSet::with_capacity(rules.len());
34	for r in &rules {
35		if !seen.insert(r.name.as_str()) {
36			return Err(Error::compile(format!(
37				"duplicate rule name after preset expansion: {:?}",
38				r.name
39			)));
40		}
41	}
42
43	Ok(RawRuleSet { rules, source_files: merged.source_files })
44}
45
46#[cfg(test)]
47mod tests {
48	use std::path::PathBuf;
49
50	use super::*;
51	use crate::preset::{PresetInvocation, RuleEntry};
52	use crate::rule::{RawRule, SourceInfo};
53
54	fn raw(name: &str) -> RawRule {
55		let raw = serde_json::json!({
56			"name": name,
57			"listen": [":443"],
58			"terminate": { "type": "http_proxy" },
59		});
60		serde_json::from_value(raw).expect("parse rule")
61	}
62
63	fn port_forward_invocation(name: &str) -> PresetInvocation {
64		PresetInvocation {
65			name: name.to_string(),
66			preset: "port_forward".to_string(),
67			listen: vec![":2222".into()],
68			args: serde_json::json!({ "upstream": "10.0.0.5:22" }),
69			tls: None,
70			source: SourceInfo::default(),
71		}
72	}
73
74	fn merged(rules: Vec<RuleEntry>) -> MergedConfig {
75		MergedConfig { rules, source_files: vec![PathBuf::from("rules/x.json")] }
76	}
77
78	#[test]
79	fn expand_passes_through_raw_only_input() {
80		let m = merged(vec![RuleEntry::Raw(raw("a")), RuleEntry::Raw(raw("b"))]);
81		let out = expand(m).expect("expand");
82		let names: Vec<_> = out.rules.iter().map(|r| r.name.as_str()).collect();
83		assert_eq!(names, vec!["a", "b"]);
84	}
85
86	#[test]
87	fn expand_concatenates_raw_and_preset_entries() {
88		let m = merged(vec![
89			RuleEntry::Raw(raw("first")),
90			RuleEntry::Preset(port_forward_invocation("fwd")),
91			RuleEntry::Raw(raw("last")),
92		]);
93		let out = expand(m).expect("expand");
94		let names: Vec<_> = out.rules.iter().map(|r| r.name.as_str()).collect();
95		assert_eq!(names, vec!["first", "fwd", "last"]);
96	}
97
98	#[test]
99	fn expand_detects_dup_name_after_preset_expansion() {
100		// Two reverse_proxy presets with the same `name` both emit `<name>.main` —
101		// expansion-side dup check catches it.
102		let inv_a = PresetInvocation {
103			name: "api".to_string(),
104			preset: "reverse_proxy".to_string(),
105			listen: vec![":443".into()],
106			args: serde_json::json!({ "upstream": "u:1" }),
107			tls: None,
108			source: SourceInfo::default(),
109		};
110		let inv_b = PresetInvocation {
111			name: "api".to_string(),
112			preset: "reverse_proxy".to_string(),
113			listen: vec![":443".into()],
114			args: serde_json::json!({ "upstream": "u:2" }),
115			tls: None,
116			source: SourceInfo::default(),
117		};
118		let m = merged(vec![RuleEntry::Preset(inv_a), RuleEntry::Preset(inv_b)]);
119		let err = expand(m).expect_err("dup must surface");
120		let msg = err.to_string();
121		assert!(msg.contains("duplicate"), "error mentions duplicate: {msg}");
122		assert!(msg.contains("api"), "error names the offending base name: {msg}");
123	}
124
125	#[test]
126	fn expand_preserves_source_files() {
127		let m = merged(vec![RuleEntry::Raw(raw("a"))]);
128		let out = expand(m).expect("expand");
129		assert_eq!(out.source_files, vec![PathBuf::from("rules/x.json")]);
130	}
131}