mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-05 08:10:39 +00:00
Add arc parse FILE.dot subcommand to print raw AST as JSON
Parses a DOT file and outputs its AST as pretty-printed JSON, useful for debugging and tooling. Adds Serialize/Deserialize to all AST types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
872107e4d6
commit
57dd00a1a5
4 changed files with 108 additions and 6 deletions
|
|
@ -38,6 +38,8 @@ enum Command {
|
|||
},
|
||||
/// Validate a workflow
|
||||
Validate(arc_workflows::cli::ValidateArgs),
|
||||
/// Parse a DOT file and print its AST
|
||||
Parse(arc_workflows::cli::ParseArgs),
|
||||
/// List and test LLM models
|
||||
Models {
|
||||
#[command(subcommand)]
|
||||
|
|
@ -98,6 +100,7 @@ async fn main() -> Result<()> {
|
|||
Command::Agent(_) => "agent",
|
||||
Command::Run { .. } => "run",
|
||||
Command::Validate(_) => "validate",
|
||||
Command::Parse(_) => "parse",
|
||||
Command::Models { .. } => "models",
|
||||
Command::Serve(_) => "serve",
|
||||
Command::Doctor { .. } => "doctor",
|
||||
|
|
@ -154,6 +157,9 @@ async fn main() -> Result<()> {
|
|||
let styles = arc_util::terminal::Styles::detect_stderr();
|
||||
arc_workflows::cli::validate::validate_command(&args, &styles)?;
|
||||
}
|
||||
Command::Parse(args) => {
|
||||
arc_workflows::cli::parse::parse_command(&args)?;
|
||||
}
|
||||
Command::Models { command } => arc_llm::cli::run_models(command).await?,
|
||||
Command::Serve(args) => {
|
||||
let styles: &'static arc_util::terminal::Styles =
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod backend;
|
||||
pub mod cli_backend;
|
||||
pub mod parse;
|
||||
pub mod progress;
|
||||
pub mod run;
|
||||
pub mod run_config;
|
||||
|
|
@ -140,6 +141,12 @@ pub struct ValidateArgs {
|
|||
pub workflow: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct ParseArgs {
|
||||
/// Path to the .dot workflow file
|
||||
pub workflow: PathBuf,
|
||||
}
|
||||
|
||||
/// Read a .dot file from disk.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
|
|||
87
crates/arc-workflows/src/cli/parse.rs
Normal file
87
crates/arc-workflows/src/cli/parse.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
use crate::error::ArcError;
|
||||
use crate::parser::{grammar, lexer};
|
||||
|
||||
use super::{read_dot_file, ParseArgs};
|
||||
|
||||
/// Parse a DOT file and print its raw AST as JSON.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the file cannot be read, parsed, or contains trailing content.
|
||||
pub fn parse_command(args: &ParseArgs) -> 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)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::parser::ast::DotGraph;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn parse_command_outputs_json_ast() {
|
||||
let mut tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
write!(
|
||||
tmp,
|
||||
r#"digraph Hello {{
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
start -> exit
|
||||
}}"#
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
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();
|
||||
|
||||
assert_eq!(deserialized.name, "Hello");
|
||||
assert_eq!(deserialized.statements.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_command_rejects_invalid_dot() {
|
||||
let mut tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
write!(tmp, "not a valid dot file").unwrap();
|
||||
|
||||
let args = ParseArgs {
|
||||
workflow: tmp.path().to_path_buf(),
|
||||
};
|
||||
let result = parse_command(&args);
|
||||
assert!(result.is_err(), "expected Err for invalid syntax");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_command_rejects_missing_file() {
|
||||
let args = ParseArgs {
|
||||
workflow: PathBuf::from("/tmp/nonexistent_parse_test_12345.dot"),
|
||||
};
|
||||
let result = parse_command(&args);
|
||||
assert!(result.is_err(), "expected Err for missing file");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A parsed DOT value before semantic interpretation.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum AstValue {
|
||||
Str(String),
|
||||
Int(i64),
|
||||
|
|
@ -13,14 +15,14 @@ pub enum AstValue {
|
|||
pub type AttrBlock = Vec<(String, AstValue)>;
|
||||
|
||||
/// A node statement: `id [attrs]?`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NodeStmt {
|
||||
pub id: String,
|
||||
pub attrs: Option<AttrBlock>,
|
||||
}
|
||||
|
||||
/// An edge statement: `A -> B -> C [attrs]?`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EdgeStmt {
|
||||
/// Chain of node IDs (at least 2).
|
||||
pub nodes: Vec<String>,
|
||||
|
|
@ -28,14 +30,14 @@ pub struct EdgeStmt {
|
|||
}
|
||||
|
||||
/// A subgraph statement: `subgraph name? { stmts }`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SubgraphStmt {
|
||||
pub name: Option<String>,
|
||||
pub statements: Vec<Statement>,
|
||||
}
|
||||
|
||||
/// A single statement in a DOT graph body.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Statement {
|
||||
/// `graph [attrs]`
|
||||
GraphAttr(AttrBlock),
|
||||
|
|
@ -54,7 +56,7 @@ pub enum Statement {
|
|||
}
|
||||
|
||||
/// The top-level parsed DOT graph.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DotGraph {
|
||||
pub name: String,
|
||||
pub statements: Vec<Statement>,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue