foxy/config/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Foxy configuration subsystem
6//!
7//! The subsystem is deliberately **pluggable**.  A running proxy is created
8//! from an ordered list of [`ConfigProvider`]s; later providers override
9//! earlier ones.  Typical stacking order looks like this:
10//!
11//! 1. `FileConfigProvider` – `foxy.{toml,json,yaml}`
12//! 2. `EnvConfigProvider`  – `FOXY__ROUTES__0__TARGET=https://…`
13//! 3. *your* provider implementing [`ConfigProvider`]
14//!
15//! Calling [`Config::get`] is therefore *deterministic*: the first provider
16//! in the chain that yields a key wins.
17//!
18//! The following document describes every *first-class* configuration key.
19//!
20//! | key | type | default | description |
21//! |-----|------|---------|-------------|
22//! | `server.listen`      | `"[::]:8080"` | – | Socket-address to bind       |
23//! | `server.body_limit`  | `5mb`         | – | Maximum inbound body size     |
24//! | `server.header_limit`| `256kb`       | – | Maximum combined header bytes |
25//! | `routes`             | *array*       | – | List of routing rules         |
26//!
27//! See [`README.md`](../../README.md#configuration) for a more narrative guide.
28
29mod file;
30mod env;
31pub mod error;
32
33#[cfg(test)]
34mod tests;
35
36pub use error::ConfigError;
37pub use file::FileConfigProvider;
38pub use env::EnvConfigProvider;
39
40use std::fmt::Debug;
41use std::sync::Arc;
42use serde::de::DeserializeOwned;
43use serde_json::Value;
44
45/// Core configuration provider trait that all configuration sources must implement.
46/// This trait is object-safe since it doesn't contain generic methods.
47pub trait ConfigProvider: Debug + Send + Sync {
48    /// Check if the configuration provider has a value for the given key.
49    fn has(&self, key: &str) -> bool;
50
51    /// Get the name of the configuration provider for debugging purposes.
52    fn provider_name(&self) -> &str;
53
54    /// Get a raw configuration value by key.
55    /// Returns a JSON Value that can be later deserialized into specific types.
56    fn get_raw(&self, key: &str) -> Result<Option<Value>, ConfigError>;
57}
58
59/// Extension trait for ConfigProvider that provides methods for typed access.
60/// This trait is not object-safe because it has generic methods.
61pub trait ConfigProviderExt: ConfigProvider {
62    /// Get a configuration value by key and deserialize it to the specified type.
63    fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, ConfigError> {
64        match self.get_raw(key)? {
65            Some(value) => {
66                serde_json::from_value(value)
67                    .map(Some)
68                    .map_err(|e| ConfigError::ParseError(format!("failed to deserialize '{}': {}", key, e)))
69            },
70            None => Ok(None),
71        }
72    }
73}
74
75// Implement ConfigProviderExt for any type that implements ConfigProvider
76impl<T: ConfigProvider> ConfigProviderExt for T {}
77
78/// Builder for the configuration system.
79#[derive(Debug, Default)]
80pub struct ConfigBuilder {
81    providers: Vec<Arc<dyn ConfigProvider>>,
82}
83
84impl ConfigBuilder {
85    /// Create a new configuration builder.
86    pub fn new() -> Self {
87        Self::default()
88    }
89
90    /// Add a configuration provider.
91    pub fn with_provider<P: ConfigProvider + 'static>(mut self, provider: P) -> Self {
92        self.providers.push(Arc::new(provider));
93        self
94    }
95
96    /// Build the configuration.
97    pub fn build(self) -> Config {
98        Config {
99            providers: self.providers,
100        }
101    }
102}
103
104/// Main configuration struct that holds all providers and handles retrieving values.
105#[derive(Debug, Clone)]
106pub struct Config {
107    providers: Vec<Arc<dyn ConfigProvider>>,
108}
109
110impl Config {
111    /// Create a new configuration builder.
112    pub fn builder() -> ConfigBuilder {
113        ConfigBuilder::new()
114    }
115
116    /// Get a raw configuration value.
117    fn get_raw(&self, key: &str) -> Result<Option<Value>, ConfigError> {
118        // Iterate through providers in reverse order to respect priority
119        // Later providers (higher index) should override earlier ones
120        for provider in self.providers.iter().rev() {
121            if provider.has(key) {
122                return provider.get_raw(key);
123            }
124        }
125        Ok(None)
126    }
127
128    /// Get a configuration value by key, checking all providers in the order they were added.
129    /// Returns the first value found.
130    pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, ConfigError> {
131        match self.get_raw(key)? {
132            Some(value) => {
133                serde_json::from_value(value)
134                    .map(Some)
135                    .map_err(|e| ConfigError::ParseError(format!("failed to deserialize '{}': {}", key, e)))
136            },
137            None => Ok(None),
138        }
139    }
140
141    /// Get a configuration value by key with a default fallback value.
142    pub fn get_or_default<T: DeserializeOwned>(&self, key: &str, default: T) -> Result<T, ConfigError> {
143        match self.get(key)? {
144            Some(value) => Ok(value),
145            None => Ok(default),
146        }
147    }
148
149    /// Create a default configuration using the file-based provider.
150    pub fn default_file(file_path: &str) -> Result<Self, ConfigError> {
151        let provider = FileConfigProvider::new(file_path)?;
152        Ok(Self::builder().with_provider(provider).build())
153    }
154}