use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
pub const USER_METADATA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StorageScope {
Global,
Local,
}
impl std::fmt::Display for StorageScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StorageScope::Global => write!(f, "global"),
StorageScope::Local => write!(f, "local"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavedAlias {
pub command: String,
pub args: Vec<String>,
pub created: DateTime<Utc>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryEntry {
pub id: u64,
pub timestamp: DateTime<Utc>,
pub command: String,
pub args: Vec<String>,
pub working_dir: PathBuf,
pub success: bool,
pub duration_ms: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HistoryState {
pub entries: Vec<HistoryEntry>,
pub next_id: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserMetadata {
pub version: u32,
pub aliases: HashMap<String, SavedAlias>,
pub history: HistoryState,
}
impl Default for UserMetadata {
fn default() -> Self {
Self {
version: USER_METADATA_VERSION,
aliases: HashMap::new(),
history: HistoryState::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AliasExportFile {
pub version: u32,
pub exported_at: DateTime<Utc>,
pub aliases: HashMap<String, SavedAlias>,
}
impl AliasExportFile {
#[must_use]
pub fn from_aliases(aliases: &[AliasWithScope]) -> Self {
let mut map = HashMap::new();
for aws in aliases {
map.insert(aws.name.clone(), aws.alias.clone());
}
Self {
version: 1,
exported_at: Utc::now(),
aliases: map,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportConflictStrategy {
Skip,
Overwrite,
Fail,
}
#[derive(Debug, Clone)]
pub struct ImportResult {
pub imported: usize,
pub skipped: usize,
pub failed: usize,
pub overwritten: usize,
pub skipped_names: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct AliasWithScope {
pub name: String,
pub alias: SavedAlias,
pub scope: StorageScope,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_saved_alias_serialization() {
let alias = SavedAlias {
command: "query".to_string(),
args: vec![
"kind:function".to_string(),
"--lang".to_string(),
"rust".to_string(),
],
created: Utc::now(),
description: Some("Find Rust functions".to_string()),
};
let bytes = postcard::to_allocvec(&alias).expect("postcard serialize");
let deserialized: SavedAlias = postcard::from_bytes(&bytes).expect("postcard deserialize");
assert_eq!(alias.command, deserialized.command);
assert_eq!(alias.args, deserialized.args);
assert_eq!(alias.description, deserialized.description);
}
#[test]
fn test_history_entry_serialization() {
let entry = HistoryEntry {
id: 42,
timestamp: Utc::now(),
command: "search".to_string(),
args: vec!["main".to_string()],
working_dir: PathBuf::from("/home/user/project"),
success: true,
duration_ms: Some(150),
};
let bytes = postcard::to_allocvec(&entry).expect("postcard serialize");
let deserialized: HistoryEntry =
postcard::from_bytes(&bytes).expect("postcard deserialize");
assert_eq!(entry.id, deserialized.id);
assert_eq!(entry.command, deserialized.command);
assert_eq!(entry.args, deserialized.args);
assert_eq!(entry.success, deserialized.success);
}
#[test]
fn test_user_metadata_default() {
let metadata = UserMetadata::default();
assert_eq!(metadata.version, USER_METADATA_VERSION);
assert!(metadata.aliases.is_empty());
assert!(metadata.history.entries.is_empty());
assert_eq!(metadata.history.next_id, 0);
}
#[test]
fn test_user_metadata_serialization() {
let mut metadata = UserMetadata::default();
metadata.aliases.insert(
"test".to_string(),
SavedAlias {
command: "search".to_string(),
args: vec!["pattern".to_string()],
created: Utc::now(),
description: None,
},
);
metadata.history.entries.push(HistoryEntry {
id: 1,
timestamp: Utc::now(),
command: "query".to_string(),
args: vec!["kind:function".to_string()],
working_dir: PathBuf::from("/tmp"),
success: true,
duration_ms: Some(50),
});
metadata.history.next_id = 2;
let bytes = postcard::to_allocvec(&metadata).expect("postcard serialize");
let deserialized: UserMetadata =
postcard::from_bytes(&bytes).expect("postcard deserialize");
assert_eq!(deserialized.version, USER_METADATA_VERSION);
assert!(deserialized.aliases.contains_key("test"));
assert_eq!(deserialized.history.entries.len(), 1);
assert_eq!(deserialized.history.next_id, 2);
}
#[test]
fn test_storage_scope_display() {
assert_eq!(format!("{}", StorageScope::Global), "global");
assert_eq!(format!("{}", StorageScope::Local), "local");
}
#[test]
fn test_alias_export_file_json() {
let mut aliases = HashMap::new();
aliases.insert(
"test".to_string(),
SavedAlias {
command: "search".to_string(),
args: vec!["main".to_string()],
created: Utc::now(),
description: None,
},
);
let export = AliasExportFile {
version: 1,
exported_at: Utc::now(),
aliases,
};
let json = serde_json::to_string(&export).expect("json serialize");
let deserialized: AliasExportFile = serde_json::from_str(&json).expect("json deserialize");
assert_eq!(deserialized.version, 1);
assert!(deserialized.aliases.contains_key("test"));
}
}