use serde::{Deserialize, Deserializer};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PromptHistoryEntry {
pub content: String,
pub content_id: Option<String>,
}
impl PromptHistoryEntry {
#[must_use]
pub const fn new(content: String, content_id: Option<String>) -> Self {
Self {
content,
content_id,
}
}
#[must_use]
pub const fn from_string(content: String) -> Self {
Self {
content,
content_id: None,
}
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum PromptHistoryEntryRepr {
Legacy(String),
Current {
content: String,
#[serde(default)]
content_id: Option<String>,
},
}
impl<'de> Deserialize<'de> for PromptHistoryEntry {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
match PromptHistoryEntryRepr::deserialize(deserializer)? {
PromptHistoryEntryRepr::Legacy(content) => Ok(Self {
content,
content_id: None,
}),
PromptHistoryEntryRepr::Current {
content,
content_id,
} => Ok(Self {
content,
content_id,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize_round_trip() {
let entry = PromptHistoryEntry {
content: "my prompt".to_string(),
content_id: Some("abc123".to_string()),
};
let json = serde_json::to_string(&entry).unwrap();
let deserialized: PromptHistoryEntry = serde_json::from_str(&json).unwrap();
assert_eq!(entry, deserialized);
}
#[test]
fn test_serialize_with_none_content_id() {
let entry = PromptHistoryEntry {
content: "my prompt".to_string(),
content_id: None,
};
let json = serde_json::to_string(&entry).unwrap();
assert!(
!json.contains("content_id"),
"When content_id is None, serialization should omit the field; got: {json}"
);
let deserialized: PromptHistoryEntry = serde_json::from_str(&json).unwrap();
assert_eq!(entry, deserialized);
}
#[test]
fn test_deserialize_legacy_bare_string() {
let json = r#""some legacy prompt""#;
let entry: PromptHistoryEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.content, "some legacy prompt");
assert_eq!(entry.content_id, None);
}
#[test]
fn test_deserialize_v1_object_with_content_id() {
let json = r#"{"content":"my prompt","content_id":"sha256abc"}"#;
let entry: PromptHistoryEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.content, "my prompt");
assert_eq!(entry.content_id.as_deref(), Some("sha256abc"));
}
#[test]
fn test_deserialize_v1_object_without_content_id() {
let json = r#"{"content":"my prompt"}"#;
let entry: PromptHistoryEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.content, "my prompt");
assert_eq!(entry.content_id, None);
}
#[test]
fn test_hashmap_deserialize_from_legacy_format() {
let json = r#"{"planning_1":"some plan prompt","development_1":"some dev prompt"}"#;
let map: std::collections::HashMap<String, PromptHistoryEntry> =
serde_json::from_str(json).unwrap();
assert_eq!(map.len(), 2);
assert_eq!(map["planning_1"].content, "some plan prompt");
assert_eq!(map["planning_1"].content_id, None);
assert_eq!(map["development_1"].content, "some dev prompt");
}
}