1use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30use std::net::IpAddr;
31use std::path::{Path, PathBuf};
32use std::str::FromStr;
33use thiserror::Error;
34
35#[derive(Debug, Error)]
37pub enum ConfigError {
38 #[error("Invalid configuration: {0}")]
40 Invalid(String),
41
42 #[error("Failed to read config file: {0}")]
44 Io(#[from] std::io::Error),
45
46 #[error("Failed to parse TOML: {0}")]
48 TomlParse(#[from] toml::de::Error),
49
50 #[error("Missing required field: {0}")]
52 MissingField(String),
53
54 #[error("Layer not found: {0}")]
56 LayerNotFound(String),
57}
58
59pub type ConfigResult<T> = Result<T, ConfigError>;
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ServerConfig {
65 #[serde(default = "default_host")]
67 pub host: IpAddr,
68
69 #[serde(default = "default_port")]
71 pub port: u16,
72
73 #[serde(default = "default_workers")]
75 pub workers: usize,
76
77 #[serde(default = "default_max_request_size")]
79 pub max_request_size: usize,
80
81 #[serde(default = "default_timeout")]
83 pub timeout_seconds: u64,
84
85 #[serde(default = "default_cors")]
87 pub enable_cors: bool,
88
89 #[serde(default)]
91 pub cors_origins: Vec<String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct CacheConfig {
97 #[serde(default = "default_memory_cache_mb")]
99 pub memory_size_mb: usize,
100
101 #[serde(default)]
103 pub disk_cache: Option<PathBuf>,
104
105 #[serde(default = "default_ttl_seconds")]
107 pub ttl_seconds: u64,
108
109 #[serde(default = "default_enable_stats")]
111 pub enable_stats: bool,
112
113 #[serde(default = "default_compression")]
115 pub compression: bool,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct LayerConfig {
121 pub name: String,
123
124 #[serde(default)]
126 pub title: Option<String>,
127
128 #[serde(default)]
130 pub abstract_: Option<String>,
131
132 pub path: PathBuf,
134
135 #[serde(default = "default_formats")]
137 pub formats: Vec<ImageFormat>,
138
139 #[serde(default = "default_tile_size")]
141 pub tile_size: u32,
142
143 #[serde(default)]
145 pub min_zoom: u8,
146
147 #[serde(default = "default_max_zoom")]
149 pub max_zoom: u8,
150
151 #[serde(default = "default_tile_matrix_sets")]
153 pub tile_matrix_sets: Vec<String>,
154
155 #[serde(default)]
157 pub style: Option<StyleConfig>,
158
159 #[serde(default)]
161 pub metadata: HashMap<String, String>,
162
163 #[serde(default = "default_enabled")]
165 pub enabled: bool,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct StyleConfig {
171 pub name: String,
173
174 #[serde(default)]
176 pub colormap: Option<String>,
177
178 #[serde(default)]
180 pub value_range: Option<(f64, f64)>,
181
182 #[serde(default = "default_alpha")]
184 pub alpha: f32,
185
186 #[serde(default = "default_gamma")]
188 pub gamma: f32,
189
190 #[serde(default)]
192 pub brightness: f32,
193
194 #[serde(default = "default_contrast")]
196 pub contrast: f32,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
201#[serde(rename_all = "lowercase")]
202pub enum ImageFormat {
203 Png,
205 Jpeg,
207 Webp,
209 Geotiff,
211}
212
213impl ImageFormat {
214 pub fn mime_type(&self) -> &'static str {
216 match self {
217 ImageFormat::Png => "image/png",
218 ImageFormat::Jpeg => "image/jpeg",
219 ImageFormat::Webp => "image/webp",
220 ImageFormat::Geotiff => "image/tiff",
221 }
222 }
223
224 pub fn extension(&self) -> &'static str {
226 match self {
227 ImageFormat::Png => "png",
228 ImageFormat::Jpeg => "jpg",
229 ImageFormat::Webp => "webp",
230 ImageFormat::Geotiff => "tif",
231 }
232 }
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct ParseImageFormatError(String);
238
239impl std::fmt::Display for ParseImageFormatError {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 write!(f, "unknown image format: {}", self.0)
242 }
243}
244
245impl std::error::Error for ParseImageFormatError {}
246
247impl FromStr for ImageFormat {
248 type Err = ParseImageFormatError;
249
250 fn from_str(s: &str) -> Result<Self, Self::Err> {
251 match s.to_lowercase().as_str() {
252 "png" => Ok(ImageFormat::Png),
253 "jpeg" | "jpg" => Ok(ImageFormat::Jpeg),
254 "webp" => Ok(ImageFormat::Webp),
255 "geotiff" | "tif" | "tiff" => Ok(ImageFormat::Geotiff),
256 _ => Err(ParseImageFormatError(s.to_string())),
257 }
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct Config {
264 #[serde(default)]
266 pub server: ServerConfig,
267
268 #[serde(default)]
270 pub cache: CacheConfig,
271
272 #[serde(default)]
274 pub layers: Vec<LayerConfig>,
275
276 #[serde(default)]
278 pub metadata: MetadataConfig,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct MetadataConfig {
284 #[serde(default = "default_service_title")]
286 pub title: String,
287
288 #[serde(default = "default_service_abstract")]
290 pub abstract_: String,
291
292 #[serde(default)]
294 pub contact: Option<ContactInfo>,
295
296 #[serde(default)]
298 pub keywords: Vec<String>,
299
300 #[serde(default)]
302 pub online_resource: Option<String>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct ContactInfo {
308 pub organization: String,
310
311 #[serde(default)]
313 pub person: Option<String>,
314
315 #[serde(default)]
317 pub email: Option<String>,
318
319 #[serde(default)]
321 pub phone: Option<String>,
322}
323
324fn default_host() -> IpAddr {
326 IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0))
327}
328
329fn default_port() -> u16 {
330 8080
331}
332
333fn default_workers() -> usize {
334 std::thread::available_parallelism()
335 .map(|n| n.get())
336 .ok()
337 .unwrap_or(4)
338}
339
340fn default_max_request_size() -> usize {
341 10 * 1024 * 1024 }
343
344fn default_timeout() -> u64 {
345 30
346}
347
348fn default_cors() -> bool {
349 true
350}
351
352fn default_memory_cache_mb() -> usize {
353 256
354}
355
356fn default_ttl_seconds() -> u64 {
357 3600
358}
359
360fn default_enable_stats() -> bool {
361 true
362}
363
364fn default_compression() -> bool {
365 false
366}
367
368fn default_formats() -> Vec<ImageFormat> {
369 vec![ImageFormat::Png, ImageFormat::Jpeg]
370}
371
372fn default_tile_size() -> u32 {
373 256
374}
375
376fn default_max_zoom() -> u8 {
377 18
378}
379
380fn default_tile_matrix_sets() -> Vec<String> {
381 vec!["WebMercatorQuad".to_string(), "WorldCRS84Quad".to_string()]
382}
383
384fn default_enabled() -> bool {
385 true
386}
387
388fn default_alpha() -> f32 {
389 1.0
390}
391
392fn default_gamma() -> f32 {
393 1.0
394}
395
396fn default_contrast() -> f32 {
397 1.0
398}
399
400fn default_service_title() -> String {
401 "OxiGDAL Tile Server".to_string()
402}
403
404fn default_service_abstract() -> String {
405 "WMS/WMTS tile server powered by OxiGDAL".to_string()
406}
407
408impl Default for ServerConfig {
409 fn default() -> Self {
410 Self {
411 host: default_host(),
412 port: default_port(),
413 workers: default_workers(),
414 max_request_size: default_max_request_size(),
415 timeout_seconds: default_timeout(),
416 enable_cors: default_cors(),
417 cors_origins: Vec::new(),
418 }
419 }
420}
421
422impl Default for CacheConfig {
423 fn default() -> Self {
424 Self {
425 memory_size_mb: default_memory_cache_mb(),
426 disk_cache: None,
427 ttl_seconds: default_ttl_seconds(),
428 enable_stats: default_enable_stats(),
429 compression: default_compression(),
430 }
431 }
432}
433
434impl Default for MetadataConfig {
435 fn default() -> Self {
436 Self {
437 title: default_service_title(),
438 abstract_: default_service_abstract(),
439 contact: None,
440 keywords: Vec::new(),
441 online_resource: None,
442 }
443 }
444}
445
446impl Config {
447 pub fn from_file<P: AsRef<Path>>(path: P) -> ConfigResult<Self> {
449 let contents = std::fs::read_to_string(path)?;
450 Self::from_toml(&contents)
451 }
452
453 pub fn from_toml(toml: &str) -> ConfigResult<Self> {
455 let config: Config = toml::from_str(toml)?;
456 config.validate()?;
457 Ok(config)
458 }
459
460 pub fn default_config() -> Self {
462 Self {
463 server: ServerConfig::default(),
464 cache: CacheConfig::default(),
465 layers: Vec::new(),
466 metadata: MetadataConfig::default(),
467 }
468 }
469
470 pub fn validate(&self) -> ConfigResult<()> {
472 let mut names = std::collections::HashSet::new();
474 for layer in &self.layers {
475 if !names.insert(&layer.name) {
476 return Err(ConfigError::Invalid(format!(
477 "Duplicate layer name: {}",
478 layer.name
479 )));
480 }
481
482 if !layer.path.exists() {
484 return Err(ConfigError::Invalid(format!(
485 "Layer path does not exist: {}",
486 layer.path.display()
487 )));
488 }
489
490 if !layer.tile_size.is_power_of_two() {
492 return Err(ConfigError::Invalid(format!(
493 "Tile size must be power of 2, got {}",
494 layer.tile_size
495 )));
496 }
497
498 if layer.min_zoom > layer.max_zoom {
500 return Err(ConfigError::Invalid(format!(
501 "min_zoom ({}) cannot be greater than max_zoom ({})",
502 layer.min_zoom, layer.max_zoom
503 )));
504 }
505 }
506
507 if self.cache.memory_size_mb == 0 {
509 return Err(ConfigError::Invalid(
510 "Cache memory size must be greater than 0".to_string(),
511 ));
512 }
513
514 Ok(())
515 }
516
517 pub fn get_layer(&self, name: &str) -> ConfigResult<&LayerConfig> {
519 self.layers
520 .iter()
521 .find(|l| l.name == name && l.enabled)
522 .ok_or_else(|| ConfigError::LayerNotFound(name.to_string()))
523 }
524
525 pub fn enabled_layers(&self) -> impl Iterator<Item = &LayerConfig> {
527 self.layers.iter().filter(|l| l.enabled)
528 }
529
530 pub fn bind_address(&self) -> String {
532 format!("{}:{}", self.server.host, self.server.port)
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn test_default_config() {
542 let config = Config::default_config();
543 assert_eq!(config.server.port, 8080);
544 assert_eq!(config.cache.memory_size_mb, 256);
545 assert!(config.layers.is_empty());
546 }
547
548 #[test]
549 fn test_image_format_mime_types() {
550 assert_eq!(ImageFormat::Png.mime_type(), "image/png");
551 assert_eq!(ImageFormat::Jpeg.mime_type(), "image/jpeg");
552 assert_eq!(ImageFormat::Webp.mime_type(), "image/webp");
553 assert_eq!(ImageFormat::Geotiff.mime_type(), "image/tiff");
554 }
555
556 #[test]
557 fn test_image_format_from_str() {
558 assert_eq!("png".parse::<ImageFormat>().ok(), Some(ImageFormat::Png));
559 assert_eq!("PNG".parse::<ImageFormat>().ok(), Some(ImageFormat::Png));
560 assert_eq!("jpeg".parse::<ImageFormat>().ok(), Some(ImageFormat::Jpeg));
561 assert_eq!("jpg".parse::<ImageFormat>().ok(), Some(ImageFormat::Jpeg));
562 assert_eq!("webp".parse::<ImageFormat>().ok(), Some(ImageFormat::Webp));
563 assert_eq!(
564 "geotiff".parse::<ImageFormat>().ok(),
565 Some(ImageFormat::Geotiff)
566 );
567 assert!("invalid".parse::<ImageFormat>().is_err());
568 }
569
570 #[test]
571 fn test_config_from_toml() {
572 let toml = r#"
573 [server]
574 host = "127.0.0.1"
575 port = 9000
576 workers = 8
577
578 [cache]
579 memory_size_mb = 512
580 ttl_seconds = 7200
581
582 [metadata]
583 title = "Test Server"
584 "#;
585
586 let config = Config::from_toml(toml).expect("valid config");
587 assert_eq!(config.server.host.to_string(), "127.0.0.1");
588 assert_eq!(config.server.port, 9000);
589 assert_eq!(config.server.workers, 8);
590 assert_eq!(config.cache.memory_size_mb, 512);
591 assert_eq!(config.metadata.title, "Test Server");
592 }
593
594 #[test]
595 fn test_bind_address() {
596 let config = Config::default_config();
597 assert_eq!(config.bind_address(), "0.0.0.0:8080");
598 }
599}