1use 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>, 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 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); }
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 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); 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}