Skip to main content

camel_integration_test/
env_layers.rs

1//! Layered hermetic environment source (ADR-0069 §4).
2//!
3//! Resolution order for [`LayeredEnv::lookup`]: harness-provisioned
4//! bindings (the `bindVar` keys of `provisioning: harness` endpoints,
5//! guaranteed absent from the document `env` map by the parser's
6//! reserved-key rule), then the document `env` map, then the injected
7//! ambient lookup — but only for keys listed in the passthrough
8//! allowlist. Everything else resolves to `None`.
9//!
10//! Hermeticity contract: nothing in this crate writes the process
11//! environment, and the only ambient reads go through the closure
12//! injected at construction. Production callers pass [`ambient_std`];
13//! tests inject map closures.
14
15use std::collections::BTreeMap;
16use std::sync::Arc;
17
18/// Ambient lookup closure: maps a variable name to its value.
19pub type AmbientLookup = Arc<dyn Fn(&str) -> Option<String> + Send + Sync>;
20
21pub struct LayeredEnv {
22    doc: BTreeMap<String, String>,
23    harness_provisioned: BTreeMap<String, String>,
24    passthrough: Vec<String>,
25    ambient: AmbientLookup,
26}
27
28impl LayeredEnv {
29    pub fn new(
30        doc: BTreeMap<String, String>,
31        harness_provisioned: BTreeMap<String, String>,
32        passthrough: Vec<String>,
33        ambient: AmbientLookup,
34    ) -> Self {
35        Self {
36            doc,
37            harness_provisioned,
38            passthrough,
39            ambient,
40        }
41    }
42
43    /// Resolves `key` through the layer precedence: harness-provisioned
44    /// bindings first, then the document `env` map, then the injected
45    /// ambient lookup iff `key` is listed in the passthrough allowlist.
46    pub fn lookup(&self, key: &str) -> Option<String> {
47        if let Some(value) = self.harness_provisioned.get(key) {
48            return Some(value.clone());
49        }
50        if let Some(value) = self.doc.get(key) {
51            return Some(value.clone());
52        }
53        if self.passthrough.iter().any(|allowed| allowed == key) {
54            return (self.ambient)(key);
55        }
56        None
57    }
58
59    /// Returns a copy of this environment with one extra
60    /// harness-provisioned binding layered on top: `lookup(key)`
61    /// resolves `value` with the same precedence as partner bindVars
62    /// (harness layer wins over the document `env` map). The receiver
63    /// is untouched — `boot_scenario` derives the extended discovery
64    /// environment from the caller's env for the inbound listener's
65    /// bindVar (rc-5yon), so route-file `${env:NAME}` templates resolve
66    /// to the staged socket.
67    pub fn with_harness_var(&self, key: &str, value: String) -> LayeredEnv {
68        let mut harness_provisioned = self.harness_provisioned.clone();
69        harness_provisioned.insert(key.to_string(), value);
70        LayeredEnv {
71            doc: self.doc.clone(),
72            harness_provisioned,
73            passthrough: self.passthrough.clone(),
74            ambient: Arc::clone(&self.ambient),
75        }
76    }
77}
78
79/// Wires `std::env::var` as the ambient lookup for production callers.
80pub fn ambient_std() -> AmbientLookup {
81    Arc::new(|key| std::env::var(key).ok())
82}