gearbox_rs_core/
config.rs1use config::{Config as ConfigLoader, Environment, File};
2use dashmap::DashMap;
3use serde::Deserialize;
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6use std::any::{Any, TypeId, type_name};
7use std::sync::Arc;
8
9use crate::error::Error;
10
11pub trait CogConfig: DeserializeOwned + Default + Send + Sync + 'static {
13 const CONFIG_KEY: &'static str;
14
15 fn validate(&self) -> Result<(), String> {
19 Ok(())
20 }
21}
22
23pub struct ConfigMeta {
25 pub key: &'static str,
26 pub type_id_fn: fn() -> TypeId,
27 pub type_name: &'static str,
28 pub deserialize_fn: fn(&Value) -> Result<Box<dyn Any + Send + Sync>, serde_json::Error>,
29 pub validate_fn: fn(&dyn Any) -> Result<(), String>,
30}
31
32inventory::collect!(ConfigMeta);
33
34#[derive(Debug, Clone, Deserialize)]
35#[serde(default)]
36pub struct GearboxAppConfig {
37 pub http_port: u16,
38 pub log_level: String,
39 pub app_name: String,
40}
41
42impl Default for GearboxAppConfig {
43 fn default() -> Self {
44 Self {
45 http_port: 8080,
46 log_level: "info".to_string(),
47 app_name: "gearbox-app".to_string(),
48 }
49 }
50}
51
52pub struct Config {
53 raw: Value,
54 configs: DashMap<TypeId, Arc<dyn Any + Send + Sync>>,
55 app: GearboxAppConfig,
56 defaulted_keys: Vec<(&'static str, &'static str)>,
57}
58
59impl Config {
60 pub fn load() -> Result<Self, Error> {
61 let config_path =
62 std::env::var("CONFIG_LOCATION").unwrap_or_else(|_| "./config.toml".to_string());
63
64 let mut builder = ConfigLoader::builder();
65
66 if std::path::Path::new(&config_path).exists() {
67 builder = builder.add_source(File::with_name(&config_path));
68 }
69
70 builder = builder.add_source(
71 Environment::with_prefix("GEARBOX")
72 .separator("__")
73 .try_parsing(true),
74 );
75
76 let raw: Value = builder.build()?.try_deserialize()?;
77
78 for (key, value) in std::env::vars() {
80 if key.starts_with("GEARBOX") {
81 tracing::debug!("env override: {}={}", key, value);
82 }
83 }
84 tracing::debug!("raw config: {:?}", raw);
85
86 Self::from_value(raw)
87 }
88
89 pub fn from_value(raw: Value) -> Result<Self, Error> {
92 let app: GearboxAppConfig = raw
93 .get("gearbox_app")
94 .map(|v| serde_json::from_value(v.clone()))
95 .transpose()
96 .map_err(|e| Error::ConfigDeserialize("GearboxAppConfig".into(), Box::new(e)))?
97 .unwrap_or_else(|| {
98 tracing::debug!("config section 'gearbox_app' not found, using defaults");
99 GearboxAppConfig::default()
100 });
101
102 let configs = DashMap::new();
103 let mut defaulted_keys = Vec::new();
104
105 for meta in inventory::iter::<ConfigMeta> {
106 let section = match raw.get(meta.key).cloned() {
107 Some(v) => v,
108 None => {
109 defaulted_keys.push((meta.key, meta.type_name));
110 tracing::debug!(
111 "config section '{}' ({}) not found, using defaults",
112 meta.key,
113 meta.type_name,
114 );
115 Value::Object(Default::default())
116 }
117 };
118 tracing::debug!("loading config section {:?}", section);
119 let config = (meta.deserialize_fn)(§ion)
120 .map_err(|e| Error::ConfigDeserialize(meta.type_name.to_string(), Box::new(e)))?;
121 (meta.validate_fn)(config.as_ref())
122 .map_err(|msg| Error::ConfigValidation(meta.type_name.to_string(), msg))?;
123 configs.insert((meta.type_id_fn)(), Arc::from(config));
124 }
125
126 if let Some(obj) = raw.as_object() {
128 let registered_keys: std::collections::HashSet<&str> =
129 inventory::iter::<ConfigMeta>.into_iter().map(|m| m.key).collect();
130 for key in obj.keys() {
131 if key != "gearbox_app" && !registered_keys.contains(key.as_str()) {
132 tracing::warn!(
133 "config section '{}' found in file but no #[cog_config(\"{}\")] is registered — possible typo?",
134 key, key,
135 );
136 }
137 }
138 }
139
140 Ok(Self {
141 raw,
142 configs,
143 app,
144 defaulted_keys,
145 })
146 }
147
148 pub fn insert<C: CogConfig + Clone + 'static>(&self, config: C) {
150 self.configs.insert(TypeId::of::<C>(), Arc::new(config));
151 }
152
153 pub(crate) fn set_app(&mut self, app: GearboxAppConfig) {
155 self.app = app;
156 }
157
158 pub fn get<C: CogConfig + Clone>(&self) -> Result<C, Error> {
159 self.configs
160 .get(&TypeId::of::<C>())
161 .and_then(|v| v.value().downcast_ref::<C>().cloned())
162 .ok_or_else(|| Error::ConfigNotFound(type_name::<C>().to_string()))
163 }
164
165 pub fn app(&self) -> &GearboxAppConfig {
166 &self.app
167 }
168
169 pub fn raw(&self) -> &Value {
170 &self.raw
171 }
172
173 pub fn defaulted_configs(&self) -> &[(&str, &str)] {
177 &self.defaulted_keys
178 }
179}
180
181impl Default for Config {
182 fn default() -> Self {
183 Self {
184 raw: Value::Object(Default::default()),
185 configs: DashMap::new(),
186 app: GearboxAppConfig::default(),
187 defaulted_keys: Vec::new(),
188 }
189 }
190}
191
192#[doc(hidden)]
193pub fn deserialize_config<T: DeserializeOwned + Send + Sync + 'static>(
194 value: &Value,
195) -> Result<Box<dyn Any + Send + Sync>, serde_json::Error> {
196 Ok(Box::new(serde_json::from_value::<T>(value.clone())?))
197}
198
199#[doc(hidden)]
200pub fn validate_config<T: CogConfig + 'static>(value: &dyn Any) -> Result<(), String> {
201 value
202 .downcast_ref::<T>()
203 .ok_or_else(|| {
204 format!(
205 "internal error: config type mismatch for {}",
206 type_name::<T>()
207 )
208 })
209 .and_then(|c| c.validate())
210}