lean_agent_core/
config.rs1use crate::{Error, Result};
4use camino::{Utf8Path, Utf8PathBuf};
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct TraceConfig {
11 pub lake_root: Utf8PathBuf,
13 pub recursive: bool,
15 #[serde(with = "duration_seconds")]
17 pub timeout: Duration,
18 pub keep_raw_output: bool,
20 pub include_warnings: bool,
22 pub include_passes: bool,
24 #[serde(default)]
26 pub exclude: Vec<String>,
27}
28
29impl TraceConfig {
30 #[must_use]
32 pub fn new(lake_root: Utf8PathBuf) -> Self {
33 Self {
34 lake_root,
35 recursive: false,
36 timeout: Duration::from_secs(60),
37 keep_raw_output: false,
38 include_warnings: true,
39 include_passes: true,
40 exclude: Vec::new(),
41 }
42 }
43
44 #[must_use]
46 pub const fn recursive(mut self, recursive: bool) -> Self {
47 self.recursive = recursive;
48 self
49 }
50
51 #[must_use]
53 pub const fn timeout(mut self, timeout: Duration) -> Self {
54 self.timeout = timeout;
55 self
56 }
57
58 #[must_use]
60 pub const fn keep_raw_output(mut self, keep_raw_output: bool) -> Self {
61 self.keep_raw_output = keep_raw_output;
62 self
63 }
64}
65
66#[derive(Clone, Debug, Serialize, Deserialize)]
68pub struct ReportConfig {
69 pub sample_limit: usize,
71}
72
73impl Default for ReportConfig {
74 fn default() -> Self {
75 Self { sample_limit: 10 }
76 }
77}
78
79#[derive(Clone, Debug, Default, Deserialize, Serialize)]
84#[serde(deny_unknown_fields)]
85pub struct FileConfig {
86 #[serde(default)]
88 pub project: ProjectConfig,
89 #[serde(default)]
91 pub trace: TraceFileConfig,
92}
93
94#[derive(Clone, Debug, Default, Deserialize, Serialize)]
96#[serde(deny_unknown_fields)]
97pub struct ProjectConfig {
98 pub name: Option<String>,
100 pub lake_root: Option<Utf8PathBuf>,
102 #[serde(default)]
104 pub source_roots: Vec<Utf8PathBuf>,
105 #[serde(default)]
107 pub exclude: Vec<String>,
108}
109
110#[derive(Clone, Debug, Default, Deserialize, Serialize)]
112#[serde(deny_unknown_fields)]
113pub struct TraceFileConfig {
114 pub timeout_secs: Option<u64>,
116 pub keep_raw_output: Option<bool>,
118 pub include_warnings: Option<bool>,
120 pub only_failures: Option<bool>,
122}
123
124impl FileConfig {
125 pub fn load(path: &Utf8Path) -> Result<Self> {
127 let text = std::fs::read_to_string(path).map_err(|source| Error::ConfigRead {
128 path: path.to_path_buf(),
129 source,
130 })?;
131 toml::from_str(&text).map_err(|source| Error::ConfigParse {
132 path: path.to_path_buf(),
133 source,
134 })
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn parse(text: &str) -> Result<FileConfig> {
143 toml::from_str(text).map_err(|source| Error::ConfigParse {
144 path: Utf8PathBuf::from("<test>"),
145 source,
146 })
147 }
148
149 #[test]
150 fn parses_full_config() -> Result<()> {
151 let config = parse(
152 r#"
153[project]
154name = "demo"
155lake_root = "."
156source_roots = ["src", "test"]
157exclude = [".lake/"]
158
159[trace]
160timeout_secs = 45
161keep_raw_output = true
162include_warnings = false
163only_failures = true
164"#,
165 )?;
166 assert_eq!(config.project.name.as_deref(), Some("demo"));
167 assert_eq!(config.project.source_roots.len(), 2);
168 assert_eq!(config.project.exclude, vec![".lake/".to_owned()]);
169 assert_eq!(config.trace.timeout_secs, Some(45));
170 assert_eq!(config.trace.keep_raw_output, Some(true));
171 assert_eq!(config.trace.include_warnings, Some(false));
172 assert_eq!(config.trace.only_failures, Some(true));
173 Ok(())
174 }
175
176 #[test]
177 fn empty_config_is_all_defaults() -> Result<()> {
178 let config = parse("")?;
179 assert!(config.project.name.is_none());
180 assert!(config.project.source_roots.is_empty());
181 assert!(config.trace.timeout_secs.is_none());
182 Ok(())
183 }
184
185 #[test]
186 fn unknown_field_is_rejected() {
187 assert!(parse("[trace]\nbogus = 1\n").is_err());
188 }
189}
190
191mod duration_seconds {
192 use serde::{Deserialize, Deserializer, Serializer};
193 use std::time::Duration;
194
195 pub(crate) fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
196 where
197 S: Serializer,
198 {
199 serializer.serialize_u64(duration.as_secs())
200 }
201
202 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
203 where
204 D: Deserializer<'de>,
205 {
206 let seconds = u64::deserialize(deserializer)?;
207 Ok(Duration::from_secs(seconds))
208 }
209}