use crate::core::Attribute;
use crate::core::Child;
use crate::core::TreeDef;
use proc_macro2::Span;
use std::collections::HashMap;
use syn::Error;
use syn::Result;
pub fn validate_tree(tree: &TreeDef) -> Result<()> {
validate_unique_names(&tree.children)?;
validate_symlink_targets(&tree.children, &tree.children)?;
validate_attribute_combinations(&tree.children)?;
Ok(())
}
fn validate_unique_names(children: &[Child]) -> Result<()> {
let mut seen: HashMap<String, Span> = HashMap::new();
for child in children {
let name = child.name();
let name_str = name.to_string();
if let Some(_first_span) = seen.get(&name_str) {
return Err(Error::new(
name.span(),
format!("duplicate field name `{name_str}`"),
));
}
seen.insert(name_str, name.span());
if let Child::Directory { children, .. } | Child::DynamicId { children, .. } = child {
validate_unique_names(children)?;
}
}
Ok(())
}
fn validate_symlink_targets(children: &[Child], root_children: &[Child]) -> Result<()> {
for child in children {
if let Some(target) = child.get_symlink_target() {
let target_str = target.value();
if target_str.is_empty() {
return Err(Error::new(target.span(), "symlink target cannot be empty"));
}
validate_symlink_target_existence(child, target, root_children, children)?;
}
if let Child::Directory { children, .. } | Child::DynamicId { children, .. } = child {
validate_symlink_targets(children, root_children)?;
}
}
Ok(())
}
fn validate_symlink_target_existence(
_symlink_child: &Child,
target: &syn::LitStr,
root_children: &[Child],
current_children: &[Child],
) -> Result<()> {
let target_str = target.value();
let target_child = if let Some(stripped) = target_str.strip_prefix('/') {
find_child_by_path(stripped, root_children)
} else {
find_child_by_name(&target_str, current_children)
.or_else(|| find_child_recursive(&target_str, root_children))
};
if let Some(target_child) = target_child {
if !target_has_default_content(target_child) && !target_child.is_required() {
return Err(Error::new(
target.span(),
format!(
"symlink target '{}' may not exist at runtime\n\
help: add #[default(...)] attribute to '{}' to ensure it exists during setup()\n\
help: or add #[required] attribute if the file should be created manually",
target_str,
target_child.name()
),
));
}
} else {
return Err(Error::new(
target.span(),
format!(
"symlink target '{target_str}' not found in tree structure\n\
help: ensure the target identifier exists in the same directory or use an absolute path"
),
));
}
Ok(())
}
fn find_child_recursive<'a>(name: &str, children: &'a [Child]) -> Option<&'a Child> {
if let Some(child) = find_child_by_name(name, children) {
return Some(child);
}
for child in children {
if let Child::Directory { children, .. } | Child::DynamicId { children, .. } = child {
if let Some(found) = find_child_recursive(name, children) {
return Some(found);
}
}
}
None
}
fn target_has_default_content(child: &Child) -> bool {
child
.attributes()
.iter()
.any(|attr| matches!(attr, Attribute::Default(_)))
}
fn find_child_by_path<'a>(path: &str, children: &'a [Child]) -> Option<&'a Child> {
let parts: Vec<&str> = path.split('/').collect();
if parts.is_empty() {
return None;
}
let first_part = parts[0];
let child = find_child_by_name(first_part, children)?;
if parts.len() == 1 {
Some(child)
} else {
match child {
Child::Directory { children, .. } | Child::DynamicId { children, .. } => {
let remaining_path = parts[1..].join("/");
find_child_by_path(&remaining_path, children)
}
Child::File { .. } => None, }
}
}
fn find_child_by_name<'a>(name: &str, children: &'a [Child]) -> Option<&'a Child> {
children.iter().find(|child| *child.name() == name)
}
fn validate_attribute_combinations(children: &[Child]) -> Result<()> {
for child in children {
let attrs = child.attributes();
let has_required = attrs.iter().any(|a| matches!(a, Attribute::Required));
let has_optional = attrs.iter().any(|a| matches!(a, Attribute::Optional));
if has_required && has_optional {
return Err(Error::new(
child.span(),
"conflicting attributes: cannot be both `required` and `optional`",
));
}
let has_pattern = attrs.iter().any(|a| matches!(a, Attribute::Pattern(_)));
if has_pattern {
#[cfg(not(feature = "pattern-validation"))]
{
return Err(Error::new(
child.span(),
"pattern validation requires the `pattern-validation` feature\nhelp: enable the feature in Cargo.toml",
));
}
}
if let Child::Directory { children, .. } | Child::DynamicId { children, .. } = child {
validate_attribute_combinations(children)?;
}
}
Ok(())
}