#![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());
}
}