Skip to main content

oxigdal_server/
config.rs

1//! Server configuration management
2//!
3//! This module handles configuration from multiple sources:
4//! - TOML configuration files
5//! - Environment variables
6//! - Command-line arguments
7//!
8//! # Example Configuration
9//!
10//! ```toml
11//! [server]
12//! host = "0.0.0.0"
13//! port = 8080
14//! workers = 4
15//!
16//! [cache]
17//! memory_size_mb = 256
18//! disk_cache = "/tmp/oxigdal-cache"
19//! ttl_seconds = 3600
20//!
21//! [[layers]]
22//! name = "landsat"
23//! path = "/data/landsat.tif"
24//! formats = ["png", "jpeg", "webp"]
25//! tile_size = 256
26//! ```
27
28use 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/// Configuration errors
36#[derive(Debug, Error)]
37pub enum ConfigError {
38    /// Invalid configuration value
39    #[error("Invalid configuration: {0}")]
40    Invalid(String),
41
42    /// Configuration file I/O error
43    #[error("Failed to read config file: {0}")]
44    Io(#[from] std::io::Error),
45
46    /// TOML parsing error
47    #[error("Failed to parse TOML: {0}")]
48    TomlParse(#[from] toml::de::Error),
49
50    /// Missing required field
51    #[error("Missing required field: {0}")]
52    MissingField(String),
53
54    /// Layer not found
55    #[error("Layer not found: {0}")]
56    LayerNotFound(String),
57}
58
59/// Result type for configuration operations
60pub type ConfigResult<T> = Result<T, ConfigError>;
61
62/// Server configuration
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ServerConfig {
65    /// Host address to bind to
66    #[serde(default = "default_host")]
67    pub host: IpAddr,
68
69    /// Port to bind to
70    #[serde(default = "default_port")]
71    pub port: u16,
72
73    /// Number of worker threads (0 = number of CPUs)
74    #[serde(default = "default_workers")]
75    pub workers: usize,
76
77    /// Maximum request size in bytes
78    #[serde(default = "default_max_request_size")]
79    pub max_request_size: usize,
80
81    /// Request timeout in seconds
82    #[serde(default = "default_timeout")]
83    pub timeout_seconds: u64,
84
85    /// Enable CORS
86    #[serde(default = "default_cors")]
87    pub enable_cors: bool,
88
89    /// Allowed CORS origins (empty = all)
90    #[serde(default)]
91    pub cors_origins: Vec<String>,
92}
93
94/// Cache configuration
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct CacheConfig {
97    /// In-memory cache size in megabytes
98    #[serde(default = "default_memory_cache_mb")]
99    pub memory_size_mb: usize,
100
101    /// Optional disk cache directory
102    #[serde(default)]
103    pub disk_cache: Option<PathBuf>,
104
105    /// Time-to-live for cached tiles in seconds
106    #[serde(default = "default_ttl_seconds")]
107    pub ttl_seconds: u64,
108
109    /// Enable cache statistics
110    #[serde(default = "default_enable_stats")]
111    pub enable_stats: bool,
112
113    /// Cache compression (gzip tiles in memory)
114    #[serde(default = "default_compression")]
115    pub compression: bool,
116}
117
118/// Layer configuration
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct LayerConfig {
121    /// Layer name (used in URLs)
122    pub name: String,
123
124    /// Display title
125    #[serde(default)]
126    pub title: Option<String>,
127
128    /// Layer description
129    #[serde(default)]
130    pub abstract_: Option<String>,
131
132    /// Path to dataset file
133    pub path: PathBuf,
134
135    /// Supported output formats
136    #[serde(default = "default_formats")]
137    pub formats: Vec<ImageFormat>,
138
139    /// Tile size in pixels
140    #[serde(default = "default_tile_size")]
141    pub tile_size: u32,
142
143    /// Minimum zoom level
144    #[serde(default)]
145    pub min_zoom: u8,
146
147    /// Maximum zoom level
148    #[serde(default = "default_max_zoom")]
149    pub max_zoom: u8,
150
151    /// Supported tile matrix sets
152    #[serde(default = "default_tile_matrix_sets")]
153    pub tile_matrix_sets: Vec<String>,
154
155    /// Optional style configuration
156    #[serde(default)]
157    pub style: Option<StyleConfig>,
158
159    /// Layer-specific metadata
160    #[serde(default)]
161    pub metadata: HashMap<String, String>,
162
163    /// Enable this layer
164    #[serde(default = "default_enabled")]
165    pub enabled: bool,
166}
167
168/// Style configuration for rendering
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct StyleConfig {
171    /// Style name
172    pub name: String,
173
174    /// Colormap name (e.g., "viridis", "terrain")
175    #[serde(default)]
176    pub colormap: Option<String>,
177
178    /// Value range for colormap
179    #[serde(default)]
180    pub value_range: Option<(f64, f64)>,
181
182    /// Alpha/transparency value (0.0-1.0)
183    #[serde(default = "default_alpha")]
184    pub alpha: f32,
185
186    /// Gamma correction
187    #[serde(default = "default_gamma")]
188    pub gamma: f32,
189
190    /// Brightness adjustment (-1.0 to 1.0)
191    #[serde(default)]
192    pub brightness: f32,
193
194    /// Contrast adjustment (0.0 to 2.0)
195    #[serde(default = "default_contrast")]
196    pub contrast: f32,
197}
198
199/// Image format enumeration
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
201#[serde(rename_all = "lowercase")]
202pub enum ImageFormat {
203    /// PNG format
204    Png,
205    /// JPEG format
206    Jpeg,
207    /// WebP format
208    Webp,
209    /// GeoTIFF format
210    Geotiff,
211}
212
213impl ImageFormat {
214    /// Get MIME type for this format
215    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    /// Get file extension for this format
225    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/// Error type for parsing image format
236#[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/// Complete server configuration
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct Config {
264    /// Server settings
265    #[serde(default)]
266    pub server: ServerConfig,
267
268    /// Cache settings
269    #[serde(default)]
270    pub cache: CacheConfig,
271
272    /// Layer definitions
273    #[serde(default)]
274    pub layers: Vec<LayerConfig>,
275
276    /// Global metadata
277    #[serde(default)]
278    pub metadata: MetadataConfig,
279}
280
281/// Service metadata configuration
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct MetadataConfig {
284    /// Service title
285    #[serde(default = "default_service_title")]
286    pub title: String,
287
288    /// Service abstract/description
289    #[serde(default = "default_service_abstract")]
290    pub abstract_: String,
291
292    /// Contact information
293    #[serde(default)]
294    pub contact: Option<ContactInfo>,
295
296    /// Keywords
297    #[serde(default)]
298    pub keywords: Vec<String>,
299
300    /// Online resource URL
301    #[serde(default)]
302    pub online_resource: Option<String>,
303}
304
305/// Contact information
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct ContactInfo {
308    /// Organization name
309    pub organization: String,
310
311    /// Contact person
312    #[serde(default)]
313    pub person: Option<String>,
314
315    /// Email address
316    #[serde(default)]
317    pub email: Option<String>,
318
319    /// Phone number
320    #[serde(default)]
321    pub phone: Option<String>,
322}
323
324// Default value functions
325fn 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 // 10 MB
342}
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    /// Load configuration from TOML file
448    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    /// Parse configuration from TOML string
454    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    /// Create a default configuration
461    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    /// Validate the configuration
471    pub fn validate(&self) -> ConfigResult<()> {
472        // Check for duplicate layer names
473        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            // Validate layer path exists
483            if !layer.path.exists() {
484                return Err(ConfigError::Invalid(format!(
485                    "Layer path does not exist: {}",
486                    layer.path.display()
487                )));
488            }
489
490            // Validate tile size is power of 2
491            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            // Validate zoom levels
499            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        // Validate cache settings
508        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    /// Get a layer by name
518    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    /// Get all enabled layers
526    pub fn enabled_layers(&self) -> impl Iterator<Item = &LayerConfig> {
527        self.layers.iter().filter(|l| l.enabled)
528    }
529
530    /// Get server bind address
531    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}