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    /// Name of the function that made this effect call (empty = unknown).
21    pub caller_fn: String,
22    /// Source line of the call expression that triggered this effect (0 = unknown).
23    pub source_line: usize,
24    /// Independent product group: effects sharing a `group_id` are order-independent.
25    /// During replay, effects within a group are matched by
26    /// (branch_path, effect_occurrence, type, args).
27    pub group_id: Option<u32>,
28    /// Branch path within nested independent products (e.g. "0.1" = branch 0 of outer,
29    /// branch 1 of inner). Disambiguates effects from different branches.
30    pub branch_path: Option<String>,
31    /// Per-branch ordinal of effect emission (0-based). Disambiguates multiple
32    /// emissions of the same effect type+args within a single branch.
33    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}