1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::PathBuf;
11
12pub const USER_METADATA_VERSION: u32 = 1;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum StorageScope {
19 Global,
21 Local,
23}
24
25impl std::fmt::Display for StorageScope {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 StorageScope::Global => write!(f, "global"),
29 StorageScope::Local => write!(f, "local"),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SavedAlias {
40 pub command: String,
42
43 pub args: Vec<String>,
45
46 pub created: DateTime<Utc>,
48
49 pub description: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct HistoryEntry {
56 pub id: u64,
58
59 pub timestamp: DateTime<Utc>,
61
62 pub command: String,
64
65 pub args: Vec<String>,
67
68 pub working_dir: PathBuf,
70
71 pub success: bool,
73
74 pub duration_ms: Option<u64>,
76}
77
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
80pub struct HistoryState {
81 pub entries: Vec<HistoryEntry>,
83
84 pub next_id: u64,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct UserMetadata {
94 pub version: u32,
96
97 pub aliases: HashMap<String, SavedAlias>,
99
100 pub history: HistoryState,
102}
103
104impl Default for UserMetadata {
105 fn default() -> Self {
106 Self {
107 version: USER_METADATA_VERSION,
108 aliases: HashMap::new(),
109 history: HistoryState::default(),
110 }
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct AliasExportFile {
117 pub version: u32,
119
120 pub exported_at: DateTime<Utc>,
122
123 pub aliases: HashMap<String, SavedAlias>,
125}
126
127impl AliasExportFile {
128 #[must_use]
130 pub fn from_aliases(aliases: &[AliasWithScope]) -> Self {
131 let mut map = HashMap::new();
132 for aws in aliases {
133 map.insert(aws.name.clone(), aws.alias.clone());
134 }
135 Self {
136 version: 1,
137 exported_at: Utc::now(),
138 aliases: map,
139 }
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum ImportConflictStrategy {
146 Skip,
148 Overwrite,
150 Fail,
152}
153
154#[derive(Debug, Clone)]
156pub struct ImportResult {
157 pub imported: usize,
159 pub skipped: usize,
161 pub failed: usize,
163 pub overwritten: usize,
165 pub skipped_names: Vec<String>,
167}
168
169#[derive(Debug, Clone)]
171pub struct AliasWithScope {
172 pub name: String,
174 pub alias: SavedAlias,
176 pub scope: StorageScope,
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_saved_alias_serialization() {
186 let alias = SavedAlias {
187 command: "query".to_string(),
188 args: vec![
189 "kind:function".to_string(),
190 "--lang".to_string(),
191 "rust".to_string(),
192 ],
193 created: Utc::now(),
194 description: Some("Find Rust functions".to_string()),
195 };
196
197 let bytes = postcard::to_allocvec(&alias).expect("postcard serialize");
199 let deserialized: SavedAlias = postcard::from_bytes(&bytes).expect("postcard deserialize");
200
201 assert_eq!(alias.command, deserialized.command);
202 assert_eq!(alias.args, deserialized.args);
203 assert_eq!(alias.description, deserialized.description);
204 }
205
206 #[test]
207 fn test_history_entry_serialization() {
208 let entry = HistoryEntry {
209 id: 42,
210 timestamp: Utc::now(),
211 command: "search".to_string(),
212 args: vec!["main".to_string()],
213 working_dir: PathBuf::from("/home/user/project"),
214 success: true,
215 duration_ms: Some(150),
216 };
217
218 let bytes = postcard::to_allocvec(&entry).expect("postcard serialize");
219 let deserialized: HistoryEntry =
220 postcard::from_bytes(&bytes).expect("postcard deserialize");
221
222 assert_eq!(entry.id, deserialized.id);
223 assert_eq!(entry.command, deserialized.command);
224 assert_eq!(entry.args, deserialized.args);
225 assert_eq!(entry.success, deserialized.success);
226 }
227
228 #[test]
229 fn test_user_metadata_default() {
230 let metadata = UserMetadata::default();
231
232 assert_eq!(metadata.version, USER_METADATA_VERSION);
233 assert!(metadata.aliases.is_empty());
234 assert!(metadata.history.entries.is_empty());
235 assert_eq!(metadata.history.next_id, 0);
236 }
237
238 #[test]
239 fn test_user_metadata_serialization() {
240 let mut metadata = UserMetadata::default();
241 metadata.aliases.insert(
242 "test".to_string(),
243 SavedAlias {
244 command: "search".to_string(),
245 args: vec!["pattern".to_string()],
246 created: Utc::now(),
247 description: None,
248 },
249 );
250 metadata.history.entries.push(HistoryEntry {
251 id: 1,
252 timestamp: Utc::now(),
253 command: "query".to_string(),
254 args: vec!["kind:function".to_string()],
255 working_dir: PathBuf::from("/tmp"),
256 success: true,
257 duration_ms: Some(50),
258 });
259 metadata.history.next_id = 2;
260
261 let bytes = postcard::to_allocvec(&metadata).expect("postcard serialize");
262 let deserialized: UserMetadata =
263 postcard::from_bytes(&bytes).expect("postcard deserialize");
264
265 assert_eq!(deserialized.version, USER_METADATA_VERSION);
266 assert!(deserialized.aliases.contains_key("test"));
267 assert_eq!(deserialized.history.entries.len(), 1);
268 assert_eq!(deserialized.history.next_id, 2);
269 }
270
271 #[test]
272 fn test_storage_scope_display() {
273 assert_eq!(format!("{}", StorageScope::Global), "global");
274 assert_eq!(format!("{}", StorageScope::Local), "local");
275 }
276
277 #[test]
278 fn test_alias_export_file_json() {
279 let mut aliases = HashMap::new();
280 aliases.insert(
281 "test".to_string(),
282 SavedAlias {
283 command: "search".to_string(),
284 args: vec!["main".to_string()],
285 created: Utc::now(),
286 description: None,
287 },
288 );
289
290 let export = AliasExportFile {
291 version: 1,
292 exported_at: Utc::now(),
293 aliases,
294 };
295
296 let json = serde_json::to_string(&export).expect("json serialize");
297 let deserialized: AliasExportFile = serde_json::from_str(&json).expect("json deserialize");
298
299 assert_eq!(deserialized.version, 1);
300 assert!(deserialized.aliases.contains_key("test"));
301 }
302}