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()?;
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> {
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 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,
})
}
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),
"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![=]) {
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![=]>()?;
if let Ok(target) = input.parse::<LitStr>() {
return Ok(Attribute::Symlink(target));
}
let mut path_parts = Vec::new();
let mut is_absolute = false;
if input.peek(Token![/]) {
input.parse::<Token![/]>()?;
is_absolute = true;
}
while input.peek(Token![..]) {
input.parse::<Token![..]>()?;
path_parts.push("..".to_string());
if input.peek(Token![/]) {
input.parse::<Token![/]>()?;
}
}
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"));
}
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"
),
)),
}
}