Skip to main content

aver/replay/
session.rs

1use std::collections::BTreeMap;
2
3use super::json::{JsonValue, format_json, parse_json};
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum RecordedOutcome {
7    Value(JsonValue),
8    RuntimeError(String),
9}
10
11#[derive(Debug, Clone, PartialEq)]
12pub struct EffectRecord {
13    pub seq: u32,
14    pub effect_type: String,
15    pub args: Vec<JsonValue>,
16    pub outcome: RecordedOutcome,
17}
18
19#[derive(Debug, Clone, PartialEq)]
20pub struct SessionRecording {
21    pub schema_version: u32,
22    pub request_id: String,
23    pub timestamp: String,
24    pub program_file: String,
25    pub module_root: String,
26    pub entry_fn: String,
27    pub input: JsonValue,
28    pub effects: Vec<EffectRecord>,
29    pub output: RecordedOutcome,
30}
31
32pub fn parse_session_recording(input: &str) -> Result<SessionRecording, String> {
33    let json = parse_json(input)?;
34    session_recording_from_json(&json)
35}
36
37pub fn session_recording_to_string_pretty(recording: &SessionRecording) -> String {
38    format_json(&session_recording_to_json(recording))
39}
40
41pub fn session_recording_to_json(recording: &SessionRecording) -> JsonValue {
42    let mut obj = BTreeMap::new();
43    obj.insert(
44        "schema_version".to_string(),
45        JsonValue::Int(recording.schema_version as i64),
46    );
47    obj.insert(
48        "request_id".to_string(),
49        JsonValue::String(recording.request_id.clone()),
50    );
51    obj.insert(
52        "timestamp".to_string(),
53        JsonValue::String(recording.timestamp.clone()),
54    );
55    obj.insert(
56        "program_file".to_string(),
57        JsonValue::String(recording.program_file.clone()),
58    );
59    obj.insert(
60        "module_root".to_string(),
61        JsonValue::String(recording.module_root.clone()),
62    );
63    obj.insert(
64        "entry_fn".to_string(),
65        JsonValue::String(recording.entry_fn.clone()),
66    );
67    obj.insert("input".to_string(), recording.input.clone());
68
69    let effects = recording
70        .effects
71        .iter()
72        .map(effect_record_to_json)
73        .collect::<Vec<_>>();
74    obj.insert("effects".to_string(), JsonValue::Array(effects));
75    obj.insert("output".to_string(), outcome_to_json(&recording.output));
76
77    JsonValue::Object(obj)
78}
79
80pub fn session_recording_from_json(json: &JsonValue) -> Result<SessionRecording, String> {
81    let obj = expect_object(json, "recording")?;
82
83    let schema_version = match obj.get("schema_version") {
84        Some(v) => parse_u32(v, "recording.schema_version")?,
85        None => 1,
86    };
87
88    let request_id = parse_string(
89        get_required(obj, "request_id", "recording")?,
90        "recording.request_id",
91    )?
92    .to_string();
93    let timestamp = parse_string(
94        get_required(obj, "timestamp", "recording")?,
95        "recording.timestamp",
96    )?
97    .to_string();
98    let program_file = parse_string(
99        get_required(obj, "program_file", "recording")?,
100        "recording.program_file",
101    )?
102    .to_string();
103    let module_root = parse_string(
104        get_required(obj, "module_root", "recording")?,
105        "recording.module_root",
106    )?
107    .to_string();
108    let entry_fn = parse_string(
109        get_required(obj, "entry_fn", "recording")?,
110        "recording.entry_fn",
111    )?
112    .to_string();
113
114    let input = get_required(obj, "input", "recording")?.clone();
115
116    let effects_json = parse_array(
117        get_required(obj, "effects", "recording")?,
118        "recording.effects",
119    )?;
120    let mut effects = Vec::with_capacity(effects_json.len());
121    for (idx, effect_json) in effects_json.iter().enumerate() {
122        let path = format!("recording.effects[{}]", idx);
123        effects.push(effect_record_from_json(effect_json, &path)?);
124    }
125
126    let output = outcome_from_json(
127        get_required(obj, "output", "recording")?,
128        "recording.output",
129    )?;
130
131    Ok(SessionRecording {
132        schema_version,
133        request_id,
134        timestamp,
135        program_file,
136        module_root,
137        entry_fn,
138        input,
139        effects,
140        output,
141    })
142}
143
144fn effect_record_to_json(effect: &EffectRecord) -> JsonValue {
145    let mut obj = BTreeMap::new();
146    obj.insert("seq".to_string(), JsonValue::Int(effect.seq as i64));
147    obj.insert(
148        "type".to_string(),
149        JsonValue::String(effect.effect_type.clone()),
150    );
151    obj.insert("args".to_string(), JsonValue::Array(effect.args.clone()));
152    obj.insert("outcome".to_string(), outcome_to_json(&effect.outcome));
153    JsonValue::Object(obj)
154}
155
156fn effect_record_from_json(json: &JsonValue, path: &str) -> Result<EffectRecord, String> {
157    let obj = expect_object(json, path)?;
158    let seq = parse_u32(get_required(obj, "seq", path)?, &format!("{}.seq", path))?;
159    let effect_type =
160        parse_string(get_required(obj, "type", path)?, &format!("{}.type", path))?.to_string();
161    let args = parse_array(get_required(obj, "args", path)?, &format!("{}.args", path))?.clone();
162    let outcome = outcome_from_json(
163        get_required(obj, "outcome", path)?,
164        &format!("{}.outcome", path),
165    )?;
166
167    Ok(EffectRecord {
168        seq,
169        effect_type,
170        args,
171        outcome,
172    })
173}
174
175fn outcome_to_json(outcome: &RecordedOutcome) -> JsonValue {
176    let mut obj = BTreeMap::new();
177    match outcome {
178        RecordedOutcome::Value(value) => {
179            obj.insert("kind".to_string(), JsonValue::String("value".to_string()));
180            obj.insert("value".to_string(), value.clone());
181        }
182        RecordedOutcome::RuntimeError(message) => {
183            obj.insert(
184                "kind".to_string(),
185                JsonValue::String("runtime_error".to_string()),
186            );
187            obj.insert("message".to_string(), JsonValue::String(message.clone()));
188        }
189    }
190    JsonValue::Object(obj)
191}
192
193fn outcome_from_json(json: &JsonValue, path: &str) -> Result<RecordedOutcome, String> {
194    let obj = expect_object(json, path)?;
195    let kind = parse_string(get_required(obj, "kind", path)?, &format!("{}.kind", path))?;
196
197    match kind {
198        "value" => Ok(RecordedOutcome::Value(
199            get_required(obj, "value", path)?.clone(),
200        )),
201        "runtime_error" => Ok(RecordedOutcome::RuntimeError(
202            parse_string(
203                get_required(obj, "message", path)?,
204                &format!("{}.message", path),
205            )?
206            .to_string(),
207        )),
208        _ => Err(format!("{}: unknown outcome kind '{}'", path, kind)),
209    }
210}
211
212fn get_required<'a>(
213    obj: &'a BTreeMap<String, JsonValue>,
214    key: &str,
215    path: &str,
216) -> Result<&'a JsonValue, String> {
217    obj.get(key)
218        .ok_or_else(|| format!("{}: missing required field '{}'", path, key))
219}
220
221fn expect_object<'a>(
222    value: &'a JsonValue,
223    path: &str,
224) -> Result<&'a BTreeMap<String, JsonValue>, String> {
225    match value {
226        JsonValue::Object(obj) => Ok(obj),
227        _ => Err(format!("{} must be an object", path)),
228    }
229}
230
231fn parse_array<'a>(value: &'a JsonValue, path: &str) -> Result<&'a Vec<JsonValue>, String> {
232    match value {
233        JsonValue::Array(arr) => Ok(arr),
234        _ => Err(format!("{} must be an array", path)),
235    }
236}
237
238fn parse_string<'a>(value: &'a JsonValue, path: &str) -> Result<&'a str, String> {
239    match value {
240        JsonValue::String(s) => Ok(s),
241        _ => Err(format!("{} must be a string", path)),
242    }
243}
244
245fn parse_u32(value: &JsonValue, path: &str) -> Result<u32, String> {
246    match value {
247        JsonValue::Int(n) if *n >= 0 => {
248            u32::try_from(*n).map_err(|_| format!("{} out of range for u32", path))
249        }
250        JsonValue::Float(n) if *n >= 0.0 && n.fract() == 0.0 => {
251            let as_i64 = *n as i64;
252            u32::try_from(as_i64).map_err(|_| format!("{} out of range for u32", path))
253        }
254        _ => Err(format!("{} must be a non-negative integer", path)),
255    }
256}