use chrono::Utc;
#[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
use rusqlite::ToSql;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{fmt::Display, time::Duration};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct HistoryItemId(pub i64);
impl HistoryItemId {
pub const fn new(i: i64) -> HistoryItemId {
HistoryItemId(i)
}
}
impl Display for HistoryItemId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HistorySessionId(pub(crate) i64);
impl HistorySessionId {
pub(crate) const fn new(i: i64) -> HistorySessionId {
HistorySessionId(i)
}
}
impl Display for HistorySessionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
impl ToSql for HistorySessionId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::Owned(
rusqlite::types::Value::Integer(self.0),
))
}
}
impl From<HistorySessionId> for i64 {
fn from(id: HistorySessionId) -> Self {
id.0
}
}
pub trait HistoryItemExtraInfo: Serialize + DeserializeOwned + Default + Send {}
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
pub struct IgnoreAllExtraInfo;
impl Serialize for IgnoreAllExtraInfo {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
Option::<IgnoreAllExtraInfo>::None.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for IgnoreAllExtraInfo {
fn deserialize<D>(d: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
serde::de::IgnoredAny::deserialize(d).map(|_| IgnoreAllExtraInfo)
}
}
impl HistoryItemExtraInfo for IgnoreAllExtraInfo {}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HistoryItem<ExtraInfo: HistoryItemExtraInfo = IgnoreAllExtraInfo> {
pub id: Option<HistoryItemId>,
pub start_timestamp: Option<chrono::DateTime<Utc>>,
pub command_line: String,
pub session_id: Option<HistorySessionId>,
pub hostname: Option<String>,
pub cwd: Option<String>,
pub duration: Option<Duration>,
pub exit_status: Option<i64>,
#[serde(deserialize_with = "Option::<ExtraInfo>::deserialize")]
pub more_info: Option<ExtraInfo>,
}
impl HistoryItem {
pub fn from_command_line(cmd: impl Into<String>) -> HistoryItem {
HistoryItem {
id: None,
start_timestamp: None,
command_line: cmd.into(),
session_id: None,
hostname: None,
cwd: None,
duration: None,
exit_status: None,
more_info: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
struct CustomExtraInfo {
mode: String,
tags: Vec<String>,
}
impl HistoryItemExtraInfo for CustomExtraInfo {}
#[test]
fn test_history_item_with_default_extra_info() {
let item = HistoryItem::from_command_line("echo hello");
assert_eq!(item.command_line, "echo hello");
assert!(item.more_info.is_none());
}
#[test]
fn test_history_item_with_custom_extra_info() {
let item: HistoryItem<CustomExtraInfo> = HistoryItem {
id: None,
start_timestamp: None,
command_line: "echo hello".to_string(),
session_id: None,
hostname: None,
cwd: None,
duration: None,
exit_status: None,
more_info: Some(CustomExtraInfo {
mode: "shell".to_string(),
tags: vec!["test".to_string()],
}),
};
assert_eq!(item.command_line, "echo hello");
let extra = item.more_info.unwrap();
assert_eq!(extra.mode, "shell");
assert_eq!(extra.tags, vec!["test".to_string()]);
}
#[test]
fn test_custom_extra_info_serialization() {
let item: HistoryItem<CustomExtraInfo> = HistoryItem {
id: Some(HistoryItemId::new(1)),
start_timestamp: None,
command_line: "ls -la".to_string(),
session_id: None,
hostname: None,
cwd: Some("/home/user".to_string()),
duration: None,
exit_status: Some(0),
more_info: Some(CustomExtraInfo {
mode: "r".to_string(),
tags: vec!["data".to_string(), "analysis".to_string()],
}),
};
let json = serde_json::to_string(&item).expect("serialization should succeed");
assert!(json.contains("\"mode\":\"r\""));
assert!(json.contains("\"tags\":[\"data\",\"analysis\"]"));
let deserialized: HistoryItem<CustomExtraInfo> =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(deserialized.command_line, "ls -la");
assert_eq!(deserialized.more_info.as_ref().unwrap().mode, "r");
}
#[test]
fn test_ignore_all_extra_info_serialization() {
let item = HistoryItem::from_command_line("pwd");
let json = serde_json::to_string(&item).expect("serialization should succeed");
assert!(json.contains("\"more_info\":null"));
let deserialized: HistoryItem =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(deserialized.command_line, "pwd");
}
}