use std::collections::{HashMap, HashSet};
use anyhow::Result;
use regex::Regex;
use crate::{
models::{action::ActionValue, flow::FlowModel},
utils::constants,
validate::ValidateTrait,
};
impl ValidateTrait for FlowModel {
fn validate(&self) -> Result<()> {
if self.name.trim().is_empty() {
anyhow::bail!("Flow has no name. Add a name for the CLI command.");
}
for arg in &self.args {
arg.validate()?;
}
for action in &self.actions {
action.validate()?;
}
let mut tags: HashSet<&str> = HashSet::new();
for arg in &self.args {
if !tags.insert(arg.name.as_str()) {
anyhow::bail!("Duplicate argument: '{}'", arg.name);
}
}
for action in &self.actions {
if action.tag.is_empty() {
anyhow::bail!("Action must have a tag.");
}
if !tags.insert(action.tag.as_str()) {
anyhow::bail!("Duplicate tag: '{}'", action.tag);
}
}
let re = Regex::new(constants::TAG_PLACEHOLDER_PATTERN).unwrap();
for action in &self.actions {
match &action.action {
ActionValue::Simple(act) => {
validate_tag_references(act, &tags, &re)?;
}
ActionValue::Switch(cases) => {
for case in cases {
validate_tag_references(&case.when, &tags, &re)?;
validate_tag_references(&case.then, &tags, &re)?;
}
}
}
}
if let Some(pattern) = &self.check {
Regex::new(pattern).map_err(|e| anyhow::anyhow!("Invalid check regex: {}", e))?;
}
validate_no_cycles(self, &tags, &re)?;
Ok(())
}
}
fn validate_tag_references(text: &str, valid_tags: &HashSet<&str>, re: &Regex) -> Result<()> {
for cap in re.captures_iter(text) {
let tag = cap.get(1).unwrap().as_str();
if !valid_tags.contains(tag) {
anyhow::bail!("Unknown tag '{{{}}}' referenced in action", tag);
}
}
Ok(())
}
fn validate_no_cycles(flow: &FlowModel, tags: &HashSet<&str>, re: &Regex) -> Result<()> {
let mut graph: HashMap<&str, Vec<&str>> = HashMap::new();
for tag in tags.iter() {
graph.insert(*tag, vec![]);
}
for action in &flow.actions {
let mut deps = Vec::new();
match &action.action {
ActionValue::Simple(act) => {
deps.extend(extract_tag_refs(act, re));
}
ActionValue::Switch(cases) => {
for case in cases {
deps.extend(extract_tag_refs(&case.when, re));
deps.extend(extract_tag_refs(&case.then, re));
}
}
}
graph.insert(
action.tag.as_str(),
deps.into_iter().filter(|d| tags.contains(d)).collect(),
);
}
let mut visited = HashSet::new();
let mut stack = HashSet::new();
for tag in tags {
dfs(tag, &graph, &mut visited, &mut stack)?;
}
Ok(())
}
fn extract_tag_refs<'a>(text: &'a str, re: &Regex) -> Vec<&'a str> {
re.captures_iter(text)
.map(|c| c.get(1).unwrap().as_str())
.collect()
}
fn dfs<'a>(
node: &'a str,
graph: &HashMap<&'a str, Vec<&'a str>>,
visited: &mut HashSet<&'a str>,
stack: &mut HashSet<&'a str>,
) -> Result<()> {
if stack.contains(node) {
anyhow::bail!("Circular dependency detected involving tag: '{}'", node);
}
if visited.contains(node) {
return Ok(());
}
visited.insert(node);
stack.insert(node);
if let Some(deps) = graph.get(node) {
for dep in deps {
dfs(dep, graph, visited, stack)?;
}
}
stack.remove(node);
Ok(())
}