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