Skip to main content

code_moniker_workspace/environment/
project_config.rs

1use std::path::Path;
2
3use anyhow::{Context as _, ensure};
4use serde::Deserialize;
5
6pub const PROJECT_CONFIG_FILE: &str = ".code-moniker.toml";
7
8#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
9#[serde(deny_unknown_fields)]
10pub struct TelemetryConfig {
11	#[serde(default)]
12	pub enabled: bool,
13	#[serde(default)]
14	pub service_name: Option<String>,
15	#[serde(default)]
16	pub endpoint: Option<String>,
17	#[serde(default)]
18	pub metric_export_interval_ms: Option<u64>,
19}
20
21impl TelemetryConfig {
22	fn validate(self) -> anyhow::Result<Self> {
23		if let Some(service_name) = self.service_name.as_deref() {
24			ensure!(
25				!service_name.trim().is_empty(),
26				"telemetry.service_name cannot be empty"
27			);
28		}
29		if let Some(endpoint) = self.endpoint.as_deref() {
30			ensure!(
31				!endpoint.trim().is_empty(),
32				"telemetry.endpoint cannot be empty"
33			);
34		}
35		if let Some(interval) = self.metric_export_interval_ms {
36			ensure!(
37				interval > 0,
38				"telemetry.metric_export_interval_ms must be greater than zero"
39			);
40		}
41		Ok(self)
42	}
43}
44
45#[derive(Default, Deserialize)]
46struct ProjectConfig {
47	#[serde(default)]
48	telemetry: TelemetryConfig,
49}
50
51pub fn load_telemetry_config(path: &Path) -> anyhow::Result<TelemetryConfig> {
52	if !path.exists() {
53		return Ok(TelemetryConfig::default());
54	}
55	let text = std::fs::read_to_string(path)
56		.with_context(|| format!("read project config {}", path.display()))?;
57	let config: ProjectConfig = toml::from_str(&text)
58		.with_context(|| format!("parse project config {}", path.display()))?;
59	config.telemetry.validate()
60}
61
62#[cfg(test)]
63mod tests {
64	use super::*;
65
66	#[test]
67	fn telemetry_config_is_optional_and_project_scoped() {
68		let directory = tempfile::tempdir().unwrap();
69		let path = directory.path().join(PROJECT_CONFIG_FILE);
70		assert_eq!(
71			load_telemetry_config(&path).unwrap(),
72			TelemetryConfig::default()
73		);
74		std::fs::write(
75			&path,
76			r#"
77[aliases]
78root = "workspace"
79
80[telemetry]
81enabled = true
82service_name = "code-moniker-daemon:example"
83endpoint = "http://127.0.0.1:4318"
84metric_export_interval_ms = 5000
85"#,
86		)
87		.unwrap();
88		assert_eq!(
89			load_telemetry_config(&path).unwrap(),
90			TelemetryConfig {
91				enabled: true,
92				service_name: Some("code-moniker-daemon:example".to_string()),
93				endpoint: Some("http://127.0.0.1:4318".to_string()),
94				metric_export_interval_ms: Some(5000),
95			}
96		);
97	}
98
99	#[test]
100	fn telemetry_config_rejects_invalid_owned_fields() {
101		let directory = tempfile::tempdir().unwrap();
102		let path = directory.path().join(PROJECT_CONFIG_FILE);
103		std::fs::write(&path, "[telemetry]\nmetric_export_interval_ms = 0\n").unwrap();
104		assert!(load_telemetry_config(&path).is_err());
105		std::fs::write(&path, "[telemetry]\nservice_name = \"  \"\n").unwrap();
106		assert!(load_telemetry_config(&path).is_err());
107		std::fs::write(&path, "[telemetry]\nunknown = true\n").unwrap();
108		assert!(load_telemetry_config(&path).is_err());
109	}
110}