kcode-k1-chat-persistence-values 0.1.1

Generic value owner for K1 chat persistence
Documentation
#![forbid(unsafe_code)]

pub use kcode_k1_chat_boxes::{BoxId, ChatBox, ToolCallId};
pub use kcode_k1_transaction_id::TxId;
use serde_json::Value;
use std::io;

pub type SessionId = [u8; 12];
type Result<T, E = Error> = std::result::Result<T, E>;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EventRecord {
    pub after_box_id: u64,
    pub event_index: u64,
    pub connected_box_id: u64,
    pub handler: String,
    pub data: Value,
}

impl EventRecord {
    pub fn new(
        after_box_id: u64,
        event_index: u64,
        connected_box_id: u64,
        handler: String,
        data: Value,
    ) -> Result<Self> {
        if handler.is_empty() {
            return Err(Error::Invalid("empty event handler"));
        }
        Ok(Self {
            after_box_id,
            event_index,
            connected_box_id,
            handler,
            data,
        })
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Record {
    Box(ChatBox),
    Event(EventRecord),
}

impl Record {
    pub fn chat_box(value: ChatBox) -> Self {
        Self::Box(value)
    }

    pub fn event(value: EventRecord) -> Self {
        Self::Event(value)
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SessionLog {
    pub boxes: Vec<ChatBox>,
    pub events: Vec<EventRecord>,
    pub records: Vec<Record>,
}

#[derive(Debug)]
pub enum Error {
    Io(io::Error),
    Json(serde_json::Error),
    Invalid(&'static str),
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Io(error) => write!(f, "I/O error: {error}"),
            Self::Json(error) => write!(f, "JSON error: {error}"),
            Self::Invalid(error) => f.write_str(error),
        }
    }
}

impl std::error::Error for Error {}

impl From<io::Error> for Error {
    fn from(value: io::Error) -> Self {
        Self::Io(value)
    }
}

impl From<serde_json::Error> for Error {
    fn from(value: serde_json::Error) -> Self {
        Self::Json(value)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    fn chat_box() -> ChatBox {
        ChatBox::new(
            BoxId::new(7),
            "Future Kind".into(),
            "opaque".into(),
            "future/v1".into(),
            "hidden".into(),
        )
    }

    #[test]
    fn constructors_preserve_values_and_reject_only_empty_handlers() {
        let event = EventRecord::new(7, 2, 6, "handler".into(), json!({"key": 1})).unwrap();
        assert_eq!(event.after_box_id, 7);
        assert_eq!(event.event_index, 2);
        assert_eq!(event.connected_box_id, 6);
        assert_eq!(event.handler, "handler");
        assert_eq!(event.data, json!({"key": 1}));
        assert!(matches!(
            EventRecord::new(0, 0, 0, String::new(), Value::Null),
            Err(Error::Invalid("empty event handler"))
        ));

        let value = chat_box();
        assert_eq!(Record::chat_box(value.clone()), Record::Box(value));
        assert_eq!(Record::event(event.clone()), Record::Event(event));
    }

    #[test]
    fn session_log_and_canonical_reexports_keep_the_established_shapes() {
        let value = chat_box();
        let log = SessionLog {
            boxes: vec![value.clone()],
            events: Vec::new(),
            records: vec![Record::Box(value)],
        };
        assert_eq!(log.boxes[0].id(), BoxId::new(7));
        assert_eq!(ToolCallId::new([3; 12], 9).nonce(), [3; 12]);
        assert_eq!(*TxId::from_bytes([4; 12]).as_bytes(), [4; 12]);
        let session: SessionId = [5; 12];
        assert_eq!(session, [5; 12]);
    }

    #[test]
    fn errors_preserve_display_and_conversion_behavior() {
        let io_error = Error::from(io::Error::other("x"));
        assert_eq!(io_error.to_string(), "I/O error: x");
        assert!(std::error::Error::source(&io_error).is_none());

        let json_error = Error::from(serde_json::from_str::<Value>("{").unwrap_err());
        assert!(json_error.to_string().starts_with("JSON error: "));
        assert!(std::error::Error::source(&json_error).is_none());

        let invalid = Error::Invalid("invalid value");
        assert_eq!(invalid.to_string(), "invalid value");
        assert!(std::error::Error::source(&invalid).is_none());
    }
}