tree-type-proc-macro 0.1.1

Procedural macros for tree-type crate
Documentation
//! Parser for `tree_type` macro syntax

use crate::core::{Attribute, Child, DefaultValue, TreeDef};
use syn::{
    Error, Expr, Ident, LitStr, Result, Token, Type, braced, bracketed,
    parse::{Parse, ParseStream},
};

impl Parse for TreeDef {
    fn parse(input: ParseStream) -> Result<Self> {
        let name: Ident = input.parse()?;

        // Reject old => syntax with helpful error
        if input.peek(Token![=>]) {
            return Err(Error::new(
                input.span(),
                "the `=>` token is no longer supported\nhelp: use `TreeName { ... }` instead of `TreeName => { ... }`",
            ));
        }

        let content;
        braced!(content in input);

        let children = parse_children_with_parent(&content, Some(&name))?;

        Ok(TreeDef { name, children })
    }
}

fn parse_children_with_parent(
    input: ParseStream,
    _parent_type: Option<&Ident>,
) -> Result<Vec<Child>> {
    let mut children = Vec::new();

    while !input.is_empty() {
        children.push(parse_child_with_parent(input, None)?);

        if input.peek(Token![,]) {
            input.parse::<Token![,]>()?;
        }
    }

    Ok(children)
}

