diff --git a/crates/arc-workflows/src/cli/parse.rs b/crates/arc-workflows/src/cli/parse.rs index 37becd18d..5839abefc 100644 --- a/crates/arc-workflows/src/cli/parse.rs +++ b/crates/arc-workflows/src/cli/parse.rs @@ -1,5 +1,4 @@ -use crate::error::ArcError; -use crate::parser::{grammar, lexer}; +use std::io::Write; use super::{read_dot_file, ParseArgs}; @@ -9,21 +8,15 @@ use super::{read_dot_file, ParseArgs}; /// /// Returns an error if the file cannot be read, parsed, or contains trailing content. pub fn parse_command(args: &ParseArgs) -> anyhow::Result<()> { + let stdout = std::io::stdout(); + parse_command_to(args, stdout.lock()) +} + +fn parse_command_to(args: &ParseArgs, mut out: impl Write) -> anyhow::Result<()> { let source = read_dot_file(&args.workflow)?; - let stripped = lexer::strip_comments(&source); - let (rest, ast) = grammar::parse_dot_graph(&stripped) - .map_err(|e| ArcError::Parse(format!("grammar error: {e}")))?; - - let remaining = rest.trim(); - if !remaining.is_empty() { - return Err(ArcError::Parse(format!( - "unexpected trailing content: {:?}", - &remaining[..remaining.len().min(50)] - )) - .into()); - } - - println!("{}", serde_json::to_string_pretty(&ast)?); + let ast = crate::parser::parse_ast(&source)?; + serde_json::to_writer_pretty(&mut out, &ast)?; + writeln!(out)?; Ok(()) } @@ -50,16 +43,10 @@ mod tests { let args = ParseArgs { workflow: tmp.path().to_path_buf(), }; - let result = parse_command(&args); - assert!(result.is_ok(), "expected Ok but got: {result:?}"); - - // Re-run and capture stdout by reading the file through the parser directly - let source = std::fs::read_to_string(tmp.path()).unwrap(); - let stripped = lexer::strip_comments(&source); - let (_, ast) = grammar::parse_dot_graph(&stripped).unwrap(); - let json = serde_json::to_string_pretty(&ast).unwrap(); - let deserialized: DotGraph = serde_json::from_str(&json).unwrap(); + let mut buf = Vec::new(); + parse_command_to(&args, &mut buf).unwrap(); + let deserialized: DotGraph = serde_json::from_slice(&buf).unwrap(); assert_eq!(deserialized.name, "Hello"); assert_eq!(deserialized.statements.len(), 3); } @@ -72,7 +59,7 @@ mod tests { let args = ParseArgs { workflow: tmp.path().to_path_buf(), }; - let result = parse_command(&args); + let result = parse_command_to(&args, Vec::new()); assert!(result.is_err(), "expected Err for invalid syntax"); } @@ -81,7 +68,7 @@ mod tests { let args = ParseArgs { workflow: PathBuf::from("/tmp/nonexistent_parse_test_12345.dot"), }; - let result = parse_command(&args); + let result = parse_command_to(&args, Vec::new()); assert!(result.is_err(), "expected Err for missing file"); } } diff --git a/crates/arc-workflows/src/parser/mod.rs b/crates/arc-workflows/src/parser/mod.rs index 0bfab2c1c..543524022 100644 --- a/crates/arc-workflows/src/parser/mod.rs +++ b/crates/arc-workflows/src/parser/mod.rs @@ -6,16 +6,18 @@ pub mod semantic; use crate::error::ArcError; use crate::graph::types::Graph; -/// Parse a DOT source string into a semantic `Graph`. +use self::ast::DotGraph; + +/// Parse a DOT source string into a raw `DotGraph` AST. /// -/// Strips comments, parses the grammar, and performs semantic transformation -/// (expanding chained edges, applying defaults, flattening subgraphs). +/// Strips comments, parses the grammar, and validates there is no +/// trailing content. Does NOT perform semantic transformation. /// /// # Errors /// /// Returns an error if the input is not valid DOT syntax or contains /// trailing content after the graph definition. -pub fn parse(input: &str) -> Result { +pub fn parse_ast(input: &str) -> Result { let stripped = lexer::strip_comments(input); let (rest, dot_graph) = grammar::parse_dot_graph(&stripped) .map_err(|e| ArcError::Parse(format!("grammar error: {e}")))?; @@ -28,6 +30,20 @@ pub fn parse(input: &str) -> Result { ))); } + Ok(dot_graph) +} + +/// Parse a DOT source string into a semantic `Graph`. +/// +/// Strips comments, parses the grammar, and performs semantic transformation +/// (expanding chained edges, applying defaults, flattening subgraphs). +/// +/// # Errors +/// +/// Returns an error if the input is not valid DOT syntax or contains +/// trailing content after the graph definition. +pub fn parse(input: &str) -> Result { + let dot_graph = parse_ast(input)?; semantic::ast_to_graph(&dot_graph) }