1use serde::{Deserialize, Serialize};
9
10fn default_true() -> bool {
11 true
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct TitleConfig {
25 #[serde(default = "default_true")]
27 pub enabled: bool,
28
29 pub provider: Option<String>,
32
33 pub model: Option<String>,
38}
39
40impl Default for TitleConfig {
41 fn default() -> Self {
42 Self {
43 enabled: true,
44 provider: None,
45 model: None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
65pub struct ObservabilityConfig {
66 #[serde(default)]
68 pub enabled: bool,
69
70 #[serde(default)]
72 pub exporter: TelemetryExporterKind,
73
74 pub endpoint: Option<String>,
79
80 pub service_name: Option<String>,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum TelemetryExporterKind {
89 #[default]
91 Otlp,
92 Stdout,
94 None,
96}
97
98impl TelemetryExporterKind {
99 pub fn is_none(self) -> bool {
101 matches!(self, Self::None)
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_title_config_default() {
111 let cfg = TitleConfig::default();
112 assert!(cfg.enabled);
113 assert!(cfg.provider.is_none());
114 assert!(cfg.model.is_none());
115 }
116
117 #[test]
118 fn test_title_config_fields() {
119 let cfg = TitleConfig {
120 enabled: false,
121 provider: Some("anthropic".to_string()),
122 model: Some("claude-haiku-4-5-20251001".to_string()),
123 };
124 assert!(!cfg.enabled);
125 assert_eq!(cfg.provider.as_deref(), Some("anthropic"));
126 assert_eq!(cfg.model.as_deref(), Some("claude-haiku-4-5-20251001"));
127 }
128
129 #[test]
130 fn test_title_config_clone_and_debug() {
131 let cfg = TitleConfig::default();
132 let cloned = cfg.clone();
133 assert_eq!(cloned.enabled, cfg.enabled);
134 assert!(format!("{:?}", cfg).contains("TitleConfig"));
136 }
137
138 #[test]
139 fn test_title_config_serde_roundtrip() {
140 let cfg = TitleConfig {
141 enabled: true,
142 provider: Some("openai".to_string()),
143 model: Some("gpt-4o-mini".to_string()),
144 };
145 let json = serde_json::to_string(&cfg).unwrap();
146 let back: TitleConfig = serde_json::from_str(&json).unwrap();
147 assert_eq!(back.enabled, cfg.enabled);
148 assert_eq!(back.provider, cfg.provider);
149 assert_eq!(back.model, cfg.model);
150 }
151
152 #[test]
153 fn test_title_config_deserialize_defaults_enabled_true() {
154 let toml_str = r#"
156provider = "anthropic"
157model = "claude-haiku-4-5-20251001"
158"#;
159 let cfg: TitleConfig = toml::from_str(toml_str).unwrap();
160 assert!(cfg.enabled);
161 assert_eq!(cfg.provider.as_deref(), Some("anthropic"));
162 }
163
164 #[test]
165 fn test_title_config_deserialize_explicit_disabled() {
166 let toml_str = r#"
167enabled = false
168"#;
169 let cfg: TitleConfig = toml::from_str(toml_str).unwrap();
170 assert!(!cfg.enabled);
171 assert!(cfg.provider.is_none());
172 assert!(cfg.model.is_none());
173 }
174
175 #[test]
176 fn observability_config_defaults_to_disabled_otlp() {
177 let cfg = ObservabilityConfig::default();
178 assert!(!cfg.enabled);
179 assert_eq!(cfg.exporter, TelemetryExporterKind::Otlp);
180 assert!(cfg.endpoint.is_none());
181 assert!(cfg.service_name.is_none());
182 let parsed: ObservabilityConfig = toml::from_str("").unwrap();
184 assert_eq!(parsed, cfg);
185 }
186
187 #[test]
188 fn observability_config_full_roundtrip() {
189 let toml_str = r#"
190enabled = true
191exporter = "stdout"
192endpoint = "http://collector:4318"
193service_name = "leviath-prod"
194"#;
195 let cfg: ObservabilityConfig = toml::from_str(toml_str).unwrap();
196 assert!(cfg.enabled);
197 assert_eq!(cfg.exporter, TelemetryExporterKind::Stdout);
198 assert_eq!(cfg.endpoint.as_deref(), Some("http://collector:4318"));
199 assert_eq!(cfg.service_name.as_deref(), Some("leviath-prod"));
200 let serialized = toml::to_string(&cfg).unwrap();
201 let back: ObservabilityConfig = toml::from_str(&serialized).unwrap();
202 assert_eq!(back, cfg);
203 assert!(format!("{cfg:?}").contains("ObservabilityConfig"));
204 }
205
206 #[test]
207 fn telemetry_exporter_kind_round_trips_through_config_syntax() {
208 for (kind, text) in [
209 (TelemetryExporterKind::Otlp, "otlp"),
210 (TelemetryExporterKind::Stdout, "stdout"),
211 (TelemetryExporterKind::None, "none"),
212 ] {
213 let toml_str = format!("exporter = \"{text}\"");
214 let cfg: ObservabilityConfig = toml::from_str(&toml_str).unwrap();
215 assert_eq!(cfg.exporter, kind);
216 let serialized = toml::to_string(&cfg).unwrap();
217 assert!(serialized.contains(text), "{serialized} missing {text}");
218 }
219 }
220
221 #[test]
222 fn telemetry_exporter_kind_is_none_helper() {
223 assert!(TelemetryExporterKind::None.is_none());
224 assert!(!TelemetryExporterKind::Otlp.is_none());
225 assert!(!TelemetryExporterKind::Stdout.is_none());
226 }
227
228 #[test]
229 fn telemetry_exporter_kind_rejects_unknown_value() {
230 let err = toml::from_str::<ObservabilityConfig>("exporter = \"grpc\"");
231 assert!(err.is_err());
232 }
233}