use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::attribute::AttributeValue;
use crate::memory::MemoryId;
use crate::summary::SummaryId;
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
pub struct SnapshotId(Ulid);
impl SnapshotId {
#[must_use]
pub fn generate() -> Self {
if std::env::var_os("KIROMI_AI_TEST_DETERMINISTIC_ULID").is_some() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(8_000_000);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
return SnapshotId(Ulid::from_parts(n, u128::from(n)));
}
SnapshotId(Ulid::new())
}
#[must_use]
pub const fn from_ulid(u: Ulid) -> Self {
SnapshotId(u)
}
#[must_use]
pub const fn as_ulid(&self) -> Ulid {
self.0
}
}
impl fmt::Display for SnapshotId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl FromStr for SnapshotId {
type Err = ulid::DecodeError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
s.parse::<Ulid>().map(SnapshotId)
}
}
impl From<SnapshotId> for String {
fn from(id: SnapshotId) -> String {
id.0.to_string()
}
}
impl TryFrom<String> for SnapshotId {
type Error = ulid::DecodeError;
fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
s.parse()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapshotRef {
pub id: SnapshotId,
pub seq: i64,
pub created_at_ms: i64,
pub tag: Option<String>,
pub reason: Option<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default)]
pub struct SnapshotOpts {
pub tag: Option<String>,
pub reason: Option<String>,
}
impl SnapshotOpts {
#[must_use]
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
#[must_use]
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotManifest {
pub snapshot_id: SnapshotId,
pub seq: i64,
pub created_at_ms: i64,
pub memory_ids: Vec<MemoryId>,
#[serde(default)]
pub summary_ids: Vec<SummaryId>,
pub link_pairs: Vec<(MemoryId, MemoryId)>,
#[serde(default)]
pub attributes: Vec<SnapshotAttribute>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotAttribute {
pub memory_id: MemoryId,
pub key: String,
pub value: AttributeValue,
}
impl SnapshotManifest {
#[must_use]
pub fn manifest_path_for(id: SnapshotId) -> String {
format!("snapshots/{id}.manifest.json")
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct RestoreOpts {
pub also_restore_attributes: bool,
}
impl Default for RestoreOpts {
fn default() -> Self {
Self {
also_restore_attributes: true,
}
}
}
impl RestoreOpts {
#[must_use]
pub fn with_also_restore_attributes(mut self, v: bool) -> Self {
self.also_restore_attributes = v;
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RestoreReport {
pub memories_re_tombstoned: u64,
pub memories_un_tombstoned: u64,
pub summaries_re_tombstoned: u64,
pub summaries_un_tombstoned: u64,
pub links_added: u64,
pub links_removed: u64,
pub attributes_set: u64,
pub attributes_cleared: u64,
pub partitions_marked_stale: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn id_round_trips_through_string() {
let id = SnapshotId::generate();
let s = id.to_string();
let back: SnapshotId = s.parse().unwrap();
assert_eq!(id, back);
}
#[test]
fn id_serde_round_trip() {
let id = SnapshotId::generate();
let j = serde_json::to_string(&id).unwrap();
let back: SnapshotId = serde_json::from_str(&j).unwrap();
assert_eq!(id, back);
}
#[test]
fn manifest_path_is_stable() {
let id = SnapshotId::from_ulid(Ulid::from_string("01HXAZ0000000000000000000Z").unwrap());
assert_eq!(
SnapshotManifest::manifest_path_for(id),
"snapshots/01HXAZ0000000000000000000Z.manifest.json"
);
}
#[test]
fn opts_default_is_empty() {
let o = SnapshotOpts::default();
assert!(o.tag.is_none() && o.reason.is_none());
}
}