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}