Skip to main content

vane_core/compile/
merge.rs

1use std::path::PathBuf;
2
3use crate::error::Error;
4use crate::rule::RawRule;
5
6#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
7pub struct RawRuleFile {
8	pub path: PathBuf,
9	#[serde(default)]
10	pub order: i32,
11	#[serde(default)]
12	pub rules: Vec<RawRule>,
13}
14
15#[derive(Debug, Clone)]
16pub struct MergedConfig {
17	pub rules: Vec<RawRule>,
18	pub source_files: Vec<PathBuf>,
19}
20
21/// Merge multiple rule files into a single canonical rule set.
22///
23/// # Errors
24/// Returns [`Error::compile`] when two rules across the input files share
25/// a `name`.
26pub fn merge(mut files: Vec<RawRuleFile>) -> Result<MergedConfig, Error> {
27	files.sort_by(|a, b| a.order.cmp(&b.order).then_with(|| a.path.cmp(&b.path)));
28
29	let mut rules: Vec<RawRule> = Vec::new();
30	let mut source_files: Vec<PathBuf> = Vec::with_capacity(files.len());
31	for file in files {
32		source_files.push(file.path);
33		for rule in file.rules {
34			if rules.iter().any(|existing| existing.name == rule.name) {
35				return Err(Error::compile(format!("duplicate rule name: {:?}", rule.name)));
36			}
37			rules.push(rule);
38		}
39	}
40	Ok(MergedConfig { rules, source_files })
41}
42
43#[cfg(test)]
44mod tests {
45	use super::*;
46	use crate::error::ErrorKind;
47
48	fn rule(name: &str) -> RawRule {
49		let raw = serde_json::json!({
50			"name": name,
51			"listen": [":443"],
52			"terminate": { "type": "http_proxy" },
53		});
54		serde_json::from_value(raw).expect("parse rule")
55	}
56
57	fn file(path: &str, order: i32, rules: Vec<RawRule>) -> RawRuleFile {
58		RawRuleFile { path: PathBuf::from(path), order, rules }
59	}
60
61	#[test]
62	fn sorts_by_order_then_path_stable() {
63		// 09-config.md ยง _Merge_: stable-sort by (order asc, filename lex).
64		let files = vec![
65			file("b.json", 10, vec![rule("b")]),
66			file("a.json", 10, vec![rule("a")]),
67			file("0.json", 0, vec![rule("zero")]),
68		];
69		let merged = merge(files).expect("merge ok");
70		let names: Vec<_> = merged.rules.iter().map(|r| r.name.as_str()).collect();
71		assert_eq!(names, vec!["zero", "a", "b"]);
72	}
73
74	#[test]
75	fn rejects_duplicate_rule_names_with_compile_error() {
76		let files = vec![file("a.json", 0, vec![rule("same")]), file("b.json", 1, vec![rule("same")])];
77		let err = merge(files).expect_err("duplicate must error");
78		assert!(matches!(err.kind(), ErrorKind::Compile));
79	}
80
81	#[test]
82	fn preserves_every_source_file_path() {
83		let files = vec![file("x.json", 0, vec![]), file("y.json", 0, vec![])];
84		let merged = merge(files).expect("merge ok");
85		assert_eq!(merged.source_files, vec![PathBuf::from("x.json"), PathBuf::from("y.json")],);
86	}
87}