ryo_storage/storage/
format.rs1use crate::txlog::TxLog;
13use std::fs::File;
14use std::io::BufReader;
15use thiserror::Error;
16
17#[derive(Debug, Error)]
19pub enum FormatError {
20 #[error("JSON error: {0}")]
23 Json(#[from] serde_json::Error),
24
25 #[error("IO error: {0}")]
27 Io(#[from] std::io::Error),
28
29 #[error("Unknown format: {0}")]
32 UnknownFormat(String),
33
34 #[error("Format mismatch: expected {expected}, got {actual}")]
37 FormatMismatch {
38 expected: String,
40 actual: String,
42 },
43}
44
45pub type FormatResult<T> = Result<T, FormatError>;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
50pub enum Format {
51 #[default]
53 Json,
54 JsonCompact,
56}
57
58impl Format {
59 pub fn extension(&self) -> &'static str {
61 match self {
62 Format::Json => "json",
63 Format::JsonCompact => "json",
64 }
65 }
66
67 pub fn name(&self) -> &'static str {
69 match self {
70 Format::Json => "JSON (pretty)",
71 Format::JsonCompact => "JSON (compact)",
72 }
73 }
74
75 pub fn is_binary(&self) -> bool {
77 match self {
78 Format::Json | Format::JsonCompact => false,
79 }
80 }
81
82 pub fn from_extension(ext: &str) -> Option<Self> {
84 match ext.to_lowercase().as_str() {
85 "json" => Some(Format::Json),
86 _ => None,
87 }
88 }
89}
90
91pub trait SessionFormat: Send + Sync {
95 fn format(&self) -> Format;
97
98 fn serialize(&self, log: &TxLog) -> FormatResult<Vec<u8>>;
100
101 fn deserialize(&self, data: &[u8]) -> FormatResult<TxLog>;
103
104 fn serialize_to_file(&self, log: &TxLog, file: File) -> FormatResult<()>;
106
107 fn deserialize_from_reader(&self, reader: BufReader<File>) -> FormatResult<TxLog>;
109}
110
111#[derive(Debug, Clone, Copy, Default)]
113pub struct JsonFormat {
114 compact: bool,
115}
116
117impl JsonFormat {
118 pub fn new() -> Self {
120 Self { compact: false }
121 }
122
123 pub fn compact() -> Self {
125 Self { compact: true }
126 }
127}
128
129impl SessionFormat for JsonFormat {
130 fn format(&self) -> Format {
131 if self.compact {
132 Format::JsonCompact
133 } else {
134 Format::Json
135 }
136 }
137
138 fn serialize(&self, log: &TxLog) -> FormatResult<Vec<u8>> {
139 let json = if self.compact {
140 serde_json::to_vec(log)?
141 } else {
142 serde_json::to_vec_pretty(log)?
143 };
144 Ok(json)
145 }
146
147 fn deserialize(&self, data: &[u8]) -> FormatResult<TxLog> {
148 let log = serde_json::from_slice(data)?;
149 Ok(log)
150 }
151
152 fn serialize_to_file(&self, log: &TxLog, file: File) -> FormatResult<()> {
153 if self.compact {
154 serde_json::to_writer(file, log)?;
155 } else {
156 serde_json::to_writer_pretty(file, log)?;
157 }
158 Ok(())
159 }
160
161 fn deserialize_from_reader(&self, reader: BufReader<File>) -> FormatResult<TxLog> {
162 let log = serde_json::from_reader(reader)?;
163 Ok(log)
164 }
165}
166
167pub fn get_serializer(format: Format) -> Box<dyn SessionFormat> {
169 match format {
170 Format::Json => Box::new(JsonFormat::new()),
171 Format::JsonCompact => Box::new(JsonFormat::compact()),
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::txlog::{TxAction, TxLog};
179
180 fn create_test_log() -> TxLog {
181 let mut log = TxLog::with_project("/test/project");
182 log.log(TxAction::GoalSet {
183 query: "test query".to_string(),
184 intent_type: "test".to_string(),
185 confidence: 0.9,
186 });
187 log.log(TxAction::MutationApplied {
188 mutation_type: "Rename".to_string(),
189 target: "foo -> bar".to_string(),
190 changes: 3,
191 mutation_data: None,
192 file_path: None,
193 pre_state: None,
194 post_state: None,
195 affected_symbols: vec![],
196 });
197 log
198 }
199
200 #[test]
201 fn test_json_roundtrip() {
202 let log = create_test_log();
203 let format = JsonFormat::new();
204
205 let bytes = format.serialize(&log).unwrap();
206 let restored = format.deserialize(&bytes).unwrap();
207
208 assert_eq!(log.entries().len(), restored.entries().len());
209 }
210
211 #[test]
212 fn test_json_compact_roundtrip() {
213 let log = create_test_log();
214 let format = JsonFormat::compact();
215
216 let bytes = format.serialize(&log).unwrap();
217 let restored = format.deserialize(&bytes).unwrap();
218
219 assert_eq!(log.entries().len(), restored.entries().len());
220
221 let pretty_bytes = JsonFormat::new().serialize(&log).unwrap();
223 assert!(bytes.len() < pretty_bytes.len());
224 }
225
226 #[test]
227 fn test_format_extension() {
228 assert_eq!(Format::Json.extension(), "json");
229 assert_eq!(Format::JsonCompact.extension(), "json");
230 }
231
232 #[test]
233 fn test_format_from_extension() {
234 assert_eq!(Format::from_extension("json"), Some(Format::Json));
235 assert_eq!(Format::from_extension("JSON"), Some(Format::Json));
236 assert_eq!(Format::from_extension("unknown"), None);
237 }
238
239 #[test]
240 fn test_format_is_binary() {
241 assert!(!Format::Json.is_binary());
242 assert!(!Format::JsonCompact.is_binary());
243 }
244}