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