use crate::core::graph::WorkflowGraph;
use crate::core::jwl_lexer::Lexer;
use crate::core::jwl_parser::JwlParser;
use anyhow::{anyhow, Result};
use std::collections::HashMap;
pub struct GraphParser;
impl GraphParser {
pub fn parse_arguments_str(args_str: &str) -> HashMap<String, String> {
let mut params = HashMap::new();
let mut buffer = String::new();
let mut key = String::new();
let mut depth = 0;
let mut in_quote = false;
let mut parsing_key = true;
let chars: Vec<char> = args_str.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
let c = chars[i];
match c {
'=' if depth == 0 && !in_quote && parsing_key => {
key = buffer.trim().to_string();
buffer.clear();
parsing_key = false;
}
',' if depth == 0 && !in_quote => {
if !key.is_empty() {
params.insert(key.clone(), buffer.trim().to_string());
}
buffer.clear();
key.clear();
parsing_key = true;
}
'"' if depth == 0 => {
in_quote = !in_quote;
buffer.push(c);
}
'\\' if in_quote && i + 1 < len && chars[i + 1] == '"' => {
buffer.push(c);
buffer.push(chars[i + 1]);
i += 1; }
'(' | '{' | '[' if !in_quote => {
depth += 1;
buffer.push(c);
}
')' | '}' | ']' if !in_quote => {
if depth > 0 {
depth -= 1;
}
buffer.push(c);
}
_ => {
buffer.push(c);
}
}
i += 1;
}
if !key.is_empty() {
params.insert(key, buffer.trim().to_string());
}
params
}
pub fn parse(content: &str) -> Result<WorkflowGraph> {
Self::parse_rdp(content)
}
pub fn parse_manifest(content: &str) -> Result<crate::core::graph::Manifest> {
crate::core::manifest_parser::ManifestParser::parse(content)
}
pub fn parse_lib(content: &str) -> Result<WorkflowGraph> {
Self::parse_lib_rdp(content)
}
fn parse_rdp(content: &str) -> Result<WorkflowGraph> {
let tokens = Lexer::new(content)
.tokenize()
.map_err(|e| anyhow!("JWL Compilation Syntax Error:\n{}", e))?;
let mut parser = JwlParser::new(&tokens, content);
let mut wf = parser.parse_workflow()?;
if wf.entry_node.is_empty() {
let entry_idx = wf
.graph
.node_indices()
.find(|&idx| {
wf.graph
.neighbors_directed(idx, petgraph::Direction::Incoming)
.next()
.is_none()
})
.or_else(|| wf.graph.node_indices().next());
if let Some(idx) = entry_idx {
wf.entry_node = wf.graph[idx].id.clone();
} else {
return Err(anyhow!(
"Architecture Error: Workflow must contain at least one valid node."
));
}
}
Ok(wf)
}
fn parse_lib_rdp(content: &str) -> Result<WorkflowGraph> {
let tokens = Lexer::new(content)
.tokenize()
.map_err(|e| anyhow!("JWL Compilation Syntax Error:\n{}", e))?;
let mut parser = JwlParser::new(&tokens, content);
let wf = parser.parse_workflow()?;
if wf.entry_node.is_empty()
&& wf.graph.node_indices().next().is_none()
&& wf.functions.is_empty()
&& wf.classes.is_empty()
{
return Err(anyhow!(
"Library Error: Library file must define at least one function or class."
));
}
Ok(wf)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::graph::NodeType;
#[test]
fn test_python_imports_parsing() {
let content = r#"
python: ["pandas", "sklearn.ensemble", "./utils.py"]
[load]: pandas.read_csv(path="data.csv")
[train]: sklearn.ensemble.RandomForestClassifier()
[load] -> [train]
"#;
let graph = GraphParser::parse(content).unwrap();
assert_eq!(graph.python_imports.len(), 3);
assert!(graph.python_imports.contains(&"pandas".to_string()));
assert!(graph
.python_imports
.contains(&"sklearn.ensemble".to_string()));
assert!(graph.python_imports.contains(&"./utils.py".to_string()));
}
#[test]
fn test_scoped_identifier_call() {
let content = r#"
python: ["pandas"]
[load]: pandas.read_csv(path="data.csv", encoding="utf-8")
"#;
let graph = GraphParser::parse(content).unwrap();
let node = graph.graph.node_weights().next().unwrap();
assert_eq!(node.id, "load");
if let NodeType::Task(action) = &node.node_type {
assert_eq!(action.name, "pandas.read_csv");
assert_eq!(action.params.get("path"), Some(&"\"data.csv\"".to_string()));
assert_eq!(
action.params.get("encoding"),
Some(&"\"utf-8\"".to_string())
);
} else {
panic!("Expected Task node type");
}
}
#[test]
fn test_switch_syntax_parsing() {
let content = r#"
[start]: notify(message="start")
[case_a]: notify(message="A")
[case_b]: notify(message="B")
[fallback]: notify(message="default")
[start] -> switch type {
"a": [case_a]
"b": [case_b]
default: [fallback]
}
"#;
let graph = GraphParser::parse(content).unwrap();
assert!(graph.switch_routes.contains_key("start"));
let switch_route = graph.switch_routes.get("start").unwrap();
assert_eq!(switch_route.subject.trim(), "type");
assert_eq!(switch_route.cases.len(), 3);
assert_eq!(switch_route.cases[0].value, Some("a".to_string()));
assert_eq!(switch_route.cases[0].target, "case_a");
assert_eq!(switch_route.cases[1].value, Some("b".to_string()));
assert_eq!(switch_route.cases[1].target, "case_b");
assert_eq!(switch_route.cases[2].value, None); assert_eq!(switch_route.cases[2].target, "fallback");
}
#[test]
fn test_missing_comma_detected() {
let content = r#"
[start]: notify(message="hello" status="ok")
"#;
let result = GraphParser::parse(content);
assert!(
result.is_err(),
"Missing comma between parameters should cause parse error"
);
}
#[test]
fn test_valid_comma_separated_params() {
let content = r#"
[start]: notify(message="hello", status="ok")
"#;
let result = GraphParser::parse(content);
assert!(
result.is_ok(),
"Comma-separated params should parse: {:?}",
result.err()
);
}
#[test]
fn test_comparison_in_expression() {
let content = r#"
[start]: notify(message="test")
[a]: notify(message="a")
[b]: notify(message="b")
[start] if output.category == "technical" -> [a]
[start] -> [b]
"#;
let result = GraphParser::parse(content);
assert!(
result.is_ok(),
"Comparison operators should be valid: {:?}",
result.err()
);
}
#[test]
fn test_duplicate_param_detected() {
let content = r#"
[start]: notify(message="first", message="second")
"#;
let result = GraphParser::parse(content);
assert!(
result.is_err(),
"Duplicate parameter keys should cause parse error"
);
}
#[test]
fn test_multiline_params_with_commas() {
let content = r#"
[start]: chat(
agent="helper",
message=input.query
)
"#;
let result = GraphParser::parse(content);
assert!(
result.is_ok(),
"Multiline params should parse: {:?}",
result.err()
);
}
#[test]
fn test_single_step_function() {
let content = r#"
[greet(name)]: bash(command="echo Hello, " + name)
[step1]: greet(name="world")
"#;
let graph = GraphParser::parse(content).unwrap();
assert!(graph.functions.contains_key("greet"));
let func = graph.functions.get("greet").unwrap();
assert_eq!(func.params, vec!["name"]);
assert_eq!(func.body.node_map.len(), 1);
}
#[test]
fn test_multi_step_function() {
let content = r#"
[build(dir)]: {
bash(command="cd " + dir + " && make")
bash(command="cd " + dir + " && make test")
}
[step1]: build(dir="/app")
"#;
let graph = GraphParser::parse(content).unwrap();
assert!(graph.functions.contains_key("build"));
let func = graph.functions.get("build").unwrap();
assert_eq!(func.params, vec!["dir"]);
assert_eq!(func.body.node_map.len(), 2);
assert_eq!(func.body.graph.edge_count(), 1);
}
#[test]
fn test_multi_step_function_with_semicolons() {
let content = r#"
[build(a, b)]: { bash(command=a); bash(command=b) }
[step1]: build(a="foo", b="bar")
"#;
let graph = GraphParser::parse(content).unwrap();
let func = graph.functions.get("build").unwrap();
assert_eq!(func.params, vec!["a", "b"]);
assert_eq!(func.body.node_map.len(), 2);
}
#[test]
fn test_function_not_in_main_graph() {
let content = r#"
[greet(name)]: bash(command="echo " + name)
[step1]: greet(name="world")
"#;
let graph = GraphParser::parse(content).unwrap();
assert!(!graph.node_map.contains_key("greet"));
assert!(graph.node_map.contains_key("step1"));
}
#[test]
fn test_no_params_backward_compat() {
let content = r#"
[start]: bash(command="echo hello")
"#;
let graph = GraphParser::parse(content).unwrap();
assert!(graph.node_map.contains_key("start"));
assert!(graph.functions.is_empty());
}
#[test]
fn test_string_concat_expression() {
let content = r#"
[start]: chat(agent="helper", message="[Expert] " + input.query)
"#;
let result = GraphParser::parse(content);
assert!(
result.is_ok(),
"String concat should parse: {:?}",
result.err()
);
}
#[test]
fn test_foreach_without_dollar() {
let content = r#"
[loop]: foreach(item in input.items) {
[step]: notify(message="ok")
}
"#;
let graph = GraphParser::parse(content).unwrap();
let node = graph.node_map.get("loop").unwrap();
let node_data = &graph.graph[*node];
if let NodeType::Foreach { item, list, .. } = &node_data.node_type {
assert_eq!(item, "item");
assert_eq!(list, "input.items");
} else {
panic!("Expected Foreach node");
}
}
#[test]
fn test_assignment_block_parsing() {
let content = r#"
[init]: count = 0, name = "Alice"
[next]: notify(message="done")
[init] -> [next]
"#;
let graph = GraphParser::parse(content).unwrap();
let node = graph.node_map.get("init").unwrap();
let node_data = &graph.graph[*node];
if let NodeType::Task(action) = &node_data.node_type {
assert_eq!(action.name, "set_context");
assert_eq!(action.params.get("count").unwrap(), "0");
assert_eq!(action.params.get("name").unwrap(), "\"Alice\"");
} else {
panic!("Expected Task node, got {:?}", node_data.node_type);
}
}
#[test]
fn test_assignment_single() {
let content = r#"
[init]: result = output.data
"#;
let graph = GraphParser::parse(content).unwrap();
let node = graph.node_map.get("init").unwrap();
let node_data = &graph.graph[*node];
if let NodeType::Task(action) = &node_data.node_type {
assert_eq!(action.name, "set_context");
assert_eq!(action.params.get("result").unwrap(), "output.data");
} else {
panic!("Expected Task node");
}
}
#[test]
fn test_triple_quoted_in_task_param() {
let input = r#"
[run]: bash(command="""echo "hello world" && echo '{"key":"value"}'""")
"#;
let wf = GraphParser::parse(input).unwrap();
let node = &wf.graph[*wf.node_map.get("run").unwrap()];
if let NodeType::Task(action) = &node.node_type {
assert_eq!(action.name, "bash");
let cmd = action.params.get("command").unwrap();
assert!(cmd.contains(r#"echo "hello world""#));
} else {
panic!("Expected Task node");
}
}
#[test]
fn test_triple_quoted_multiline_param() {
let input = "[run]: bash(command=\"\"\"line1\nline2\nline3\"\"\")";
let wf = GraphParser::parse(input).unwrap();
assert!(wf.node_map.contains_key("run"));
}
#[test]
fn test_triple_quoted_with_regular_string() {
let input = r#"
[a]: bash(command="""echo "test" done""")
[b]: bash(command="echo simple")
"#;
let wf = GraphParser::parse(input).unwrap();
assert!(wf.node_map.contains_key("a"));
assert!(wf.node_map.contains_key("b"));
}
#[test]
fn test_triple_quoted_assignment() {
let input = r#"
[setup]: cmd = """curl -H "Auth: key" https://api.com"""
"#;
let wf = GraphParser::parse(input).unwrap();
assert!(wf.node_map.contains_key("setup"));
}
}