Skip to main content

featherbit/graph/
prepare.rs

1//! The pre-compile pipeline shared by the sandbox, the MCP `validate_policy`
2//! tool and config apply: structural validation, `config_ref` resolution and
3//! supernode expansion, in that order.
4
5use crate::config::{GatewayConfig, PluginConfigDef, PolicyConfig, SupernodeConfig};
6use crate::graph::{expand_policy, validate_policy};
7
8/// Turns a stored policy into the compile-ready form.
9///
10/// Errors are the human-readable strings the Admin API already returns:
11/// `validate_policy` violations joined with `"; "`, then the first
12/// resolution or expansion error.
13pub fn prepare_policy(
14    policy: PolicyConfig,
15    supernodes: &[SupernodeConfig],
16    plugin_configs: &[PluginConfigDef],
17) -> Result<PolicyConfig, String> {
18    if let Err(errors) = validate_policy(&policy) {
19        return Err(errors.join("; "));
20    }
21    let mut tmp: GatewayConfig = serde_yaml::from_str("{}").expect("empty config parses");
22    tmp.policies = vec![policy];
23    tmp.supernodes = supernodes.to_vec();
24    tmp.plugin_configs = plugin_configs.to_vec();
25    let resolved = crate::config::resolve_plugin_configs(&tmp)?;
26    for warning in crate::config::collect_template_warnings(&resolved) {
27        tracing::warn!("{warning}");
28    }
29    let policy = resolved
30        .policies
31        .into_iter()
32        .next()
33        .expect("one policy in, one policy out");
34    expand_policy(&policy, &resolved.supernodes)
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    fn policy(yaml: &str) -> PolicyConfig {
42        serde_yaml::from_str(yaml).unwrap()
43    }
44
45    #[test]
46    fn rejects_structural_errors_with_joined_messages() {
47        let p = policy("name: p\nnodes:\n  - id: l\n    type: listener\nedges: []\n");
48        let err = prepare_policy(p, &[], &[]).unwrap_err();
49        assert!(err.contains("client"), "{err}");
50    }
51
52    #[test]
53    fn resolves_config_ref_and_passes_valid_policy_through() {
54        let p = policy(
55            "name: p\nnodes:\n  - id: l\n    type: listener\n  - id: e\n    type: echo\n    config_ref: shared-echo\n  - id: c\n    type: client\nedges:\n  - from: l.out\n    to: e.in\n  - from: e.out\n    to: c.in\n",
56        );
57        let pc: PluginConfigDef =
58            serde_yaml::from_str("name: shared-echo\ntype: echo\nconfig:\n  body: hi\n").unwrap();
59        let out = prepare_policy(p, &[], &[pc]).unwrap();
60        let echo = out.nodes.iter().find(|n| n.id == "e").unwrap();
61        assert_eq!(echo.config.get("body").and_then(|v| v.as_str()), Some("hi"));
62    }
63
64    #[test]
65    fn unknown_config_ref_is_an_error() {
66        let p = policy(
67            "name: p\nnodes:\n  - id: l\n    type: listener\n  - id: e\n    type: echo\n    config_ref: nope\n  - id: c\n    type: client\nedges:\n  - from: l.out\n    to: e.in\n  - from: e.out\n    to: c.in\n",
68        );
69        let err = prepare_policy(p, &[], &[]).unwrap_err();
70        assert!(err.contains("nope"), "{err}");
71    }
72}