reflex/config/
mod.rs

1//! Environment-backed configuration.
2//!
3//! Most settings have defaults. Override with `REFLEX_*` environment variables.
4
5pub mod error;
6
7#[cfg(test)]
8mod tests;
9
10pub use error::ConfigError;
11
12use std::env;
13use std::net::IpAddr;
14use std::path::PathBuf;
15
16/// Server configuration loaded from environment variables.
17///
18/// Use [`Config::from_env`] to read `REFLEX_*` overrides on top of defaults.
19#[derive(Debug, Clone)]
20pub struct Config {
21    /// HTTP server port. Default: `8080`.
22    pub port: u16,
23
24    /// IP address to bind to. Default: `127.0.0.1`.
25    pub bind_addr: IpAddr,
26
27    /// Directory for persistent cache storage. Default: `./.data`.
28    pub storage_path: PathBuf,
29
30    /// Path to the embedding model file (Sinter / GGUF).
31    pub model_path: Option<PathBuf>,
32
33    /// Path to the reranker model directory (BERT + tokenizer).
34    pub reranker_path: Option<PathBuf>,
35
36    /// Qdrant endpoint URL. Default: `http://localhost:6334`.
37    pub qdrant_url: String,
38
39    /// Max entries in the in-memory L1 cache. Default: `10_000`.
40    pub l1_capacity: u64,
41}
42
43/// Default Qdrant URL used when `REFLEX_QDRANT_URL` is not set.
44pub const DEFAULT_QDRANT_URL: &str = "http://localhost:6334";
45
46impl Default for Config {
47    fn default() -> Self {
48        Self {
49            port: 8080,
50            bind_addr: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
51            storage_path: PathBuf::from("./.data"),
52            model_path: None,
53            reranker_path: None,
54            qdrant_url: DEFAULT_QDRANT_URL.to_string(),
55            l1_capacity: 10_000,
56        }
57    }
58}
59
60impl Config {
61    const ENV_PORT: &'static str = "REFLEX_PORT";
62    const ENV_BIND_ADDR: &'static str = "REFLEX_BIND_ADDR";
63    const ENV_STORAGE_PATH: &'static str = "REFLEX_STORAGE_PATH";
64    const ENV_MODEL_PATH: &'static str = "REFLEX_MODEL_PATH";
65    const ENV_RERANKER_PATH: &'static str = "REFLEX_RERANKER_PATH";
66    const ENV_QDRANT_URL: &'static str = "REFLEX_QDRANT_URL";
67    const ENV_L1_CAPACITY: &'static str = "REFLEX_L1_CAPACITY";
68
69    /// Loads configuration from environment variables (falling back to defaults).
70    pub fn from_env() -> Result<Self, ConfigError> {
71        let defaults = Self::default();
72
73        let port = Self::parse_port_from_env(defaults.port)?;
74        let bind_addr = Self::parse_bind_addr_from_env(defaults.bind_addr)?;
75        let storage_path = Self::parse_path_from_env(Self::ENV_STORAGE_PATH, defaults.storage_path);
76        let model_path = Self::parse_optional_path_from_env(Self::ENV_MODEL_PATH);
77        let reranker_path = Self::parse_optional_path_from_env(Self::ENV_RERANKER_PATH);
78        let qdrant_url = Self::parse_string_from_env(Self::ENV_QDRANT_URL, defaults.qdrant_url);
79        let l1_capacity = Self::parse_u64_from_env(Self::ENV_L1_CAPACITY, defaults.l1_capacity);
80
81        Ok(Self {
82            port,
83            bind_addr,
84            storage_path,
85            model_path,
86            reranker_path,
87            qdrant_url,
88            l1_capacity,
89        })
90    }
91
92    /// Validates paths and basic invariants (does not create directories).
93    pub fn validate(&self) -> Result<(), ConfigError> {
94        if self.storage_path.exists() && !self.storage_path.is_dir() {
95            return Err(ConfigError::NotADirectory {
96                path: self.storage_path.clone(),
97            });
98        }
99
100        if let Some(ref path) = self.model_path {
101            if !path.exists() {
102                return Err(ConfigError::PathNotFound { path: path.clone() });
103            }
104            if !path.is_file() {
105                return Err(ConfigError::NotAFile { path: path.clone() });
106            }
107        }
108
109        if let Some(ref path) = self.reranker_path {
110            if !path.exists() {
111                return Err(ConfigError::PathNotFound { path: path.clone() });
112            }
113            if !path.is_dir() {
114                return Err(ConfigError::NotADirectory { path: path.clone() });
115            }
116        }
117
118        Ok(())
119    }
120
121    /// Returns `"{bind_addr}:{port}"` (useful for logging/binding).
122    pub fn socket_addr(&self) -> String {
123        format!("{}:{}", self.bind_addr, self.port)
124    }
125
126    fn parse_port_from_env(default: u16) -> Result<u16, ConfigError> {
127        match env::var(Self::ENV_PORT) {
128            Ok(value) => {
129                let port: u16 = value.parse().map_err(|e| ConfigError::PortParseError {
130                    value: value.clone(),
131                    source: e,
132                })?;
133
134                if port == 0 {
135                    return Err(ConfigError::InvalidPort { value });
136                }
137
138                Ok(port)
139            }
140            Err(_) => Ok(default),
141        }
142    }
143
144    fn parse_bind_addr_from_env(default: IpAddr) -> Result<IpAddr, ConfigError> {
145        match env::var(Self::ENV_BIND_ADDR) {
146            Ok(value) => value
147                .parse()
148                .map_err(|e| ConfigError::InvalidBindAddr { value, source: e }),
149            Err(_) => Ok(default),
150        }
151    }
152
153    fn parse_path_from_env(var_name: &str, default: PathBuf) -> PathBuf {
154        env::var(var_name).map(PathBuf::from).unwrap_or(default)
155    }
156
157    fn parse_optional_path_from_env(var_name: &str) -> Option<PathBuf> {
158        env::var(var_name)
159            .ok()
160            .map(|v| v.trim().to_string())
161            .filter(|v| !v.is_empty())
162            .map(PathBuf::from)
163    }
164
165    fn parse_string_from_env(var_name: &str, default: String) -> String {
166        env::var(var_name).unwrap_or(default)
167    }
168
169    fn parse_u64_from_env(var_name: &str, default: u64) -> u64 {
170        env::var(var_name)
171            .ok()
172            .and_then(|v| v.parse().ok())
173            .unwrap_or(default)
174    }
175}