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