chronicle/config/
user_config.rs1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::setup_error::{
6 NoHomeDirectorySnafu, ReadConfigSnafu, ReadFileSnafu, WriteConfigSnafu, WriteFileSnafu,
7};
8use crate::error::SetupError;
9use snafu::ResultExt;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct UserConfig {
14 pub provider: ProviderConfig,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct ProviderConfig {
20 #[serde(rename = "type")]
21 pub provider_type: ProviderType,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub model: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub api_key_env: Option<String>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "kebab-case")]
31pub enum ProviderType {
32 ClaudeCode,
33 Anthropic,
34 None,
35}
36
37impl std::fmt::Display for ProviderType {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 ProviderType::ClaudeCode => write!(f, "claude-code"),
41 ProviderType::Anthropic => write!(f, "anthropic"),
42 ProviderType::None => write!(f, "none"),
43 }
44 }
45}
46
47impl UserConfig {
48 pub fn path() -> Result<PathBuf, SetupError> {
50 let home = std::env::var("HOME")
51 .ok()
52 .map(PathBuf::from)
53 .filter(|p| p.is_absolute())
54 .ok_or_else(|| NoHomeDirectorySnafu.build())?;
55 Ok(home.join(".git-chronicle.toml"))
56 }
57
58 pub fn load() -> Result<Option<Self>, SetupError> {
61 let path = Self::path()?;
62 if !path.exists() {
63 return Ok(None);
64 }
65 let contents = std::fs::read_to_string(&path).context(ReadFileSnafu {
66 path: path.display().to_string(),
67 })?;
68 let config: UserConfig = toml::from_str(&contents).context(ReadConfigSnafu)?;
69 Ok(Some(config))
70 }
71
72 pub fn save(&self) -> Result<(), SetupError> {
74 let path = Self::path()?;
75 let contents = toml::to_string_pretty(self).context(WriteConfigSnafu)?;
76 std::fs::write(&path, contents).context(WriteFileSnafu {
77 path: path.display().to_string(),
78 })?;
79 Ok(())
80 }
81}
82
83impl Default for UserConfig {
84 fn default() -> Self {
85 Self {
86 provider: ProviderConfig {
87 provider_type: ProviderType::None,
88 model: None,
89 api_key_env: None,
90 },
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_provider_type_serialization() {
101 let config = UserConfig {
102 provider: ProviderConfig {
103 provider_type: ProviderType::ClaudeCode,
104 model: None,
105 api_key_env: None,
106 },
107 };
108 let toml_str = toml::to_string_pretty(&config).unwrap();
109 assert!(toml_str.contains("\"claude-code\""));
110
111 let config2 = UserConfig {
112 provider: ProviderConfig {
113 provider_type: ProviderType::Anthropic,
114 model: Some("claude-sonnet-4-5-20250929".to_string()),
115 api_key_env: Some("ANTHROPIC_API_KEY".to_string()),
116 },
117 };
118 let toml_str2 = toml::to_string_pretty(&config2).unwrap();
119 assert!(toml_str2.contains("\"anthropic\""));
120 assert!(toml_str2.contains("claude-sonnet-4-5-20250929"));
121 }
122
123 #[test]
124 fn test_roundtrip() {
125 let config = UserConfig {
126 provider: ProviderConfig {
127 provider_type: ProviderType::ClaudeCode,
128 model: Some("claude-sonnet-4-5-20250929".to_string()),
129 api_key_env: None,
130 },
131 };
132 let toml_str = toml::to_string_pretty(&config).unwrap();
133 let parsed: UserConfig = toml::from_str(&toml_str).unwrap();
134 assert_eq!(config, parsed);
135 }
136
137 #[test]
138 fn test_none_provider() {
139 let config = UserConfig {
140 provider: ProviderConfig {
141 provider_type: ProviderType::None,
142 model: None,
143 api_key_env: None,
144 },
145 };
146 let toml_str = toml::to_string_pretty(&config).unwrap();
147 assert!(toml_str.contains("\"none\""));
148 let parsed: UserConfig = toml::from_str(&toml_str).unwrap();
149 assert_eq!(config, parsed);
150 }
151
152 #[test]
153 fn test_provider_type_display() {
154 assert_eq!(ProviderType::ClaudeCode.to_string(), "claude-code");
155 assert_eq!(ProviderType::Anthropic.to_string(), "anthropic");
156 assert_eq!(ProviderType::None.to_string(), "none");
157 }
158}