kcode_k1_chat_persistence_values/
lib.rs1#![forbid(unsafe_code)]
2
3pub use kcode_k1_chat_boxes::{BoxId, ChatBox, ToolCallId};
4pub use kcode_k1_transaction_id::TxId;
5use serde_json::Value;
6use std::io;
7
8pub type SessionId = [u8; 12];
9type Result<T, E = Error> = std::result::Result<T, E>;
10
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct EventRecord {
13 pub after_box_id: u64,
14 pub event_index: u64,
15 pub connected_box_id: u64,
16 pub handler: String,
17 pub data: Value,
18}
19
20impl EventRecord {
21 pub fn new(
22 after_box_id: u64,
23 event_index: u64,
24 connected_box_id: u64,
25 handler: String,
26 data: Value,
27 ) -> Result<Self> {
28 if handler.is_empty() {
29 return Err(Error::Invalid("empty event handler"));
30 }
31 Ok(Self {
32 after_box_id,
33 event_index,
34 connected_box_id,
35 handler,
36 data,
37 })
38 }
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum Record {
43 Box(ChatBox),
44 Event(EventRecord),
45}
46
47impl Record {
48 pub fn chat_box(value: ChatBox) -> Self {
49 Self::Box(value)
50 }
51
52 pub fn event(value: EventRecord) -> Self {
53 Self::Event(value)
54 }
55}
56
57#[derive(Clone, Debug, Default, PartialEq, Eq)]
58pub struct SessionLog {
59 pub boxes: Vec<ChatBox>,
60 pub events: Vec<EventRecord>,
61 pub records: Vec<Record>,
62}
63
64#[derive(Debug)]
65pub enum Error {
66 Io(io::Error),
67 Json(serde_json::Error),
68 Invalid(&'static str),
69}
70
71impl std::fmt::Display for Error {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match self {
74 Self::Io(error) => write!(f, "I/O error: {error}"),
75 Self::Json(error) => write!(f, "JSON error: {error}"),
76 Self::Invalid(error) => f.write_str(error),
77 }
78 }
79}
80
81impl std::error::Error for Error {}
82
83impl From<io::Error> for Error {
84 fn from(value: io::Error) -> Self {
85 Self::Io(value)
86 }
87}
88
89impl From<serde_json::Error> for Error {
90 fn from(value: serde_json::Error) -> Self {
91 Self::Json(value)
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use serde_json::json;
99
100 fn chat_box() -> ChatBox {
101 ChatBox::new(
102 BoxId::new(7),
103 "Future Kind".into(),
104 "opaque".into(),
105 "future/v1".into(),
106 "hidden".into(),
107 )
108 }
109
110 #[test]
111 fn constructors_preserve_values_and_reject_only_empty_handlers() {
112 let event = EventRecord::new(7, 2, 6, "handler".into(), json!({"key": 1})).unwrap();
113 assert_eq!(event.after_box_id, 7);
114 assert_eq!(event.event_index, 2);
115 assert_eq!(event.connected_box_id, 6);
116 assert_eq!(event.handler, "handler");
117 assert_eq!(event.data, json!({"key": 1}));
118 assert!(matches!(
119 EventRecord::new(0, 0, 0, String::new(), Value::Null),
120 Err(Error::Invalid("empty event handler"))
121 ));
122
123 let value = chat_box();
124 assert_eq!(Record::chat_box(value.clone()), Record::Box(value));
125 assert_eq!(Record::event(event.clone()), Record::Event(event));
126 }
127
128 #[test]
129 fn session_log_and_canonical_reexports_keep_the_established_shapes() {
130 let value = chat_box();
131 let log = SessionLog {
132 boxes: vec![value.clone()],
133 events: Vec::new(),
134 records: vec![Record::Box(value)],
135 };
136 assert_eq!(log.boxes[0].id(), BoxId::new(7));
137 assert_eq!(ToolCallId::new([3; 12], 9).nonce(), [3; 12]);
138 assert_eq!(*TxId::from_bytes([4; 12]).as_bytes(), [4; 12]);
139 let session: SessionId = [5; 12];
140 assert_eq!(session, [5; 12]);
141 }
142
143 #[test]
144 fn errors_preserve_display_and_conversion_behavior() {
145 let io_error = Error::from(io::Error::other("x"));
146 assert_eq!(io_error.to_string(), "I/O error: x");
147 assert!(std::error::Error::source(&io_error).is_none());
148
149 let json_error = Error::from(serde_json::from_str::<Value>("{").unwrap_err());
150 assert!(json_error.to_string().starts_with("JSON error: "));
151 assert!(std::error::Error::source(&json_error).is_none());
152
153 let invalid = Error::Invalid("invalid value");
154 assert_eq!(invalid.to_string(), "invalid value");
155 assert!(std::error::Error::source(&invalid).is_none());
156 }
157}