Extract parse_ast() and use Write sink in parse_command

- Extract shared strip+parse+trailing-check into parser::parse_ast()
  so both parser::parse() and parse_command reuse it
- Accept impl Write in parse_command so tests verify actual JSON output
  instead of re-parsing independently

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-05 16:19:34 -05:00
parent 57dd00a1a5
commit dc348d553e
2 changed files with 34 additions and 31 deletions

View file

@ -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");
}
}

View file

@ -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<Graph, ArcError> {
pub fn parse_ast(input: &str) -> Result<DotGraph, ArcError> {
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<Graph, ArcError> {
)));
}
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<Graph, ArcError> {
let dot_graph = parse_ast(input)?;
semantic::ast_to_graph(&dot_graph)
}