Skip to main content

aver/replay/
session.rs

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}