1use serde::{Deserialize, Serialize};
2use std::collections::HashSet;
3use std::str::FromStr;
4
5#[derive(Debug, Serialize, Deserialize, Clone)]
7pub struct Account {
8 pub name: String,
10 pub platform: Platform,
12 pub key_path: String,
14 pub host_alias: String,
16 pub username: String,
18 pub email: String,
20 pub gpg_key_id: Option<String>,
22}
23
24#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
26#[serde(rename_all = "lowercase")]
27pub enum Platform {
28 #[default]
30 Github,
31 Gitlab,
33 Codeberg,
35 Bitbucket,
37}
38
39impl Platform {
40 #[must_use]
42 pub fn host(&self) -> &str {
43 match self {
44 Platform::Github => "github.com",
45 Platform::Gitlab => "gitlab.com",
46 Platform::Codeberg => "codeberg.org",
47 Platform::Bitbucket => "bitbucket.org",
48 }
49 }
50
51 #[must_use]
53 pub fn provider_key_url(&self) -> &'static str {
54 match self {
55 Platform::Github => "https://github.com/settings/keys",
56 Platform::Gitlab => "https://gitlab.com/-/profile/keys",
57 Platform::Codeberg => "https://codeberg.org/user/keys",
58 Platform::Bitbucket => "https://bitbucket.org/account/settings/ssh-keys/",
59 }
60 }
61}
62
63impl FromStr for Platform {
64 type Err = ();
65
66 fn from_str(s: &str) -> Result<Self, Self::Err> {
67 match s.to_lowercase().as_str() {
68 "github" => Ok(Platform::Github),
69 "gitlab" => Ok(Platform::Gitlab),
70 "codeberg" => Ok(Platform::Codeberg),
71 "bitbucket" | "bb" => Ok(Platform::Bitbucket),
72 _ => Err(()),
73 }
74 }
75}
76
77#[derive(Debug, Serialize, Deserialize, Default, Clone)]
79pub struct GitcoreConfig {
80 pub accounts: Vec<Account>,
82}
83
84#[derive(Debug, Serialize, Deserialize, Clone)]
86pub struct Vault {
87 pub config: GitcoreConfig,
89 pub keys: Vec<VaultKey>,
91}
92
93#[derive(Debug, Serialize, Deserialize, Clone)]
95pub struct VaultKey {
96 pub filename: String,
98 pub private_content: String,
100 pub public_content: String,
102}
103
104#[must_use]
106pub fn is_valid_account_name(name: &str) -> bool {
107 !name.is_empty()
108 && name
109 .chars()
110 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
111}
112
113pub fn validate_accounts(accounts: &[Account]) -> Result<(), String> {
118 let mut names = HashSet::new();
119 let mut aliases = HashSet::new();
120
121 for acc in accounts {
122 if !is_valid_account_name(&acc.name) {
123 return Err(format!("Invalid account name '{}'", acc.name));
124 }
125
126 if acc.username.trim().is_empty() {
127 return Err(format!("Account '{}' has an empty username", acc.name));
128 }
129
130 if acc.email.trim().is_empty() {
131 return Err(format!("Account '{}' has an empty email", acc.name));
132 }
133
134 let normalized_name = acc.name.to_ascii_lowercase();
135 if !names.insert(normalized_name) {
136 return Err(format!("Duplicate account name '{}'", acc.name));
137 }
138
139 let normalized_alias = acc.host_alias.to_ascii_lowercase();
140 if !aliases.insert(normalized_alias) {
141 return Err(format!("Duplicate host alias '{}'", acc.host_alias));
142 }
143 }
144
145 Ok(())
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_is_valid_account_name() {
154 assert!(is_valid_account_name("work"));
155 assert!(is_valid_account_name("personal-git"));
156 assert!(is_valid_account_name("user_123"));
157 assert!(!is_valid_account_name(""));
158 assert!(!is_valid_account_name("work account"));
159 assert!(!is_valid_account_name("work@git"));
160 }
161
162 #[test]
163 fn test_validate_accounts_duplicates() {
164 let accounts = vec![
165 Account {
166 name: "test".to_string(),
167 platform: Platform::Github,
168 key_path: "key".to_string(),
169 host_alias: "alias".to_string(),
170 username: "user".to_string(),
171 email: "email".to_string(),
172 gpg_key_id: None,
173 },
174 Account {
175 name: "TEST".to_string(), platform: Platform::Gitlab,
177 key_path: "k2".to_string(),
178 host_alias: "h2".to_string(),
179 username: "u2".to_string(),
180 email: "e2".to_string(),
181 gpg_key_id: None,
182 },
183 ];
184 assert!(validate_accounts(&accounts).is_err());
185
186 let accounts = vec![
187 Account {
188 name: "test".to_string(),
189 platform: Platform::Github,
190 key_path: "key".to_string(),
191 host_alias: "alias".to_string(),
192 username: "user".to_string(),
193 email: "email".to_string(),
194 gpg_key_id: None,
195 },
196 Account {
197 name: "a2".to_string(),
198 platform: Platform::Github,
199 key_path: "k2".to_string(),
200 host_alias: "ALIAS".to_string(), username: "u2".to_string(),
202 email: "e2".to_string(),
203 gpg_key_id: None,
204 },
205 ];
206 assert!(validate_accounts(&accounts).is_err());
207 }
208
209 #[test]
210 fn test_validate_accounts_empty_fields() {
211 let accounts = vec![Account {
212 name: "work".to_string(),
213 platform: Platform::Github,
214 key_path: "k1".to_string(),
215 host_alias: "h1".to_string(),
216 username: " ".to_string(),
217 email: "e1".to_string(),
218 gpg_key_id: None,
219 }];
220 assert!(validate_accounts(&accounts).is_err());
221 }
222
223 #[test]
224 fn provider_key_urls_match_expected_hosts() {
225 assert_eq!(
226 Platform::Github.provider_key_url(),
227 "https://github.com/settings/keys"
228 );
229 assert_eq!(
230 Platform::Gitlab.provider_key_url(),
231 "https://gitlab.com/-/profile/keys"
232 );
233 }
234}