Skip to main content

synapse_proxy/
builder.rs

1//! Compile parsed config + a transform registry into ready-to-serve routes.
2//! The binary uses built-ins only; library consumers register custom transforms
3//! by name before `build()`.
4
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use crate::config::{Config, RequestStep, ResponseStep, Route};
9use crate::context::ContextStore;
10use crate::transform::error_remap::ErrorRemap;
11use crate::transform::inject::Inject;
12use crate::transform::wrap::Wrap;
13use crate::transform::{RequestTransform, ResponseTransform, TransformRegistry};
14
15pub struct CompiledRoute {
16    pub name: String,
17    pub path_prefix: String,
18    pub upstream: String,
19    pub strip_prefix: bool,
20    pub methods: Vec<String>, // upper-case; empty = any
21    pub require_context: Vec<String>,
22    pub request: Vec<Arc<dyn RequestTransform>>,
23    pub response: Vec<Arc<dyn ResponseTransform>>,
24}
25
26impl std::fmt::Debug for CompiledRoute {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.debug_struct("CompiledRoute")
29            .field("name", &self.name)
30            .field("path_prefix", &self.path_prefix)
31            .field("upstream", &self.upstream)
32            .field("strip_prefix", &self.strip_prefix)
33            .field("methods", &self.methods)
34            .field("require_context", &self.require_context)
35            .field("request_len", &self.request.len())
36            .field("response_len", &self.response.len())
37            .finish()
38    }
39}
40
41pub struct BuiltProxy {
42    pub routes: Vec<CompiledRoute>,
43    pub context: Arc<ContextStore>,
44    pub admin_addr: String,
45    pub metrics_addr: String,
46    pub addr: String,
47}
48
49impl std::fmt::Debug for BuiltProxy {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.debug_struct("BuiltProxy")
52            .field("routes", &self.routes)
53            .field("admin_addr", &self.admin_addr)
54            .field("metrics_addr", &self.metrics_addr)
55            .field("addr", &self.addr)
56            .finish()
57    }
58}
59
60pub struct ProxyBuilder {
61    config: Config,
62    registry: TransformRegistry,
63}
64
65impl ProxyBuilder {
66    pub fn from_config(config: Config) -> Self {
67        Self {
68            config,
69            registry: TransformRegistry::default(),
70        }
71    }
72    pub fn request_transform(
73        mut self,
74        name: impl Into<String>,
75        t: Arc<dyn RequestTransform>,
76    ) -> Self {
77        self.registry.register_request(name, t);
78        self
79    }
80    pub fn response_transform(
81        mut self,
82        name: impl Into<String>,
83        t: Arc<dyn ResponseTransform>,
84    ) -> Self {
85        self.registry.register_response(name, t);
86        self
87    }
88
89    /// Build the context store (static ⊕ env, env wins) and compile every route.
90    pub fn build(self) -> anyhow::Result<BuiltProxy> {
91        let mut base: HashMap<String, String> = self.config.context.static_values.clone();
92        for (key, var) in &self.config.context.env {
93            if let Ok(val) = std::env::var(var) {
94                if !val.trim().is_empty() {
95                    base.insert(key.clone(), val); // env precedence over static
96                }
97            }
98        }
99        let context = Arc::new(ContextStore::new(base));
100
101        let routes = self
102            .config
103            .routes
104            .iter()
105            .map(|r| compile_route(r, &self.registry))
106            .collect::<anyhow::Result<Vec<_>>>()?;
107
108        Ok(BuiltProxy {
109            routes,
110            context,
111            admin_addr: self.config.admin_addr.clone(),
112            metrics_addr: self.config.metrics_addr.clone(),
113            addr: self.config.addr.clone(),
114        })
115    }
116}
117
118fn compile_route(r: &Route, reg: &TransformRegistry) -> anyhow::Result<CompiledRoute> {
119    let mut request: Vec<Arc<dyn RequestTransform>> = Vec::new();
120    // cycle-1 sugar: static headers become inject steps first.
121    for (name, value) in &r.headers {
122        request.push(Arc::new(Inject::from_spec(&crate::config::InjectSpec {
123            header: Some(name.clone()),
124            body: None,
125            from_context: None,
126            constant: Some(serde_json::Value::String(value.clone())),
127        })?));
128    }
129    for step in &r.request_steps {
130        request.push(match step {
131            RequestStep::Inject(s) => Arc::new(Inject::from_spec(s)?),
132            RequestStep::Wrap(s) => Arc::new(Wrap::from_spec(s)?),
133            RequestStep::Transform(name) => reg.request.get(name).cloned().ok_or_else(|| {
134                anyhow::anyhow!(
135                    "unknown request transform '{name}' on route '{}'",
136                    r.label()
137                )
138            })?,
139        });
140    }
141    let mut response: Vec<Arc<dyn ResponseTransform>> = Vec::new();
142    for step in &r.response_steps {
143        response.push(match step {
144            ResponseStep::ErrorRemap(s) => Arc::new(ErrorRemap::from_spec(s)?),
145            ResponseStep::Transform(name) => reg.response.get(name).cloned().ok_or_else(|| {
146                anyhow::anyhow!(
147                    "unknown response transform '{name}' on route '{}'",
148                    r.label()
149                )
150            })?,
151        });
152    }
153    Ok(CompiledRoute {
154        name: r.label().to_string(),
155        path_prefix: r.path_prefix.clone(),
156        upstream: r.upstream.clone(),
157        strip_prefix: r.strip_prefix,
158        methods: r.methods.iter().map(|m| m.to_ascii_uppercase()).collect(),
159        require_context: r.require_context.clone(),
160        request,
161        response,
162    })
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn compiles_builtins_and_sugar() {
171        let cfg = Config::from_toml_str(
172            r#"
173            [[routes]]
174            name = "r"
175            path_prefix = "/x"
176            upstream = "http://u"
177            headers = { x-static = "v" }
178            request_steps = [ { inject = { header = "X-Org", from_context = "org" } } ]
179            response_steps = [ { error_remap = { when_status = 401, error = "auth_expired" } } ]
180        "#,
181        )
182        .unwrap();
183        let built = ProxyBuilder::from_config(cfg).build().unwrap();
184        let route = &built.routes[0];
185        assert_eq!(route.request.len(), 2); // static header sugar + inject step
186        assert_eq!(route.response.len(), 1);
187    }
188
189    #[test]
190    fn unknown_named_transform_fails_fast() {
191        let cfg = Config::from_toml_str(
192            r#"
193            [[routes]]
194            path_prefix = "/x"
195            upstream = "http://u"
196            request_steps = [ { transform = "nope" } ]
197        "#,
198        )
199        .unwrap();
200        let err = ProxyBuilder::from_config(cfg).build().unwrap_err();
201        assert!(err.to_string().contains("unknown request transform 'nope'"));
202    }
203}