use crate::commands::check::errors::CheckError;
use dampen_core::ir::node::{WidgetKind, WidgetNode};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct TreeNodeInfo {
pub id: String,
pub label: String,
pub file: PathBuf,
pub line: u32,
pub col: u32,
}
#[derive(Debug, Default)]
pub struct TreeViewValidator {
node_ids: HashMap<String, TreeNodeInfo>,
errors: Vec<CheckError>,
current_file: PathBuf,
}
impl TreeViewValidator {
pub fn new() -> Self {
Self::default()
}
pub fn set_file(&mut self, file: PathBuf) {
self.current_file = file;
}
pub fn validate_tree_view(&mut self, node: &WidgetNode) {
self.node_ids.clear();
self.errors.clear();
for child in &node.children {
if child.kind == WidgetKind::TreeNode {
self.validate_tree_node(child);
}
}
}
fn validate_tree_node(&mut self, node: &WidgetNode) {
let span = &node.span;
let id_value = node.id.clone().unwrap_or_default();
let label = node.attributes.get("label");
if node.id.is_none() {
self.errors.push(CheckError::MissingRequiredAttribute {
attr: "id".to_string(),
widget: "tree_node".to_string(),
file: self.current_file.clone(),
line: span.line,
col: span.column,
});
}
if label.is_none() {
self.errors.push(CheckError::MissingRequiredAttribute {
attr: "label".to_string(),
widget: "tree_node".to_string(),
file: self.current_file.clone(),
line: span.line,
col: span.column,
});
}
if !id_value.is_empty() {
if let Some(existing) = self.node_ids.get(&id_value) {
self.errors.push(CheckError::DuplicateTreeNodeId {
id: id_value.clone(),
file: self.current_file.clone(),
line: span.line,
col: span.column,
first_file: existing.file.clone(),
first_line: existing.line,
first_col: existing.col,
});
} else {
let label_value = label.map_or_else(
|| id_value.clone(),
|attr| match attr {
dampen_core::ir::node::AttributeValue::Static(s) => s.clone(),
_ => id_value.clone(),
},
);
self.node_ids.insert(
id_value.clone(),
TreeNodeInfo {
id: id_value.clone(),
label: label_value,
file: self.current_file.clone(),
line: span.line,
col: span.column,
},
);
}
}
for child in &node.children {
if child.kind == WidgetKind::TreeNode {
self.validate_tree_node(child);
}
}
}
pub fn errors(&self) -> &[CheckError] {
&self.errors
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use dampen_core::ir::Span;
use dampen_core::ir::node::AttributeValue;
use std::collections::HashMap;
fn create_test_node(id: &str, label: &str, line: u32) -> WidgetNode {
let mut attributes = HashMap::new();
attributes.insert(
"label".to_string(),
AttributeValue::Static(label.to_string()),
);
WidgetNode {
kind: WidgetKind::TreeNode,
id: Some(id.to_string()),
attributes,
events: vec![],
children: vec![],
span: Span::new(0, 0, line, 1),
style: None,
layout: None,
theme_ref: None,
classes: vec![],
breakpoint_attributes: HashMap::new(),
inline_state_variants: HashMap::new(),
}
}
fn create_node_without_id(line: u32) -> WidgetNode {
let mut attributes = HashMap::new();
attributes.insert(
"label".to_string(),
AttributeValue::Static("Test".to_string()),
);
WidgetNode {
kind: WidgetKind::TreeNode,
id: None,
attributes,
events: vec![],
children: vec![],
span: Span::new(0, 0, line, 1),
style: None,
layout: None,
theme_ref: None,
classes: vec![],
breakpoint_attributes: HashMap::new(),
inline_state_variants: HashMap::new(),
}
}
fn create_node_without_label(line: u32) -> WidgetNode {
WidgetNode {
kind: WidgetKind::TreeNode,
id: Some("test".to_string()),
attributes: HashMap::new(),
events: vec![],
children: vec![],
span: Span::new(0, 0, line, 1),
style: None,
layout: None,
theme_ref: None,
classes: vec![],
breakpoint_attributes: HashMap::new(),
inline_state_variants: HashMap::new(),
}
}
#[test]
fn test_unique_node_ids() {
let mut validator = TreeViewValidator::new();
validator.set_file(PathBuf::from("test.dampen"));
let tree_view = WidgetNode {
kind: WidgetKind::TreeView,
id: None,
attributes: HashMap::new(),
events: vec![],
children: vec![
create_test_node("node1", "Node 1", 10),
create_test_node("node2", "Node 2", 15),
create_test_node("node3", "Node 3", 20),
],
span: Span::new(0, 0, 1, 1),
style: None,
layout: None,
theme_ref: None,
classes: vec![],
breakpoint_attributes: HashMap::new(),
inline_state_variants: HashMap::new(),
};
validator.validate_tree_view(&tree_view);
assert!(!validator.has_errors());
}
#[test]
fn test_duplicate_node_ids() {
let mut validator = TreeViewValidator::new();
validator.set_file(PathBuf::from("test.dampen"));
let tree_view = WidgetNode {
kind: WidgetKind::TreeView,
id: None,
attributes: HashMap::new(),
events: vec![],
children: vec![
create_test_node("node1", "Node 1", 10),
create_test_node("node1", "Node 1 Duplicate", 15),
],
span: Span::new(0, 0, 1, 1),
style: None,
layout: None,
theme_ref: None,
classes: vec![],
breakpoint_attributes: HashMap::new(),
inline_state_variants: HashMap::new(),
};
validator.validate_tree_view(&tree_view);
assert!(validator.has_errors());
assert_eq!(validator.errors().len(), 1);
}
#[test]
fn test_missing_required_attributes() {
let mut validator = TreeViewValidator::new();
validator.set_file(PathBuf::from("test.dampen"));
let tree_view = WidgetNode {
kind: WidgetKind::TreeView,
id: None,
attributes: HashMap::new(),
events: vec![],
children: vec![create_node_without_id(10), create_node_without_label(15)],
span: Span::new(0, 0, 1, 1),
style: None,
layout: None,
theme_ref: None,
classes: vec![],
breakpoint_attributes: HashMap::new(),
inline_state_variants: HashMap::new(),
};
validator.validate_tree_view(&tree_view);
assert!(validator.has_errors());
assert_eq!(validator.errors().len(), 2);
}
}