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}