1use crate::{CacheStorageMode, CacheStrategy, CompressStrategy};
2use anyhow::{bail, Result};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
9#[serde(rename_all = "snake_case")]
10pub enum ProxyModeConfig {
11 #[default]
13 Dynamic,
14 PreGenerate,
17}
18
19#[derive(Debug, Clone, Deserialize, Serialize)]
38pub struct Config {
39 #[serde(default = "default_http_port")]
41 pub http_port: u16,
42
43 pub https_port: Option<u16>,
46
47 pub cert_path: Option<PathBuf>,
49
50 pub key_path: Option<PathBuf>,
52
53 #[serde(default = "default_control_port")]
55 pub control_port: u16,
56
57 pub control_auth: Option<String>,
59
60 pub server: HashMap<String, ServerConfig>,
62}
63
64#[derive(Debug, Clone, Deserialize, Serialize)]
66pub struct ServerConfig {
67 #[serde(default = "default_bind_to")]
79 pub bind_to: String,
80
81 #[serde(default = "default_proxy_url")]
83 pub proxy_url: String,
84
85 #[serde(default)]
88 pub include_paths: Vec<String>,
89
90 #[serde(default)]
94 pub exclude_paths: Vec<String>,
95
96 #[serde(default = "default_enable_websocket")]
105 pub enable_websocket: bool,
106
107 #[serde(default = "default_forward_get_only")]
109 pub forward_get_only: bool,
110
111 #[serde(default = "default_cache_404_capacity")]
113 pub cache_404_capacity: usize,
114
115 #[serde(default = "default_use_404_meta")]
117 pub use_404_meta: bool,
118
119 #[serde(default)]
121 pub cache_strategy: CacheStrategy,
122
123 #[serde(default)]
125 pub compress_strategy: CompressStrategy,
126
127 #[serde(default)]
129 pub cache_storage_mode: CacheStorageMode,
130
131 #[serde(default)]
133 pub cache_directory: Option<PathBuf>,
134
135 #[serde(default)]
137 pub proxy_mode: ProxyModeConfig,
138
139 #[serde(default)]
141 pub pre_generate_paths: Vec<String>,
142
143 #[serde(default = "default_pre_generate_fallthrough")]
146 pub pre_generate_fallthrough: bool,
147}
148
149fn default_http_port() -> u16 {
152 3000
153}
154
155fn default_control_port() -> u16 {
156 17809
157}
158
159fn default_bind_to() -> String {
160 "*".to_string()
161}
162
163fn default_proxy_url() -> String {
164 "http://localhost:8080".to_string()
165}
166
167fn default_enable_websocket() -> bool {
168 true
169}
170
171fn default_forward_get_only() -> bool {
172 false
173}
174
175fn default_cache_404_capacity() -> usize {
176 100
177}
178
179fn default_use_404_meta() -> bool {
180 false
181}
182
183fn default_pre_generate_fallthrough() -> bool {
184 false
185}
186
187impl Config {
190 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
191 let content = std::fs::read_to_string(path)?;
192 let config: Config = toml::from_str(&content)?;
193 config.validate()?;
194 Ok(config)
195 }
196
197 fn validate(&self) -> Result<()> {
198 if self.https_port.is_some() {
199 if self.cert_path.is_none() {
200 bail!("`cert_path` is required when `https_port` is set");
201 }
202 if self.key_path.is_none() {
203 bail!("`key_path` is required when `https_port` is set");
204 }
205 }
206 if self.server.is_empty() {
207 bail!("at least one `[server.NAME]` block is required");
208 }
209 Ok(())
210 }
211}
212
213impl Default for ServerConfig {
214 fn default() -> Self {
215 Self {
216 bind_to: default_bind_to(),
217 proxy_url: default_proxy_url(),
218 include_paths: vec![],
219 exclude_paths: vec![],
220 enable_websocket: default_enable_websocket(),
221 forward_get_only: default_forward_get_only(),
222 cache_404_capacity: default_cache_404_capacity(),
223 use_404_meta: default_use_404_meta(),
224 cache_strategy: CacheStrategy::default(),
225 compress_strategy: CompressStrategy::default(),
226 cache_storage_mode: CacheStorageMode::default(),
227 cache_directory: None,
228 proxy_mode: ProxyModeConfig::default(),
229 pre_generate_paths: vec![],
230 pre_generate_fallthrough: false,
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 fn single_server_toml(extra: &str) -> String {
240 format!(
241 "[server.default]\nproxy_url = \"http://localhost:8080\"\n{}",
242 extra
243 )
244 }
245
246 #[test]
247 fn test_config_defaults_cache_strategy_to_all() {
248 let config: Config = toml::from_str(&single_server_toml("")).unwrap();
249 let s = config.server.get("default").unwrap();
250 assert_eq!(s.cache_strategy, CacheStrategy::All);
251 assert_eq!(s.compress_strategy, CompressStrategy::Brotli);
252 assert_eq!(s.cache_storage_mode, CacheStorageMode::Memory);
253 assert_eq!(s.cache_directory, None);
254 }
255
256 #[test]
257 fn test_config_parses_cache_strategy() {
258 let config: Config =
259 toml::from_str(&single_server_toml("cache_strategy = \"none\"\n")).unwrap();
260 let s = config.server.get("default").unwrap();
261 assert_eq!(s.cache_strategy, CacheStrategy::None);
262 }
263
264 #[test]
265 fn test_config_parses_compress_strategy() {
266 let config: Config =
267 toml::from_str(&single_server_toml("compress_strategy = \"gzip\"\n")).unwrap();
268 let s = config.server.get("default").unwrap();
269 assert_eq!(s.compress_strategy, CompressStrategy::Gzip);
270 }
271
272 #[test]
273 fn test_config_parses_cache_storage_mode() {
274 let config: Config = toml::from_str(&single_server_toml(
275 "cache_storage_mode = \"filesystem\"\ncache_directory = \"cache-bodies\"\n",
276 ))
277 .unwrap();
278 let s = config.server.get("default").unwrap();
279 assert_eq!(s.cache_storage_mode, CacheStorageMode::Filesystem);
280 assert_eq!(s.cache_directory, Some(PathBuf::from("cache-bodies")));
281 }
282
283 #[test]
284 fn test_config_top_level_ports() {
285 let toml = "http_port = 8080\ncontrol_port = 9000\n".to_string()
286 + &single_server_toml("");
287 let config: Config = toml::from_str(&toml).unwrap();
288 assert_eq!(config.http_port, 8080);
289 assert_eq!(config.control_port, 9000);
290 assert_eq!(config.https_port, None);
291 }
292
293 #[test]
294 fn test_https_validation_requires_cert_and_key() {
295 let toml = "https_port = 443\n".to_string() + &single_server_toml("");
296 let config: Config = toml::from_str(&toml).unwrap();
297 assert!(config.validate().is_err());
298 }
299
300 #[test]
301 fn test_multiple_servers() {
302 let toml = "[server.frontend]\nbind_to = \"*\"\nproxy_url = \"http://localhost:5173\"\n\
303 [server.api]\nbind_to = \"/api\"\nproxy_url = \"http://localhost:8080\"\n";
304 let config: Config = toml::from_str(toml).unwrap();
305 assert_eq!(config.server.len(), 2);
306 assert_eq!(
307 config.server.get("api").unwrap().bind_to,
308 "/api"
309 );
310 assert_eq!(
311 config.server.get("frontend").unwrap().bind_to,
312 "*"
313 );
314 }
315}