random_image_server/
config.rs

1use std::{
2    net::{Ipv4Addr, SocketAddr},
3    path::PathBuf,
4    str::FromStr,
5};
6
7use anyhow::{Result, anyhow};
8use log::LevelFilter;
9use serde::Deserialize;
10use url::Url;
11
12const DEFAULT_PORT: u16 = 3000;
13const DEFAULT_HOST: url::Host = url::Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1));
14const DEFAULT_LOG_LEVEL: LevelFilter = LevelFilter::Info;
15
16/// Configuration structure for the server
17#[derive(Debug, Default, Deserialize, Clone, PartialEq, Eq)]
18pub struct Config {
19    pub server: ServerConfig,
20    #[serde(default)]
21    pub cache: CacheConfig,
22}
23
24#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
25pub struct ServerConfig {
26    #[serde(default = "default_port")]
27    pub port: u16,
28    #[serde(deserialize_with = "deserialize_host", default = "default_host")]
29    pub host: url::Host,
30    #[serde(
31        deserialize_with = "deserialize_log_level",
32        default = "default_log_level"
33    )]
34    pub log_level: LevelFilter,
35    #[serde(deserialize_with = "deserialize_sources")]
36    pub sources: Vec<ImageSource>,
37}
38
39const fn default_port() -> u16 {
40    DEFAULT_PORT
41}
42const fn default_host() -> url::Host {
43    DEFAULT_HOST
44}
45const fn default_log_level() -> LevelFilter {
46    DEFAULT_LOG_LEVEL
47}
48
49fn deserialize_host<'de, D>(deserializer: D) -> Result<url::Host, D::Error>
50where
51    D: serde::Deserializer<'de>,
52{
53    let s: String = Deserialize::deserialize(deserializer)?;
54    url::Host::parse(&s).map_err(serde::de::Error::custom)
55}
56
57fn deserialize_log_level<'de, D>(deserializer: D) -> Result<LevelFilter, D::Error>
58where
59    D: serde::Deserializer<'de>,
60{
61    let level: String = Deserialize::deserialize(deserializer)?;
62    LevelFilter::from_str(&level).map_err(serde::de::Error::custom)
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum ImageSource {
67    Url(Url),
68    Path(PathBuf),
69}
70
71#[derive(Debug, Default, Deserialize, Clone, Copy, PartialEq, Eq)]
72pub struct CacheConfig {
73    pub backend: CacheBackendType,
74}
75
76#[derive(Debug, Default, Deserialize, Clone, Copy, PartialEq, Eq)]
77#[serde(rename_all = "snake_case")]
78pub enum CacheBackendType {
79    #[default]
80    InMemory,
81    FileSystem,
82}
83
84impl FromStr for ImageSource {
85    type Err = anyhow::Error;
86
87    fn from_str(s: &str) -> Result<Self, Self::Err> {
88        if let Ok(url) = Url::parse(s) {
89            Ok(Self::Url(url))
90        } else if PathBuf::from(s).exists() {
91            Ok(Self::Path(PathBuf::from(s).canonicalize()?))
92        } else {
93            Err(anyhow!(
94                "Image source doesn't exist or couldn't be parsed as a URL: {s}"
95            ))
96        }
97    }
98}
99
100impl FromStr for CacheBackendType {
101    type Err = String;
102
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        match s.to_lowercase().as_str() {
105            "in_memory" => Ok(Self::InMemory),
106            "file_system" => Ok(Self::FileSystem),
107            _ => Err(format!("Unknown cache backend type: {s}")),
108        }
109    }
110}
111
112fn deserialize_sources<'de, D>(deserializer: D) -> Result<Vec<ImageSource>, D::Error>
113where
114    D: serde::Deserializer<'de>,
115{
116    let sources: Vec<String> = Deserialize::deserialize(deserializer)?;
117    let mut image_sources = Vec::new();
118
119    for source in sources {
120        match ImageSource::from_str(&source) {
121            Ok(image_source) => image_sources.push(image_source),
122            Err(e) => log::warn!("Invalid image source '{source}': {e}"),
123        }
124    }
125
126    if image_sources.is_empty() {
127        return Err(serde::de::Error::custom("No valid image sources found"));
128    }
129
130    Ok(image_sources)
131}
132
133impl Default for ServerConfig {
134    fn default() -> Self {
135        Self {
136            port: DEFAULT_PORT,
137            host: DEFAULT_HOST,
138            log_level: DEFAULT_LOG_LEVEL,
139            sources: vec![],
140        }
141    }
142}
143
144impl Config {
145    /// Load configuration from a TOML file
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if the file cannot be read or parsed.
150    pub fn from_file(path: &str) -> Result<Self> {
151        let content = std::fs::read_to_string(path)?;
152        let config: Self = toml::from_str(&content)?;
153        Ok(config)
154    }
155
156    /// Create a new configuration, with it's values updated from environment variables
157    ///
158    /// This function reads environment variables prefixed with `RANDOM_IMAGE_SERVER_`
159    /// and updates the configuration accordingly. It supports the following variables:
160    /// - `RANDOM_IMAGE_SERVER_PORT`: The port for the server
161    /// - `RANDOM_IMAGE_SERVER_HOST`: The host for the server
162    /// - `RANDOM_IMAGE_SERVER_LOG_LEVEL`: The log level for the server
163    /// - `RANDOM_IMAGE_SERVER_SOURCES`: A comma-separated list of image sources (URLs or paths)
164    /// - `RANDOM_IMAGE_SERVER_CACHE_BACKEND`: The cache backend type, either `in_memory` or `file_system`
165    ///
166    /// # Errors
167    ///
168    /// Returns an error if any environment variable is invalid or cannot be parsed.
169    pub fn with_env(self) -> Result<Self> {
170        self.with_env_backend(&crate::env::StdEnvBackend)
171    }
172
173    /// Create a new configuration, with it's values updated from environment variables.
174    ///
175    /// Same as `with_env`, but allows passing a custom environment backend (e.g., for testing).
176    ///
177    /// # Errors
178    ///
179    /// Returns an error if any environment variable is invalid or cannot be parsed.
180    pub fn with_env_backend(mut self, env: &impl crate::env::EnvBackend) -> Result<Self> {
181        macro_rules! set_from_env {
182            ($field:expr, $var:literal,  $parse_fn:expr) => {
183                if let Ok(value) = env.var(concat!("RANDOM_IMAGE_SERVER_", $var)) {
184                    $field = $parse_fn(&value).map_err(|e| {
185                        anyhow!("Failed to parse environment variable '{}': {}", $var, e)
186                    })?
187                }
188            };
189        }
190
191        set_from_env!(self.server.port, "PORT", u16::from_str);
192        set_from_env!(self.server.host, "HOST", url::Host::parse);
193        set_from_env!(self.server.log_level, "LOG_LEVEL", LevelFilter::from_str);
194        set_from_env!(self.server.sources, "SOURCES", |s: &str| {
195            s.split(',')
196                .map(ImageSource::from_str)
197                .collect::<Result<Vec<_>, _>>()
198                .and_then(|sources| {
199                    if sources.is_empty() {
200                        Err(anyhow!("No valid image sources found"))
201                    } else {
202                        Ok(sources)
203                    }
204                })
205        });
206        set_from_env!(
207            self.cache.backend,
208            "CACHE_BACKEND",
209            CacheBackendType::from_str
210        );
211
212        Ok(self)
213    }
214
215    /// Get the socket address for the server
216    ///
217    /// # Errors
218    ///
219    /// Shouldn't fail unless the host or port is invalid.
220    pub fn socket_addr(&self) -> Result<SocketAddr, std::net::AddrParseError> {
221        format!("{}:{}", self.server.host, self.server.port).parse()
222    }
223}