1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use std::sync::Arc;
4use schemars::JsonSchema;
5use crate::config::site::SiteConfig;
6use crate::config::strategy::{Strategy, StrategyRef, LegacyStrategyCollection as StrategyCollection};
7
8#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
11#[serde(default, deny_unknown_fields)]
12pub struct GlobalConfig {
13 #[serde(default)]
15 pub domain: String,
16
17 #[serde(default)]
19 pub domains: Vec<String>,
20
21 #[serde(default)]
23 pub listeners: Vec<Listener>,
24
25 #[serde(default)]
27 pub routes: Vec<Route>,
28
29 #[serde(default)]
31 pub log: LogConfig,
32
33 #[serde(default = "default_proxy_id")]
35 pub proxy_id: String,
36
37 #[serde(default)]
39 pub sites_enabled: PathBuf,
40
41 #[serde(default = "default_strategy")]
43 pub strategy: Option<StrategyRef>,
44
45 #[serde(default)]
47 pub strategies: StrategyCollection,
48}
49
50impl Default for GlobalConfig {
51 fn default() -> Self {
52 Self {
53 domain: String::default(),
54 domains: Vec::default(),
55 listeners: Vec::default(),
56 routes: Vec::default(),
57 log: LogConfig::default(),
58 proxy_id: default_proxy_id(),
59 sites_enabled: PathBuf::default(),
60 strategy: default_strategy(),
61 strategies: StrategyCollection::default(),
62 }
63 }
64}
65
66impl GlobalConfig {
67 pub fn get_all_domains(&self) -> Vec<String> {
69 let mut domains = Vec::with_capacity(1 + self.domains.len());
70 if !self.domain.is_empty() {
71 domains.push(self.domain.clone());
72 }
73 domains.extend(self.domains.iter().cloned());
74 domains
75 }
76
77 pub fn has_domains(&self) -> bool {
79 !self.domain.is_empty() || !self.domains.is_empty()
80 }
81
82 pub fn to_site_config(&self) -> SiteConfig {
84 SiteConfig {
85 domain: self.domain.clone(),
86 domains: self.domains.clone(),
87 listeners: self.listeners.clone(),
88 routes: self.routes.clone(),
89 strategy: self.strategy.clone(),
90 strategies: self.strategies.clone(),
91 }
92 }
93
94 pub fn get_default_strategy(&self) -> Option<Strategy> {
96 match &self.strategy {
97 Some(strategy_ref) => strategy_ref.resolve(&self.strategies),
98 None => None,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
104pub struct Listener {
105 #[serde(default = "default_host")]
107 pub host: String,
108
109 #[serde(default)]
111 pub port: u16,
112
113 #[serde(default)]
115 pub tls: Option<Tls>,
116}
117
118fn default_host() -> String {
119 "0.0.0.0".to_string()
120}
121
122#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
123pub struct Tls {
124 #[serde(default)]
125 pub self_signed: bool,
126 #[serde(default)]
127 pub cert: PathBuf,
128 #[serde(default)]
129 pub key: PathBuf,
130}
131
132#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
134#[serde(untagged)]
135pub enum Route {
136 Proxy {
137 #[serde(rename = "match")]
138 r#match: Match,
139 backend: String,
140 #[serde(default)]
141 strategy: Option<StrategyRef>,
142 #[serde(default)]
143 strategies: Option<StrategyCollection>,
144 },
145 Static {
146 #[serde(rename = "match")]
147 r#match: Match,
148 static_dir: PathBuf,
149 #[serde(default)]
150 strategy: Option<StrategyRef>,
151 #[serde(default)]
152 strategies: Option<StrategyCollection>,
153 },
154 Redirect {
155 #[serde(rename = "match")]
156 r#match: Match,
157 redirect: Redirect,
158 #[serde(default)]
159 strategy: Option<StrategyRef>,
160 #[serde(default)]
161 strategies: Option<StrategyCollection>,
162 },
163}
164
165impl Route {
166 pub fn get_strategy(&self) -> Option<&StrategyRef> {
168 match self {
169 Route::Proxy { strategy, .. } => strategy.as_ref(),
170 Route::Static { strategy, .. } => strategy.as_ref(),
171 Route::Redirect { strategy, .. } => strategy.as_ref(),
172 }
173 }
174
175 pub fn get_strategies(&self) -> Option<&StrategyCollection> {
177 match self {
178 Route::Proxy { strategies, .. } => strategies.as_ref(),
179 Route::Static { strategies, .. } => strategies.as_ref(),
180 Route::Redirect { strategies, .. } => strategies.as_ref(),
181 }
182 }
183
184 pub fn get_match(&self) -> &Match {
186 match self {
187 Route::Proxy { r#match, .. } => r#match,
188 Route::Static { r#match, .. } => r#match,
189 Route::Redirect { r#match, .. } => r#match,
190 }
191 }
192}
193
194#[derive(Debug, Clone, Deserialize, Serialize, Default, JsonSchema)]
195pub struct Match {
196 #[serde(default)]
198 pub path: Option<String>,
199
200 #[serde(default)]
202 pub path_regex: Option<String>,
203}
204
205#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
206pub struct Redirect {
207 pub to: String,
208
209 #[serde(default = "default_redirect_code")]
210 pub code: u16,
211}
212
213fn default_redirect_code() -> u16 {
214 301
215}
216
217#[derive(Debug, Clone, Deserialize, Serialize, Default, JsonSchema)]
218pub struct LogConfig {
219 #[serde(default = "default_log_level")]
221 pub level: String,
222}
223
224fn default_log_level() -> String {
225 "warn".to_string()
226}
227
228fn default_strategy() -> Option<StrategyRef> {
229 Some(StrategyRef::Named("default".to_string()))
230}
231
232fn default_proxy_id() -> String {
233 "httpward".to_string()
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::config::strategy::StrategyRef;
240 use crate::config::MiddlewareConfig;
241
242 #[test]
243 fn test_global_default_strategy() {
244 let config = GlobalConfig::default();
245
246 assert!(config.strategy.is_some(), "Strategy should be Some, got {:?}", config.strategy);
248
249 match config.strategy.unwrap() {
250 StrategyRef::Named(name) => assert_eq!(name, "default"),
251 _ => panic!("Expected Named strategy"),
252 }
253 }
254
255 #[test]
256 fn test_global_strategy_resolution() {
257 let mut config = GlobalConfig::default();
258
259 config.strategies.insert("default".to_string(), vec![
261 crate::config::strategy::MiddlewareConfig::new_named_json(
262 "logging".to_string(),
263 serde_json::json!({"level": "info"})
264 )
265 ]);
266
267 let resolved = config.get_default_strategy();
269 assert!(resolved.is_some());
270
271 let strategy = resolved.unwrap();
272 assert_eq!(strategy.name, "default");
273 assert_eq!(strategy.middleware.len(), 1);
274 assert_eq!(strategy.middleware[0].name(), "logging");
275 }
276
277 #[test]
278 fn test_global_inline_strategy() {
279 let mut config = GlobalConfig::default();
280
281 let inline_strategy = Strategy {
283 name: "inline_test".to_string(),
284 middleware: Arc::new(vec![
285 crate::config::strategy::MiddlewareConfig::new_named_json(
286 "rate_limit".to_string(),
287 serde_json::json!({"requests": 1000, "window": "1m"})
288 ),
289 crate::config::strategy::MiddlewareConfig::new_named_json(
290 "logging".to_string(),
291 serde_json::json!({"level": "debug"})
292 )
293 ])
294 };
295
296 config.strategy = Some(StrategyRef::InlineMiddleware(vec![
297 MiddlewareConfig::new_named_json(
298 "rate_limit".to_string(),
299 serde_json::json!({"requests": 1000, "window": "1m"})
300 ),
301 MiddlewareConfig::new_named_json(
302 "logging".to_string(),
303 serde_json::json!({"level": "debug"})
304 )
305 ]));
306
307 let resolved = config.get_default_strategy();
309 assert!(resolved.is_some());
310
311 let strategy = resolved.unwrap();
312 assert_eq!(strategy.name, "inline");
313 assert_eq!(strategy.middleware.len(), 2);
314 assert_eq!(strategy.middleware[0].name(), "rate_limit");
315 assert_eq!(strategy.middleware[1].name(), "logging");
316 }
317}