1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value as SerdeJsonValue;
5
6use super::json::JsonValue;
7
8#[derive(Debug, Clone, PartialEq)]
9pub enum RecordedOutcome {
10 Value(JsonValue),
11 RuntimeError(String),
12}
13
14#[derive(Debug, Clone, PartialEq)]
15pub struct EffectRecord {
16 pub seq: u32,
17 pub effect_type: String,
18 pub args: Vec<JsonValue>,
19 pub outcome: RecordedOutcome,
20 pub caller_fn: String,
22 pub source_line: usize,
24 pub group_id: Option<u32>,
28 pub branch_path: Option<String>,
31 pub effect_occurrence: Option<u32>,
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub struct SessionRecording {
38 pub schema_version: u32,
39 pub request_id: String,
40 pub timestamp: String,
41 pub program_file: String,
42 pub module_root: String,
43 pub entry_fn: String,
44 pub input: JsonValue,
45 pub effects: Vec<EffectRecord>,
46 pub output: RecordedOutcome,
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50struct SerdeSessionRecording {
51 schema_version: u32,
52 request_id: String,
53 timestamp: String,
54 program_file: String,
55 module_root: String,
56 entry_fn: String,
57 input: SerdeJsonValue,
58 effects: Vec<SerdeEffectRecord>,
59 output: SerdeRecordedOutcome,
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63struct SerdeEffectRecord {
64 seq: u32,
65 #[serde(rename = "type")]
66 effect_type: String,
67 args: Vec<SerdeJsonValue>,
68 outcome: SerdeRecordedOutcome,
69 #[serde(default, skip_serializing_if = "String::is_empty")]
70 caller_fn: String,
71 #[serde(default, skip_serializing_if = "is_zero")]
72 source_line: usize,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 group_id: Option<u32>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 branch_path: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 effect_occurrence: Option<u32>,
79}
80
81fn is_zero(v: &usize) -> bool {
82 *v == 0
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86#[serde(tag = "kind")]
87enum SerdeRecordedOutcome {
88 #[serde(rename = "value")]
89 Value { value: SerdeJsonValue },
90 #[serde(rename = "runtime_error")]
91 RuntimeError { message: String },
92}
93
94pub fn parse_session_recording(input: &str) -> Result<SessionRecording, String> {
95 let recording: SerdeSessionRecording =
96 serde_json::from_str(input).map_err(|e| format!("invalid replay recording: {}", e))?;
97 session_recording_from_serde(recording)
98}
99
100pub fn session_recording_to_string_pretty(recording: &SessionRecording) -> String {
101 serde_json::to_string_pretty(&session_recording_to_serde(recording))
102 .expect("SessionRecording should always serialize to JSON")
103}
104
105pub fn session_recording_to_json(recording: &SessionRecording) -> JsonValue {
106 let value = serde_json::to_value(session_recording_to_serde(recording))
107 .expect("SessionRecording should always convert to serde_json::Value");
108 serde_to_json_value(value).expect("serde_json::Value should always convert to JsonValue")
109}
110
111pub fn session_recording_from_json(json: &JsonValue) -> Result<SessionRecording, String> {
112 let value = json_value_to_serde(json);
113 let recording: SerdeSessionRecording =
114 serde_json::from_value(value).map_err(|e| format!("invalid replay recording: {}", e))?;
115 session_recording_from_serde(recording)
116}
117
118fn session_recording_to_serde(recording: &SessionRecording) -> SerdeSessionRecording {
119 SerdeSessionRecording {
120 schema_version: recording.schema_version,
121 request_id: recording.request_id.clone(),
122 timestamp: recording.timestamp.clone(),
123 program_file: recording.program_file.clone(),
124 module_root: recording.module_root.clone(),
125 entry_fn: recording.entry_fn.clone(),
126 input: json_value_to_serde(&recording.input),
127 effects: recording
128 .effects
129 .iter()
130 .map(effect_record_to_serde)
131 .collect(),
132 output: outcome_to_serde(&recording.output),
133 }
134}
135
136fn session_recording_from_serde(
137 recording: SerdeSessionRecording,
138) -> Result<SessionRecording, String> {
139 Ok(SessionRecording {
140 schema_version: recording.schema_version,
141 request_id: recording.request_id,
142 timestamp: recording.timestamp,
143 program_file: recording.program_file,
144 module_root: recording.module_root,
145 entry_fn: recording.entry_fn,
146 input: serde_to_json_value(recording.input)?,
147 effects: recording
148 .effects
149 .into_iter()
150 .map(effect_record_from_serde)
151 .collect::<Result<Vec<_>, _>>()?,
152 output: outcome_from_serde(recording.output)?,
153 })
154}
155
156fn effect_record_to_serde(effect: &EffectRecord) -> SerdeEffectRecord {
157 SerdeEffectRecord {
158 seq: effect.seq,
159 effect_type: effect.effect_type.clone(),
160 args: effect.args.iter().map(json_value_to_serde).collect(),
161 outcome: outcome_to_serde(&effect.outcome),
162 caller_fn: effect.caller_fn.clone(),
163 source_line: effect.source_line,
164 group_id: effect.group_id,
165 branch_path: effect.branch_path.clone(),
166 effect_occurrence: effect.effect_occurrence,
167 }
168}
169
170fn effect_record_from_serde(effect: SerdeEffectRecord) -> Result<EffectRecord, String> {
171 Ok(EffectRecord {
172 seq: effect.seq,
173 effect_type: effect.effect_type,
174 args: effect
175 .args
176 .into_iter()
177 .map(serde_to_json_value)
178 .collect::<Result<Vec<_>, _>>()?,
179 outcome: outcome_from_serde(effect.outcome)?,
180 caller_fn: effect.caller_fn,
181 source_line: effect.source_line,
182 group_id: effect.group_id,
183 branch_path: effect.branch_path,
184 effect_occurrence: effect.effect_occurrence,
185 })
186}
187
188fn outcome_to_serde(outcome: &RecordedOutcome) -> SerdeRecordedOutcome {
189 match outcome {
190 RecordedOutcome::Value(value) => SerdeRecordedOutcome::Value {
191 value: json_value_to_serde(value),
192 },
193 RecordedOutcome::RuntimeError(message) => SerdeRecordedOutcome::RuntimeError {
194 message: message.clone(),
195 },
196 }
197}
198
199fn outcome_from_serde(outcome: SerdeRecordedOutcome) -> Result<RecordedOutcome, String> {
200 match outcome {
201 SerdeRecordedOutcome::Value { value } => {
202 Ok(RecordedOutcome::Value(serde_to_json_value(value)?))
203 }
204 SerdeRecordedOutcome::RuntimeError { message } => {
205 Ok(RecordedOutcome::RuntimeError(message))
206 }
207 }
208}
209
210fn json_value_to_serde(value: &JsonValue) -> SerdeJsonValue {
211 match value {
212 JsonValue::Null => SerdeJsonValue::Null,
213 JsonValue::Bool(b) => SerdeJsonValue::Bool(*b),
214 JsonValue::Int(i) => SerdeJsonValue::Number((*i).into()),
215 JsonValue::Float(f) => SerdeJsonValue::Number(
216 serde_json::Number::from_f64(*f).expect("replay JSON cannot encode non-finite floats"),
217 ),
218 JsonValue::String(s) => SerdeJsonValue::String(s.clone()),
219 JsonValue::Array(items) => {
220 SerdeJsonValue::Array(items.iter().map(json_value_to_serde).collect())
221 }
222 JsonValue::Object(obj) => SerdeJsonValue::Object(
223 obj.iter()
224 .map(|(k, v)| (k.clone(), json_value_to_serde(v)))
225 .collect(),
226 ),
227 }
228}
229
230fn serde_to_json_value(value: SerdeJsonValue) -> Result<JsonValue, String> {
231 match value {
232 SerdeJsonValue::Null => Ok(JsonValue::Null),
233 SerdeJsonValue::Bool(b) => Ok(JsonValue::Bool(b)),
234 SerdeJsonValue::Number(n) => {
235 if let Some(i) = n.as_i64() {
236 Ok(JsonValue::Int(i))
237 } else if let Some(u) = n.as_u64() {
238 let i = i64::try_from(u)
239 .map_err(|_| format!("JSON integer {} is out of range for i64", u))?;
240 Ok(JsonValue::Int(i))
241 } else if let Some(f) = n.as_f64() {
242 Ok(JsonValue::Float(f))
243 } else {
244 Err(format!("unsupported JSON number: {}", n))
245 }
246 }
247 SerdeJsonValue::String(s) => Ok(JsonValue::String(s)),
248 SerdeJsonValue::Array(items) => Ok(JsonValue::Array(
249 items
250 .into_iter()
251 .map(serde_to_json_value)
252 .collect::<Result<Vec<_>, _>>()?,
253 )),
254 SerdeJsonValue::Object(obj) => {
255 let mut out = BTreeMap::new();
256 for (key, value) in obj {
257 out.insert(key, serde_to_json_value(value)?);
258 }
259 Ok(JsonValue::Object(out))
260 }
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn session_recording_json_shape_stays_stable() {
270 let recording = SessionRecording {
271 schema_version: 1,
272 request_id: "rec-1".to_string(),
273 timestamp: "unix-1".to_string(),
274 program_file: "examples/app.av".to_string(),
275 module_root: ".".to_string(),
276 entry_fn: "main".to_string(),
277 input: JsonValue::Null,
278 effects: vec![EffectRecord {
279 seq: 1,
280 effect_type: "Console.print".to_string(),
281 args: vec![JsonValue::String("hi".to_string())],
282 outcome: RecordedOutcome::Value(JsonValue::Null),
283 caller_fn: String::new(),
284 source_line: 0,
285 group_id: None,
286 branch_path: None,
287 effect_occurrence: None,
288 }],
289 output: RecordedOutcome::RuntimeError("boom".to_string()),
290 };
291
292 let json = session_recording_to_json(&recording);
293 let JsonValue::Object(root) = json else {
294 panic!("recording should serialize as object");
295 };
296 let JsonValue::Array(effects) = root.get("effects").expect("effects field") else {
297 panic!("effects should be array");
298 };
299 let JsonValue::Object(effect) = &effects[0] else {
300 panic!("effect should be object");
301 };
302 let JsonValue::String(effect_type) = effect.get("type").expect("type field") else {
303 panic!("type should be string");
304 };
305 assert_eq!(effect_type, "Console.print");
306
307 let JsonValue::Object(output) = root.get("output").expect("output field") else {
308 panic!("output should be object");
309 };
310 let JsonValue::String(kind) = output.get("kind").expect("kind field") else {
311 panic!("kind should be string");
312 };
313 assert_eq!(kind, "runtime_error");
314 }
315
316 #[test]
317 fn parse_session_recording_roundtrips_existing_shape() {
318 let raw = r#"{
319 "schema_version": 1,
320 "request_id": "rec-1",
321 "timestamp": "unix-1",
322 "program_file": "examples/app.av",
323 "module_root": ".",
324 "entry_fn": "main",
325 "input": null,
326 "effects": [
327 {
328 "seq": 1,
329 "type": "Console.print",
330 "args": ["hi"],
331 "outcome": { "kind": "value", "value": null }
332 }
333 ],
334 "output": { "kind": "value", "value": {"ok": true} }
335}"#;
336
337 let recording = parse_session_recording(raw).expect("parse recording");
338 assert_eq!(recording.effects[0].effect_type, "Console.print");
339 assert_eq!(
340 recording.effects[0].args,
341 vec![JsonValue::String("hi".to_string())]
342 );
343 assert_eq!(
344 recording.output,
345 RecordedOutcome::Value(JsonValue::Object(BTreeMap::from([(
346 "ok".to_string(),
347 JsonValue::Bool(true),
348 )])))
349 );
350 }
351}