1use car_ir::json_extract::{extract_json_object, repair_json};
8use car_workflow::Workflow;
9
10pub fn parse_workflow(text: &str) -> Result<Workflow, String> {
12 let trimmed = text.trim();
13
14 if let Ok(wf) = serde_json::from_str::<Workflow>(trimmed) {
16 return Ok(wf);
17 }
18
19 if let Some(json) = extract_json_object(trimmed) {
21 if let Ok(wf) = serde_json::from_str::<Workflow>(&json) {
22 return Ok(wf);
23 }
24 let repaired = repair_json(&json);
25 return serde_json::from_str::<Workflow>(&repaired).map_err(|e| {
28 format!("extracted a JSON object but it is not a valid workflow: {e}")
29 });
30 }
31
32 Err(format!(
33 "no JSON workflow object found in output ({} chars)",
34 text.len()
35 ))
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41
42 const MINIMAL: &str = r#"{
43 "id": "wf1",
44 "name": "Test",
45 "start": "a",
46 "stages": [
47 {"id": "a", "name": "A", "step": {"type": "approval", "prompt": "ok?", "fields": [], "output_key": "decision"}}
48 ],
49 "edges": []
50 }"#;
51
52 #[test]
53 fn parses_clean_json() {
54 let wf = parse_workflow(MINIMAL).unwrap();
55 assert_eq!(wf.id, "wf1");
56 assert_eq!(wf.start, "a");
57 assert_eq!(wf.stages.len(), 1);
58 }
59
60 #[test]
61 fn parses_markdown_fenced() {
62 let text = format!("Here is the workflow:\n\n```json\n{MINIMAL}\n```\n\nDone.");
63 let wf = parse_workflow(&text).unwrap();
64 assert_eq!(wf.id, "wf1");
65 }
66
67 #[test]
68 fn parses_with_preamble() {
69 let text = format!("Sure! {MINIMAL}");
70 let wf = parse_workflow(&text).unwrap();
71 assert_eq!(wf.id, "wf1");
72 }
73
74 #[test]
75 fn garbage_fails() {
76 assert!(parse_workflow("not json at all").is_err());
77 }
78
79 #[test]
80 fn json_object_that_isnt_a_workflow_fails() {
81 let err = parse_workflow(r#"{"hello": "world"}"#).unwrap_err();
82 assert!(err.contains("not a valid workflow"));
83 }
84}