Skip to main content

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 env;
30pub mod error;
31mod file;
32mod proxy;
33#[cfg(feature = "vault-config")]
34mod vault;
35
36#[cfg(test)]
37#[path = "../../tests/unit/config/tests.rs"]
38mod tests;
39
40pub use env::EnvConfigProvider;
41pub use error::ConfigError;
42pub use file::FileConfigProvider;
43#[cfg(feature = "vault-config")]
44pub use vault::VaultConfigProvider;
45
46use serde::de::DeserializeOwned;
47use serde_json::Value;
48use std::fmt::Debug;
49use std::sync::Arc;
50
51/// Core configuration provider trait that all configuration sources must implement.
52/// This trait is object-safe since it doesn't contain generic methods.
53pub trait ConfigProvider: Debug + Send + Sync {
54    /// Check if the configuration provider has a value for the given key.
55    fn has(&self, key: &str) -> bool;
56
57    /// Get the name of the configuration provider for debugging purposes.
58    fn provider_name(&self) -> &str;
59
60    /// Get a raw configuration value by key.
61    /// Returns a JSON Value that can be later deserialized into specific types.
62    fn get_raw(&self, key: &str) -> Result<Option<Value>, ConfigError>;
63}
64
65/// Extension trait for ConfigProvider that provides methods for typed access.
66/// This trait is not object-safe because it has generic methods.
67pub trait ConfigProviderExt: ConfigProvider {
68    /// Get a configuration value by key and deserialize it to the specified type.
69    fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, ConfigError> {
70        match self.get_raw(key)? {
71            Some(value) => serde_json::from_value(value).map(Some).map_err(|e| {
72                ConfigError::ParseError(format!("failed to deserialize '{key}': {e}"))
73            }),
74            None => Ok(None),
75        }
76    }
77}
78
79// Implement ConfigProviderExt for any type that implements ConfigProvider
80impl<T: ConfigProvider> ConfigProviderExt for T {}
81
82/// Builder for the configuration system.
83#[derive(Debug, Default)]
84pub struct ConfigBuilder {
85    providers: Vec<Arc<dyn ConfigProvider>>,
86}
87
88impl ConfigBuilder {
89    /// Create a new configuration builder.
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Add a configuration provider.
95    pub fn with_provider<P: ConfigProvider + 'static>(mut self, provider: P) -> Self {
96        self.providers.push(Arc::new(provider));
97        self
98    }
99
100    /// Build the configuration.
101    pub fn build(self) -> Config {
102        Config {
103            providers: self.providers,
104        }
105    }
106}
107
108/// Main configuration struct that holds all providers and handles retrieving values.
109#[derive(Debug, Clone)]
110pub struct Config {
111    providers: Vec<Arc<dyn ConfigProvider>>,
112}
113
114impl Config {
115    /// Create a new configuration builder.
116    pub fn builder() -> ConfigBuilder {
117        ConfigBuilder::new()
118    }
119
120    /// Get a raw configuration value.
121    fn get_raw(&self, key: &str) -> Result<Option<Value>, ConfigError> {
122        // Iterate through providers in reverse order to respect priority
123        // Later providers (higher index) should override earlier ones
124        for provider in self.providers.iter().rev() {
125            if provider.has(key) {
126                return provider.get_raw(key);
127            }
128        }
129        Ok(None)
130    }
131
132    /// Get a configuration value by key, checking all providers in the order they were added.
133    /// Returns the first value found.
134    pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, ConfigError> {
135        match self.get_raw(key)? {
136            Some(value) => serde_json::from_value(value).map(Some).map_err(|e| {
137                ConfigError::ParseError(format!("failed to deserialize '{key}': {e}"))
138            }),
139            None => Ok(None),
140        }
141    }
142
143    /// Get a configuration value by key with a default fallback value.
144    pub fn get_or_default<T: DeserializeOwned>(
145        &self,
146        key: &str,
147        default: T,
148    ) -> Result<T, ConfigError> {
149        match self.get(key)? {
150            Some(value) => Ok(value),
151            None => Ok(default),
152        }
153    }
154
155    /// Create a default configuration using the file-based provider.
156    pub fn default_file(file_path: &str) -> Result<Self, ConfigError> {
157        let provider = FileConfigProvider::new(file_path)?;
158        Ok(Self::builder().with_provider(provider).build())
159    }
160}