tuitbot_core/config/
mod.rs1mod defaults;
11mod enrichment;
12mod env_overrides;
13pub mod merge;
14mod types;
15mod types_policy;
16mod validation;
17
18#[cfg(test)]
19mod tests;
20#[cfg(test)]
21mod tests_backend;
22
23pub use enrichment::{EnrichmentStage, ProfileCompleteness};
24pub use merge::{
25 effective_config, merge_overrides, split_patch_by_scope, validate_override_keys,
26 EffectiveConfigResult, ACCOUNT_SCOPED_KEYS,
27};
28pub use types::{
29 AuthConfig, BusinessProfile, ConnectorConfig, ContentSourceEntry, ContentSourcesConfig,
30 DeploymentCapabilities, DeploymentMode, GoogleDriveConnectorConfig, IntervalsConfig,
31 LimitsConfig, LlmConfig, LoggingConfig, ScoringConfig, ServerConfig, StorageConfig,
32 TargetsConfig, XApiConfig,
33};
34pub use types_policy::{CircuitBreakerConfig, McpPolicyConfig, ScheduleConfig};
35
36use crate::error::ConfigError;
37use serde::{Deserialize, Serialize};
38use std::env;
39use std::path::PathBuf;
40
41fn default_approval_mode() -> bool {
42 true
43}
44
45fn default_max_batch_approve() -> usize {
46 25
47}
48
49#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
56#[serde(rename_all = "lowercase")]
57pub enum OperatingMode {
58 #[default]
60 Autopilot,
61 Composer,
63}
64
65impl std::fmt::Display for OperatingMode {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 OperatingMode::Autopilot => write!(f, "autopilot"),
69 OperatingMode::Composer => write!(f, "composer"),
70 }
71 }
72}
73
74#[derive(Debug, Clone, Default, Deserialize, Serialize)]
76pub struct Config {
77 #[serde(default)]
79 pub mode: OperatingMode,
80
81 #[serde(default)]
83 pub x_api: XApiConfig,
84
85 #[serde(default)]
87 pub auth: AuthConfig,
88
89 #[serde(default)]
91 pub business: BusinessProfile,
92
93 #[serde(default)]
95 pub scoring: ScoringConfig,
96
97 #[serde(default)]
99 pub limits: LimitsConfig,
100
101 #[serde(default)]
103 pub intervals: IntervalsConfig,
104
105 #[serde(default)]
107 pub llm: LlmConfig,
108
109 #[serde(default)]
111 pub targets: TargetsConfig,
112
113 #[serde(default = "default_approval_mode")]
115 pub approval_mode: bool,
116
117 #[serde(default = "default_max_batch_approve")]
119 pub max_batch_approve: usize,
120
121 #[serde(default)]
123 pub server: ServerConfig,
124
125 #[serde(default)]
127 pub storage: StorageConfig,
128
129 #[serde(default)]
131 pub logging: LoggingConfig,
132
133 #[serde(default)]
135 pub schedule: ScheduleConfig,
136
137 #[serde(default)]
139 pub mcp_policy: McpPolicyConfig,
140
141 #[serde(default)]
143 pub circuit_breaker: CircuitBreakerConfig,
144
145 #[serde(default)]
147 pub content_sources: ContentSourcesConfig,
148
149 #[serde(default)]
152 pub deployment_mode: DeploymentMode,
153
154 #[serde(default)]
156 pub connectors: ConnectorConfig,
157}
158
159impl Config {
160 pub fn load(config_path: Option<&str>) -> Result<Config, ConfigError> {
167 let (path, explicit) = Self::resolve_config_path(config_path);
168
169 let mut config = match std::fs::read_to_string(&path) {
170 Ok(contents) => toml::from_str::<Config>(&contents)
171 .map_err(|e| ConfigError::ParseError { source: e })?,
172 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
173 if explicit {
174 return Err(ConfigError::FileNotFound {
175 path: path.display().to_string(),
176 });
177 }
178 Config::default()
179 }
180 Err(_) => {
181 return Err(ConfigError::FileNotFound {
182 path: path.display().to_string(),
183 });
184 }
185 };
186
187 config.apply_env_overrides()?;
188
189 Ok(config)
190 }
191
192 pub fn load_and_validate(config_path: Option<&str>) -> Result<Config, Vec<ConfigError>> {
194 let config = Config::load(config_path).map_err(|e| vec![e])?;
195 config.validate()?;
196 Ok(config)
197 }
198
199 pub fn effective_approval_mode(&self) -> bool {
207 self.approval_mode || self.mode == OperatingMode::Composer
208 }
209
210 pub fn is_composer_mode(&self) -> bool {
212 self.mode == OperatingMode::Composer
213 }
214
215 fn resolve_config_path(config_path: Option<&str>) -> (PathBuf, bool) {
220 if let Some(path) = config_path {
221 return (expand_tilde(path), true);
222 }
223
224 if let Ok(env_path) = env::var("TUITBOT_CONFIG") {
225 return (expand_tilde(&env_path), true);
226 }
227
228 (expand_tilde("~/.tuitbot/config.toml"), false)
229 }
230}
231
232fn expand_tilde(path: &str) -> PathBuf {
234 if let Some(rest) = path.strip_prefix("~/") {
235 if let Some(home) = dirs::home_dir() {
236 return home.join(rest);
237 }
238 } else if path == "~" {
239 if let Some(home) = dirs::home_dir() {
240 return home;
241 }
242 }
243 PathBuf::from(path)
244}