"""Tree validation helper module for draft/published workflow.""" from typing import Any class TreeValidationError(Exception): """Custom exception for tree validation errors.""" def __init__(self, field: str, message: str): self.field = field self.message = message super().__init__(f"{field}: {message}") def validate_tree_structure(tree_structure: dict[str, Any]) -> tuple[bool, list[dict[str, str]]]: """Validate tree structure for publishing. A valid tree for publishing must have: - A root node with id, type, and appropriate content fields - All decision nodes must have a question field - All decision nodes with children must have at least 2 children - All action nodes must have an action field - All solution nodes must have a solution field - No orphaned nodes (all nodes reachable from root) Args: tree_structure: The tree structure dict to validate Returns: Tuple of (is_valid, list of errors) Each error is a dict with 'field' and 'message' keys """ errors = [] # Check root node exists if not tree_structure: errors.append({"field": "tree_structure", "message": "Tree structure cannot be empty"}) return False, errors if "id" not in tree_structure: errors.append({"field": "tree_structure.id", "message": "Root node must have an id"}) if "type" not in tree_structure: errors.append({"field": "tree_structure.type", "message": "Root node must have a type"}) return False, errors # Validate root node based on type _validate_node(tree_structure, "root", errors) # Validate all child nodes recursively if "children" in tree_structure: _validate_children(tree_structure["children"], "root.children", errors) return len(errors) == 0, errors def _validate_node(node: dict[str, Any], path: str, errors: list[dict[str, str]]) -> None: """Validate a single node in the tree structure. Args: node: The node dict to validate path: Current path in the tree (for error messages) errors: List to append errors to """ node_type = node.get("type") if node_type == "decision": if "question" not in node or not node["question"]: errors.append({ "field": f"{path}.question", "message": "Decision nodes must have a non-empty question" }) # If node has children, must have at least 2 (for decision branches) children = node.get("children", []) if children and len(children) < 2: errors.append({ "field": f"{path}.children", "message": "Decision nodes with children must have at least 2 branches" }) elif node_type == "action": if "action" not in node or not node["action"]: errors.append({ "field": f"{path}.action", "message": "Action nodes must have a non-empty action" }) elif node_type == "solution": if "solution" not in node or not node["solution"]: errors.append({ "field": f"{path}.solution", "message": "Solution nodes must have a non-empty solution" }) else: errors.append({ "field": f"{path}.type", "message": f"Unknown node type: {node_type}" }) def _validate_children(children: list[dict[str, Any]], path: str, errors: list[dict[str, str]]) -> None: """Recursively validate child nodes. Args: children: List of child nodes path: Current path in the tree (for error messages) errors: List to append errors to """ for i, child in enumerate(children): child_path = f"{path}[{i}]" if "id" not in child: errors.append({"field": f"{child_path}.id", "message": "Child node must have an id"}) if "type" not in child: errors.append({"field": f"{child_path}.type", "message": "Child node must have a type"}) continue _validate_node(child, child_path, errors) # Recursively validate grandchildren if "children" in child: _validate_children(child["children"], f"{child_path}.children", errors) def can_publish_tree(tree_structure: dict[str, Any], name: str, description: str | None = None) -> tuple[bool, list[dict[str, str]]]: """Check if a tree can be published. Validates: - Tree has a name (non-empty) - Tree structure is valid Args: tree_structure: The tree structure to validate name: The tree name description: Optional tree description Returns: Tuple of (can_publish, list of errors) """ errors = [] # Validate name if not name or not name.strip(): errors.append({"field": "name", "message": "Tree must have a name to be published"}) # Validate tree structure structure_valid, structure_errors = validate_tree_structure(tree_structure) errors.extend(structure_errors) return len(errors) == 0, errors