vane_core/compile/
expand.rs1use 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
14pub 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 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}