Skip to main content

fraiseql_server/config/
env.rs

1//! Helpers for resolving configuration values from environment variables.
2//!
3//! Provides [`resolve_env_value`] which transparently dereferences values
4//! that start with `$` as environment variable names, and parse utilities
5//! for human-friendly size strings (`"10MB"`) and duration strings (`"30s"`).
6
7use std::{env, time::Duration};
8
9/// Resolve a value that may be an environment variable reference
10///
11/// # Errors
12///
13/// Returns `EnvError::MissingVar` if the referenced environment variable is not set.
14/// Returns `EnvError::MissingVarWithMessage` if the variable uses the `:?` syntax and is not set.
15pub fn resolve_env_value(value: &str) -> Result<String, EnvError> {
16    if value.starts_with("${") && value.ends_with('}') {
17        let var_name = &value[2..value.len() - 1];
18
19        // Support default values: ${VAR:-default}
20        if let Some((name, default)) = var_name.split_once(":-") {
21            return env::var(name).or_else(|_| Ok(default.to_string()));
22        }
23
24        // Support required with message: ${VAR:?message}
25        if let Some((name, message)) = var_name.split_once(":?") {
26            return env::var(name).map_err(|_| EnvError::MissingVarWithMessage {
27                name:    name.to_string(),
28                message: message.to_string(),
29            });
30        }
31
32        env::var(var_name).map_err(|_| EnvError::MissingVar {
33            name: var_name.to_string(),
34        })
35    } else {
36        Ok(value.to_string())
37    }
38}
39
40/// Get value from environment variable name stored in config
41///
42/// # Errors
43///
44/// Returns `EnvError::MissingVar` if the named environment variable is not set.
45pub fn get_env_value(env_var_name: &str) -> Result<String, EnvError> {
46    env::var(env_var_name).map_err(|_| EnvError::MissingVar {
47        name: env_var_name.to_string(),
48    })
49}
50
51/// Parse size strings like "10MB", "1GB"
52///
53/// # Errors
54///
55/// Returns `ParseError::InvalidSize` if the string is not a valid size or the number overflows.
56pub fn parse_size(s: &str) -> Result<usize, ParseError> {
57    let s = s.trim();
58    let s_upper = s.to_uppercase();
59
60    let (num_str, multiplier) = if s_upper.ends_with("GB") {
61        (&s[..s.len() - 2], 1024 * 1024 * 1024)
62    } else if s_upper.ends_with("MB") {
63        (&s[..s.len() - 2], 1024 * 1024)
64    } else if s_upper.ends_with("KB") {
65        (&s[..s.len() - 2], 1024)
66    } else if s_upper.ends_with('B') {
67        (&s[..s.len() - 1], 1)
68    } else {
69        // Assume bytes if no unit
70        (s, 1)
71    };
72
73    let num: usize = num_str.trim().parse().map_err(|_| ParseError::InvalidSize {
74        value:  s.to_string(),
75        reason: "Invalid number".to_string(),
76    })?;
77
78    num.checked_mul(multiplier).ok_or_else(|| ParseError::InvalidSize {
79        value:  s.to_string(),
80        reason: "Value too large".to_string(),
81    })
82}
83
84/// Parse duration strings like "30s", "5m", "1h"
85///
86/// # Errors
87///
88/// Returns `ParseError::InvalidDuration` if the string is missing a unit suffix or the number is
89/// invalid.
90pub fn parse_duration(s: &str) -> Result<Duration, ParseError> {
91    let s = s.trim().to_lowercase();
92
93    let (num_str, multiplier_ms) = if s.ends_with("ms") {
94        (&s[..s.len() - 2], 1u64)
95    } else if s.ends_with('s') {
96        (&s[..s.len() - 1], 1000)
97    } else if s.ends_with('m') {
98        (&s[..s.len() - 1], 60 * 1000)
99    } else if s.ends_with('h') {
100        (&s[..s.len() - 1], 60 * 60 * 1000)
101    } else if s.ends_with('d') {
102        (&s[..s.len() - 1], 24 * 60 * 60 * 1000)
103    } else {
104        return Err(ParseError::InvalidDuration {
105            value:  s,
106            reason: "Missing unit (ms, s, m, h, d)".to_string(),
107        });
108    };
109
110    let num: u64 = num_str.trim().parse().map_err(|_| ParseError::InvalidDuration {
111        value:  s.clone(),
112        reason: "Invalid number".to_string(),
113    })?;
114
115    Ok(Duration::from_millis(num * multiplier_ms))
116}
117
118/// Errors produced when a required environment variable is absent.
119#[derive(Debug, thiserror::Error)]
120#[non_exhaustive]
121pub enum EnvError {
122    /// A required environment variable was not set.
123    #[error("Missing environment variable: {name}")]
124    MissingVar {
125        /// Name of the missing variable.
126        name: String,
127    },
128
129    /// A required environment variable was not set; carries an extra explanation.
130    #[error("Missing environment variable {name}: {message}")]
131    MissingVarWithMessage {
132        /// Name of the missing variable.
133        name:    String,
134        /// Human-readable explanation of why the variable is required.
135        message: String,
136    },
137}
138
139/// Errors produced when a configuration string cannot be parsed.
140#[derive(Debug, thiserror::Error)]
141#[non_exhaustive]
142pub enum ParseError {
143    /// A size string (e.g. `"10MB"`) could not be interpreted.
144    #[error("Invalid size value '{value}': {reason}")]
145    InvalidSize {
146        /// The raw string that failed parsing.
147        value:  String,
148        /// Explanation of why parsing failed.
149        reason: String,
150    },
151
152    /// A duration string (e.g. `"30s"`) could not be interpreted.
153    #[error("Invalid duration value '{value}': {reason}")]
154    InvalidDuration {
155        /// The raw string that failed parsing.
156        value:  String,
157        /// Explanation of why parsing failed.
158        reason: String,
159    },
160}