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}