use crate::core::Attribute;
use crate::core::Child;
use crate::core::DefaultValue;
use crate::core::TreeDef;
use syn::Error;
use syn::Expr;
use syn::Ident;
use syn::LitStr;
use syn::Result;
use syn::Token;
use syn::Type;
use syn::braced;
use syn::bracketed;
use syn::parse::Parse;
use syn::parse::ParseStream;
impl Parse for TreeDef {
fn parse(input: ParseStream) -> Result<Self> {
let attributes = parse_attributes(input)?;
let name: Ident = input.parse()?;
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,
attributes,
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> {
let attributes = parse_attributes(input)?;
if input.peek(syn::token::Bracket) {
return parse_dynamic_id_with_parent(input, attributes, None);
}
let name: Ident = input.parse()?;
if name == "parent" {
return Err(syn::Error::new(
name.span(),
"Directory name 'parent' is reserved for the parent() method",
));
}
let is_directory = input.peek(Token![/]);
if is_directory {
input.parse::<Token![/]>()?;
}
let custom_filename = if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
Some(content.parse::<LitStr>()?)
} else {
None
};
let custom_type = if input.peek(Token![as]) {
input.parse::<Token![as]>()?;
Some(input.parse::<Ident>()?)
} else {
None
};
if input.peek(Token![#]) {
return Err(Error::new(
input.span(),
"attributes must appear before the item, not after\nhelp: move `#[...]` before the item name",
));
}
if is_directory {
let children = if input.peek(syn::token::Brace) {
let content;
braced!(content in input);
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> {
let content;
bracketed!(content in input);
let id_name: Ident = content.parse()?;
content.parse::<Token![:]>()?;
let id_type: Type = content.parse()?;
let is_directory = input.peek(Token![/]);
if is_directory {
input.parse::<Token![/]>()?;
}
let pattern = if input.peek(syn::token::Paren) {
let paren_content;
syn::parenthesized!(paren_content in input);
let pattern_lit: LitStr = paren_content.parse()?;
let pattern_str = pattern_lit.value();
let open_count = pattern_str.matches('{').count();
let close_count = pattern_str.matches('}').count();
if open_count != 1 || close_count != 1 {
return Err(Error::new(
pattern_lit.span(),
"pattern must contain exactly one {placeholder}",
));
}
if let Some(start) = pattern_str.find('{') {
if let Some(end) = pattern_str.find('}') {
let placeholder = &pattern_str[start + 1..end];
if placeholder.is_empty()
|| !placeholder.chars().all(|c| c.is_alphanumeric() || c == '_')
{
return Err(Error::new(
pattern_lit.span(),
"placeholder name must be a valid identifier (alphanumeric and underscores)",
));
}
}
}
Some(pattern_lit)
} else {
None
};
let child_type = if input.peek(Token![as]) {
input.parse::<Token![as]>()?;
input.parse::<Ident>()?
} else {
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())
};
let children = if is_directory && input.peek(syn::token::Brace) {
let content;
braced!(content in input);
parse_children_with_parent(&content, Some(&child_type))?
} else {
Vec::new()
};
Ok(Child::DynamicId {
id_name,
id_type,
child_type,
attributes,
children,
is_directory,
pattern,
})
}
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);
let attr = parse_attribute(&content)?;
attributes.push(attr);
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]`",
));
}
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),
"transparent" => Ok(Attribute::Transparent),
"default" => {
if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
if let Ok(lit) = content.parse::<LitStr>() {
Ok(Attribute::Default(DefaultValue::Literal(lit)))
} else {
let expr: Expr = content.parse()?;
Ok(Attribute::Default(DefaultValue::Function(expr)))
}
} else if input.peek(Token![=]) {
Err(Error::new(
input.span(),
"assignment syntax is no longer supported\nhelp: use `#[default(function_name)]` instead of `#[default = function_name]`",
))
} 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" => {
if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
if let Ok(target) = content.parse::<LitStr>() {
return Ok(Attribute::Symlink(target));
}
let mut path_parts = Vec::new();
let mut is_absolute = false;
if content.peek(Token![/]) {
content.parse::<Token![/]>()?;
is_absolute = true;
}
while content.peek(Token![..]) {
content.parse::<Token![..]>()?;
path_parts.push("..".to_string());
if content.peek(Token![/]) {
content.parse::<Token![/]>()?;
}
}
while let Ok(ident) = content.parse::<Ident>() {
path_parts.push(ident.to_string());
if content.peek(Token![/]) {
content.parse::<Token![/]>()?;
} else {
break;
}
}
if path_parts.is_empty() {
return Err(Error::new(content.span(), "symlink target cannot be empty"));
}
let path_str = if is_absolute {
format!("/{}", path_parts.join("/"))
} else {
path_parts.join("/")
};
let target_str = LitStr::new(&path_str, content.span());
Ok(Attribute::Symlink(target_str))
} else if input.peek(Token![=]) {
Err(Error::new(
input.span(),
"assignment syntax is no longer supported\nhelp: use `#[symlink(target)]` instead of `#[symlink = target]`",
))
} else {
Err(Error::new(
input.span(),
"symlink attribute requires a target\nhelp: use `#[symlink(target)]`",
))
}
}
_ => Err(Error::new(
name.span(),
format!(
"unknown attribute `{name_str}`\nhelp: valid attributes are: required, optional, default, validate, pattern, symlink"
),
)),
}
}