use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryUpsertRequest {
pub name: String,
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub written_by_agent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub written_by_device: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryUpsertResponse {
pub memory_id: String,
pub name: String,
pub version: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryRestoreRequest {
pub version: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub written_by_agent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub written_by_device: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct MemoryListQuery {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name_prefix: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryRow {
pub memory_id: String,
pub name: String,
pub version: i64,
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub written_by_agent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub written_by_device: Option<String>,
pub written_at: String,
pub is_tombstone: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemorySummary {
pub name: String,
pub version: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>,
pub written_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryWithHistory {
#[serde(flatten)]
pub latest: MemoryRow,
pub history: Vec<MemoryRow>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryListResponse {
pub items: Vec<MemorySummary>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn upsert_request_minimal_deserializes() {
let raw = serde_json::json!({
"name": "feedback_demo",
"content": "body",
});
let req: MemoryUpsertRequest = serde_json::from_value(raw).unwrap();
assert_eq!(req.name, "feedback_demo");
assert_eq!(req.content, "body");
assert!(req.description.is_none());
assert!(req.r#type.is_none());
assert!(req.written_by_agent.is_none());
assert!(req.written_by_device.is_none());
}
#[test]
fn upsert_request_full_deserializes() {
let raw = serde_json::json!({
"name": "feedback_demo",
"content": "body",
"description": "what it does",
"type": "feedback",
"written_by_agent": "11111111-1111-1111-1111-111111111111",
"written_by_device": "22222222-2222-2222-2222-222222222222",
});
let req: MemoryUpsertRequest = serde_json::from_value(raw).unwrap();
assert_eq!(req.description.as_deref(), Some("what it does"));
assert_eq!(req.r#type.as_deref(), Some("feedback"));
assert_eq!(
req.written_by_agent.as_deref(),
Some("11111111-1111-1111-1111-111111111111")
);
assert_eq!(
req.written_by_device.as_deref(),
Some("22222222-2222-2222-2222-222222222222")
);
}
#[test]
fn upsert_request_skips_none_on_serialize() {
let req = MemoryUpsertRequest {
name: "n".into(),
content: "c".into(),
description: None,
r#type: None,
written_by_agent: None,
written_by_device: None,
};
let s = serde_json::to_string(&req).unwrap();
assert!(!s.contains("description"));
assert!(!s.contains("type"));
assert!(!s.contains("written_by_agent"));
assert!(!s.contains("written_by_device"));
}
#[test]
fn list_query_defaults() {
let q: MemoryListQuery = serde_json::from_value(serde_json::json!({})).unwrap();
assert!(q.r#type.is_none());
assert!(q.name_prefix.is_none());
assert!(q.limit.is_none());
}
#[test]
fn list_query_full_deserializes() {
let q: MemoryListQuery = serde_json::from_value(serde_json::json!({
"type": "feedback",
"name_prefix": "feedback_",
"limit": 50,
}))
.unwrap();
assert_eq!(q.r#type.as_deref(), Some("feedback"));
assert_eq!(q.name_prefix.as_deref(), Some("feedback_"));
assert_eq!(q.limit, Some(50));
}
#[test]
fn restore_request_deserializes() {
let raw = serde_json::json!({"version": 3});
let req: MemoryRestoreRequest = serde_json::from_value(raw).unwrap();
assert_eq!(req.version, 3);
assert!(req.written_by_agent.is_none());
}
#[test]
fn memory_with_history_flattens_latest() {
let latest = MemoryRow {
memory_id: "11111111-1111-1111-1111-111111111111".into(),
name: "n".into(),
version: 2,
content: "c".into(),
description: None,
r#type: None,
written_by_agent: None,
written_by_device: None,
written_at: "2026-05-22T00:00:00+00:00".into(),
is_tombstone: false,
};
let payload = MemoryWithHistory {
latest: latest.clone(),
history: vec![latest],
};
let s = serde_json::to_value(&payload).unwrap();
assert_eq!(s["name"], "n");
assert_eq!(s["version"], 2);
assert!(s["history"].is_array());
}
#[test]
fn list_response_serialized_as_named_envelope() {
let resp = MemoryListResponse {
items: vec![MemorySummary {
name: "n".into(),
version: 1,
description: None,
r#type: None,
written_at: "2026-05-22T00:00:00+00:00".into(),
}],
};
let s = serde_json::to_string(&resp).unwrap();
assert!(s.contains("\"items\""));
assert!(s.contains("\"name\":\"n\""));
}
}