fn parse_child_with_parent(input: ParseStream, _parent_type: Option<&Ident>) -> Result<Child> {
    // Parse attributes FIRST (before item)
    let attributes = parse_attributes(input)?;

    // Check for dynamic ID: [id: Type] => ChildType => { ... }
    if input.peek(syn::token::Bracket) {
        return parse_dynamic_id_with_parent(input, attributes, None);
    }

    // Parse name
    let name: Ident = input.parse()?;

    // Block reserved method names
    if name == "parent" {
        return Err(syn::Error::new(
            name.span(),
            "Directory name 'parent' is reserved for the parent() method",
        ));
    }

    // Check for directory marker (/)
    let is_directory = input.peek(Token![/]);
    if is_directory {
        input.parse::<Token![/]>()?;
    }

    // Parse custom filename: name("filename")
    let custom_filename = if input.peek(syn::token::Paren) {
        let content;
        syn::parenthesized!(content in input);
        Some(content.parse::<LitStr>()?)
    } else {
        None
    };

    // Parse custom type: name as Type
    let custom_type = if input.peek(Token![as]) {
        input.parse::<Token![as]>()?;
        Some(input.parse::<Ident>()?)
    } else {
        None
    };

    // Reject old trailing attribute syntax
    if input.peek(Token![#]) {
        return Err(Error::new(
            input.span(),
            "attributes must appear before the item, not after\nhelp: move `#[...]` before the item name",
        ));
    }

    // Parse children for directories
    if is_directory {
        let children = if input.peek(syn::token::Brace) {
            let content;
            braced!(content in input);
            // Pass the current directory type as parent for its children
            let current_type = custom_type.as_ref().unwrap_or(&name);
            parse_children_with_parent(&content, Some(current_type))?
        } else {
            Vec::new()
        };

        Ok(Child::Directory {
            name,
            custom_filename,
            custom_type,
            attributes,
            children,
        })
    } else {
        Ok(Child::File {
            name,
            custom_filename,
            custom_type,
            attributes,
        })
    }
}

fn parse_dynamic_id_with_parent(
    input: ParseStream,
    attributes: Vec<Attribute>,
    _parent_type: Option<&Ident>,
) -> Result<Child> {
    // Parse [id: Type]
    let content;
    bracketed!(content in input);

    let id_name: Ident = content.parse()?;
    content.parse::<Token![:]>()?;
    let id_type: Type = content.parse()?;

    // Check for directory marker (/)
    let is_directory = input.peek(Token![/]);
    if is_directory {
        input.parse::<Token![/]>()?;
    }

    // Parse optional custom type: as ChildType
    let child_type = if input.peek(Token![as]) {
        input.parse::<Token![as]>()?;
        input.parse::<Ident>()?
    } else {
        // Generate default type name from id_name
        let default_name = format!(
            "{}Type",
            id_name
                .to_string()
                .chars()
                .next()
                .unwrap()
                .to_uppercase()
                .collect::<String>()
                + &id_name.to_string()[1..]
        );
        Ident::new(&default_name, id_name.span())
    };

    // Parse children for directories
    let children = if is_directory && input.peek(syn::token::Brace) {
        let content;
        braced!(content in input);
        // Pass the child_type as parent for its children
        parse_children_with_parent(&content, Some(&child_type))?
    } else {
        Vec::new()
    };

    Ok(Child::DynamicId {
        id_name,
        id_type,
        child_type,
        attributes,
        children,
        is_directory,
    })
}

fn parse_attributes(input: ParseStream) -> Result<Vec<Attribute>> {
    let mut attributes = Vec::new();

    while input.peek(Token![#]) {
        input.parse::<Token![#]>()?;

        let content;
        bracketed!(content in input);

        // Parse single attribute (no comma-separated lists)
        let attr = parse_attribute(&content)?;
        attributes.push(attr);

        // Reject grouped attributes with helpful error
        if content.peek(Token![,]) {
            return Err(Error::new(
                content.span(),
                "grouped attributes are no longer supported\nhelp: use separate `#[...]` for each attribute instead of `#[a, b]`",
            ));
        }

        // Ensure attribute is fully consumed
        if !content.is_empty() {
            return Err(Error::new(content.span(), "unexpected tokens in attribute"));
        }
    }

    Ok(attributes)
}

fn parse_attribute(input: ParseStream) -> Result<Attribute> {
    let name: Ident = input.parse()?;
    let name_str = name.to_string();

    match name_str.as_str() {
        "required" => Ok(Attribute::Required),
        "optional" => Ok(Attribute::Optional),
        "default" => {
            // Support both `default(expr)` and `default = expr` syntax
            if input.peek(syn::token::Paren) {
                let content;
                syn::parenthesized!(content in input);

                // Try to parse as literal string first
                if let Ok(lit) = content.parse::<LitStr>() {
                    Ok(Attribute::Default(DefaultValue::Literal(lit)))
                } else {
                    // Parse as expression (function call)
                    let expr: Expr = content.parse()?;
                    Ok(Attribute::Default(DefaultValue::Function(expr)))
                }
            } else if input.peek(Token![=]) {
                input.parse::<Token![=]>()?;
                let expr: Expr = input.parse()?;
                Ok(Attribute::Default(DefaultValue::Function(expr)))
            } else {
                Ok(Attribute::Default(DefaultValue::DefaultTrait))
            }
        }
        "validate" => {
            let content;
            syn::parenthesized!(content in input);
            let expr: Expr = content.parse()?;
            Ok(Attribute::Validate(expr))
        }
        "pattern" => {
            let content;
            syn::parenthesized!(content in input);
            let pattern: LitStr = content.parse()?;
            Ok(Attribute::Pattern(pattern))
        }
        "symlink" => {
            input.parse::<Token![=]>()?;

            // Try to parse as string literal first (legacy syntax)
            if let Ok(target) = input.parse::<LitStr>() {
                return Ok(Attribute::Symlink(target));
            }

            // Parse identity path (e.g., src/main, ../other/file, /config/main)
            let mut path_parts = Vec::new();
            let mut is_absolute = false;

            // Handle leading / for absolute paths
            if input.peek(Token![/]) {
                input.parse::<Token![/]>()?;
                is_absolute = true;
            }

            // Handle leading .. for relative paths
            while input.peek(Token![..]) {
                input.parse::<Token![..]>()?;
                path_parts.push("..".to_string());
                if input.peek(Token![/]) {
                    input.parse::<Token![/]>()?;
                }
            }

            // Parse path components separated by /
            while let Ok(ident) = input.parse::<Ident>() {
                path_parts.push(ident.to_string());

                if input.peek(Token![/]) {
                    input.parse::<Token![/]>()?;
                } else {
                    break;
                }
            }

            if path_parts.is_empty() {
                return Err(Error::new(input.span(), "symlink target cannot be empty"));
            }

            // Join path parts and create string literal
            let path_str = if is_absolute {
                format!("/{}", path_parts.join("/"))
            } else {
                path_parts.join("/")
            };
            let target_str = LitStr::new(&path_str, input.span());
            Ok(Attribute::Symlink(target_str))
        }
        _ => Err(Error::new(
            name.span(),
            format!(
                "unknown attribute `{name_str}`\nhelp: valid attributes are: required, optional, default, validate, pattern, symlink"
            ),
        )),
    }
}