use crate::core::Attribute;
use crate::core::Child;
use crate::core::DefaultValue;
use crate::core::TreeDef;
use quote::format_ident;
use quote::quote;
use syn::Ident;
#[derive(Debug, Clone)]
#[expect(dead_code)]
struct DynamicIdInfo {
id_type: proc_macro2::TokenStream,
}
#[allow(dead_code)]
fn collect_dynamic_ancestors(
children: &[Child],
target_name: &str,
current_path: &mut Vec<DynamicIdInfo>,
result: &mut Option<Vec<DynamicIdInfo>>,
) {
for child in children {
match child {
Child::DynamicId {
id_type,
child_type,
children,
..
} => {
current_path.push(DynamicIdInfo {
id_type: quote! { #id_type },
});
if *child_type == target_name {
*result = Some(current_path.clone());
current_path.pop();
return;
}
collect_dynamic_ancestors(children, target_name, current_path, result);
current_path.pop();
}
Child::Directory { children, .. } => {
collect_dynamic_ancestors(children, target_name, current_path, result);
}
Child::File { .. } => {
}
}
}
}
fn get_param_type_and_conversion(
id_type: &syn::Type,
) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
(
quote! { impl std::fmt::Display },
quote! { id.to_string().parse::<#id_type>().expect("Failed to parse ID") },
)
}
pub fn get_serde_derives() -> proc_macro2::TokenStream {
if cfg!(feature = "serde") {
quote! {
#[derive(serde::Serialize, serde::Deserialize)]
}
} else {
quote! {}
}
}
pub fn get_serde_derives_transparent() -> proc_macro2::TokenStream {
if cfg!(feature = "serde") {
quote! {
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
}
} else {
quote! {}
}
}
fn resolve_symlink_target_path(
target_path: &str,
root_children: &[Child],
up_dirs: &str,
_current_depth: usize,
) -> Option<proc_macro2::TokenStream> {
let path_to_resolve = target_path.strip_prefix('/').unwrap_or(target_path);
let path_parts: Vec<&str> = path_to_resolve
.split('/')
.filter(|s| !s.is_empty())
.collect();
if path_parts.is_empty() {
return None;
}
if let Some(target_child) = find_child_by_identity(&path_parts, root_children) {
let target_filename = match target_child {
Child::File {
custom_filename: Some(filename_lit),
..
} => filename_lit.value(),
Child::File {
name,
custom_filename: None,
..
}
| Child::Directory { name, .. } => {
name.to_string() }
Child::DynamicId { .. } => {
return None;
}
};
let path_prefix = if path_parts.len() > 1 {
path_parts[..path_parts.len() - 1].join("/")
} else {
String::new()
};
let full_path = if path_prefix.is_empty() {
format!("{up_dirs}{target_filename}")
} else {
format!("{up_dirs}{path_prefix}/{target_filename}")
};
return Some(quote! { #full_path });
}
None
}
fn find_child_by_identity<'a>(path_parts: &[&str], children: &'a [Child]) -> Option<&'a Child> {
if path_parts.is_empty() {
return None;
}
let first_part = path_parts[0];
let remaining_parts = &path_parts[1..];
for child in children {
let child_name = match child {
Child::File { name, .. } | Child::Directory { name, .. } => name.to_string(),
Child::DynamicId { id_name, .. } => id_name.to_string(),
};
if child_name == first_part {
if remaining_parts.is_empty() {
return Some(child);
}
if let Child::Directory {
children: dir_children,
..
} = child
{
return find_child_by_identity(remaining_parts, dir_children);
}
}
}
None
}
pub fn generate_code(tree: &TreeDef) -> proc_macro2::TokenStream {
let root_name = &tree.name;
let mut structs = Vec::new();
structs.push(generate_root_struct(root_name, &tree.children));
generate_child_structs(root_name, &tree.children, &mut structs, 0, &tree.children);
quote! {
#(#structs)*
}
}
#[allow(clippy::too_many_lines)]
fn generate_root_struct(name: &Ident, children: &[Child]) -> proc_macro2::TokenStream {
let nav_methods = children
.iter()
.map(|child| generate_nav_method(name, child));
let children_method = generate_children_method(children, false, false);
let parent_method = generate_parent_method(None);
let validate_impl = generate_validate_method(children);
let setup_impl = generate_setup_method(children, 0, children); let ensure_impl = generate_ensure_method(children);
let sync_impl = generate_sync_method(children);
let from_impl = if name == "GenericDir" {
quote! {}
} else {
quote! {
pub fn from_generic(dir: ::tree_type::GenericDir) -> Self {
Self { path: dir.as_path().to_path_buf() }
}
}
};
let from_trait = if name == "GenericDir" {
quote! {}
} else {
quote! {
impl From<#name> for ::tree_type::GenericDir {
fn from(dir: #name) -> Self {
Self::new(dir.path).expect("Path validation already performed")
}
}
}
};
let serde_derives = get_serde_derives();
let walk_fns = build_walk_fns();
let display_impl = generate_display_impl(name);
let debug_impl = generate_debug_impl(name);
quote! {
#serde_derives
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct #name {
path: std::path::PathBuf,
}
impl #name {
pub fn new(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
let path_buf = path.into();
if path_buf.as_os_str().is_empty() {
return Err(std::io::Error::from(std::io::ErrorKind::InvalidFilename));
}
Ok(Self { path: path_buf })
}
pub fn as_path(&self) -> &std::path::Path {
&self.path
}
pub fn exists(&self) -> bool {
self.path.exists()
}
pub fn as_generic(&self) -> ::tree_type::GenericDir {
::tree_type::GenericDir::new(self.path.clone()).expect("Path validation already performed")
}
pub fn create(&self) -> std::io::Result<()> {
::tree_type::fs::create_dir(&self.path)
}
pub fn create_all(&self) -> std::io::Result<()> {
::tree_type::fs::create_dir_all(&self.path)
}
pub fn remove(&self) -> std::io::Result<()> {
::tree_type::fs::remove_dir(&self.path)
}
pub fn remove_all(&self) -> std::io::Result<()> {
::tree_type::fs::remove_dir_all(&self.path)
}
pub fn read_dir(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<::tree_type::GenericPath>>> {
::tree_type::fs::read_dir(&self.path)
.map(|read_dir| read_dir.map(|result| result.and_then(::tree_type::GenericPath::try_from)))
}
pub fn fs_metadata(&self) -> std::io::Result<::tree_type::fs::Metadata> {
::tree_type::fs::metadata(&self.path)
}
pub fn file_name(&self) -> String {
self.path.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default()
}
#walk_fns
#validate_impl
#setup_impl
#ensure_impl
#sync_impl
#from_impl
#(#nav_methods)*
#children_method
#parent_method
}
impl AsRef<std::path::Path> for #name {
fn as_ref(&self) -> &std::path::Path {
&self.path
}
}
#display_impl
#debug_impl
#from_trait
}
}
pub fn build_walk_fns() -> proc_macro2::TokenStream {
if cfg!(feature = "walk") {
quote! {
pub fn walk_dir(&self) -> ::tree_type::deps::walk::WalkDir {
self.as_generic().walk_dir()
}
pub fn walk(&self) -> impl Iterator<Item = std::io::Result<::tree_type::GenericPath>> {
::tree_type::deps::walk::WalkDir::new(&self.path)
.into_iter()
.map(|r| r.map_err(|e| e.into()).and_then(::tree_type::GenericPath::try_from))
}
pub fn size_in_bytes(&self) -> std::io::Result<u64> {
self.as_generic().size_in_bytes()
}
pub fn lsl(
&self
) -> std::io::Result<Vec<(std::path::PathBuf, std::fs::Metadata)>> {
self.as_generic().lsl()
}
}
} else {
quote! {}
}
}
fn generate_child_structs(
parent_name: &Ident,
children: &[Child],
structs: &mut Vec<proc_macro2::TokenStream>,
depth: usize,
root_children: &[Child],
) {
generate_child_structs_with_parent_info(
parent_name,
children,
structs,
depth,
root_children,
None,
false, );
}
fn generate_child_structs_with_parent_info(
parent_name: &Ident,
children: &[Child],
structs: &mut Vec<proc_macro2::TokenStream>,
depth: usize,
root_children: &[Child],
parent_dynamic_id_info: Option<&syn::Type>,
grandparent_is_dynamic: bool,
) {
for child in children {
match child {
Child::File {
name,
custom_type,
attributes,
..
} => generate_child_file_structs_with_parent_info(
parent_name,
structs,
parent_dynamic_id_info,
grandparent_is_dynamic,
name,
custom_type.as_ref(),
attributes,
),
Child::Directory {
name,
custom_type,
children,
..
} => generate_child_directory_structs_with_parent_info(
parent_name,
structs,
depth,
root_children,
parent_dynamic_id_info,
grandparent_is_dynamic,
name,
custom_type.as_ref(),
children,
),
Child::DynamicId {
child_type,
children,
attributes,
is_directory,
id_name,
id_type,
..
} => generate_child_dynamic_id_structs_with_parent_info(
parent_name,
structs,
depth,
root_children,
parent_dynamic_id_info,
grandparent_is_dynamic,
child_type,
children,
attributes,
*is_directory,
id_name,
id_type,
),
}
}
}
#[expect(clippy::too_many_arguments)]
fn generate_child_dynamic_id_structs_with_parent_info(
parent_name: &Ident,
structs: &mut Vec<proc_macro2::TokenStream>,
depth: usize,
root_children: &[Child],
parent_dynamic_id_info: Option<&syn::Type>,
grandparent_is_dynamic: bool,
child_type: &Ident,
children: &[Child],
attributes: &[Attribute],
is_directory: bool,
id_name: &Ident,
id_type: &syn::Type,
) {
if is_directory {
if let Some(parent_id_type) = parent_dynamic_id_info {
structs.push(generate_dynamic_dir_struct_with_dynamic_parent(
child_type,
children,
depth + 1,
root_children,
parent_name,
id_name,
id_type,
parent_id_type,
grandparent_is_dynamic,
));
} else {
structs.push(generate_dynamic_dir_struct(
child_type,
children,
depth + 1,
root_children,
Some(parent_name),
id_name,
id_type,
));
}
generate_child_structs_with_parent_info(
child_type,
children,
structs,
depth + 1,
root_children,
Some(id_type),
parent_dynamic_id_info.is_some(), );
} else if let Some(parent_id_type) = parent_dynamic_id_info {
structs.push(generate_dynamic_file_struct_with_dynamic_parent(
child_type,
attributes,
parent_name,
id_name,
id_type,
parent_id_type,
grandparent_is_dynamic,
));
} else {
structs.push(generate_dynamic_file_struct(
child_type,
attributes,
Some(parent_name),
id_name,
id_type,
));
}
}
#[expect(clippy::too_many_arguments)]
fn generate_child_directory_structs_with_parent_info(
parent_name: &Ident,
structs: &mut Vec<proc_macro2::TokenStream>,
depth: usize,
root_children: &[Child],
parent_dynamic_id_info: Option<&syn::Type>,
grandparent_is_dynamic: bool,
name: &Ident,
custom_type: Option<&Ident>,
children: &[Child],
) {
let struct_name = get_child_type_name(parent_name, name, custom_type);
if let Some(parent_id_type) = parent_dynamic_id_info {
structs.push(generate_dir_struct_with_dynamic_parent(
&struct_name,
children,
depth + 1,
root_children,
parent_name,
parent_id_type,
));
generate_child_structs_with_parent_info(
&struct_name,
children,
structs,
depth + 1,
root_children,
Some(parent_id_type),
grandparent_is_dynamic,
);
} else {
structs.push(generate_dir_struct(
&struct_name,
children,
depth + 1,
root_children,
Some(parent_name),
));
generate_child_structs(&struct_name, children, structs, depth + 1, root_children);
}
}
fn generate_child_file_structs_with_parent_info(
parent_name: &Ident,
structs: &mut Vec<proc_macro2::TokenStream>,
parent_dynamic_id_info: Option<&syn::Type>,
grandparent_is_dynamic: bool,
name: &Ident,
custom_type: Option<&Ident>,
attributes: &[Attribute],
) {
let struct_name = get_child_type_name(parent_name, name, custom_type);
if let Some(parent_id_type) = parent_dynamic_id_info {
structs.push(generate_file_struct_with_dynamic_parent(
&struct_name,
attributes,
parent_name,
parent_id_type,
grandparent_is_dynamic,
));
} else {
structs.push(generate_file_struct(
&struct_name,
attributes,
Some(parent_name),
));
}
}
fn generate_nav_method_for_dynamic_id_parent(
parent_name: &Ident,
child: &Child,
_parent_id_name: &Ident,
) -> proc_macro2::TokenStream {
match child {
Child::File {
name,
custom_filename,
custom_type,
..
} => {
let method_name = name;
let filename = custom_filename
.as_ref()
.map_or_else(|| name.to_string(), syn::LitStr::value);
let return_type = get_child_type_name(parent_name, name, custom_type.as_ref());
quote! {
pub fn #method_name(&self) -> #return_type {
#return_type::new(self.path.join(#filename), self.id.clone()).expect("Path validation already performed")
}
}
}
Child::Directory {
name,
custom_filename,
custom_type,
..
} => {
let method_name = name;
let filename = custom_filename
.as_ref()
.map_or_else(|| name.to_string(), syn::LitStr::value);
let return_type = get_child_type_name(parent_name, name, custom_type.as_ref());
quote! {
pub fn #method_name(&self) -> #return_type {
#return_type::new(self.path.join(#filename), self.id.clone()).expect("Path validation already performed")
}
}
}
Child::DynamicId { .. } => {
generate_nav_method_with_parent_info(parent_name, child, true)
}
}
}
fn generate_nav_method(parent_name: &Ident, child: &Child) -> proc_macro2::TokenStream {
match child {
Child::File {
name,
custom_filename,
custom_type,
..
} => {
let method_name = name;
let filename = custom_filename
.as_ref()
.map_or_else(|| name.to_string(), syn::LitStr::value);
let return_type = get_child_type_name(parent_name, name, custom_type.as_ref());
quote! {
pub fn #method_name(&self) -> #return_type {
#return_type::new(self.path.join(#filename)).expect("Path validation already performed")
}
}
}
Child::Directory {
name,
custom_filename,
custom_type,
..
} => {
let method_name = name;
let dirname = custom_filename
.as_ref()
.map_or_else(|| name.to_string(), syn::LitStr::value);
let return_type = get_child_type_name(parent_name, name, custom_type.as_ref());
quote! {
pub fn #method_name(&self) -> #return_type {
#return_type::new(self.path.join(#dirname)).expect("Path validation already performed")
}
}
}
Child::DynamicId {
id_name,
id_type,
child_type,
..
} => {
let method_name = id_name;
let (param_type, conversion) = get_param_type_and_conversion(id_type);
quote! {
pub fn #method_name(&self, id: #param_type) -> #child_type {
let id_value = #conversion;
#child_type::new(self.path.join(id_value.to_string()), id_value).expect("Path validation already performed")
}
}
}
}
}
fn generate_nav_method_with_parent_info(
parent_name: &Ident,
child: &Child,
parent_is_dynamic_id: bool,
) -> proc_macro2::TokenStream {
match child {
Child::File {
name,
custom_filename,
custom_type,
..
} => {
let method_name = name;
let filename = custom_filename
.as_ref()
.map_or_else(|| name.to_string(), syn::LitStr::value);
let return_type = get_child_type_name(parent_name, name, custom_type.as_ref());
if parent_is_dynamic_id {
quote! {
pub fn #method_name(&self) -> #return_type {
#return_type::new(self.path.join(#filename), self.id.clone()).expect("Path validation already performed")
}
}
} else {
quote! {
pub fn #method_name(&self) -> #return_type {
#return_type::new(self.path.join(#filename)).expect("Path validation already performed")
}
}
}
}
Child::Directory {
name,
custom_filename,
custom_type,
..
} => {
let method_name = name;
let dirname = custom_filename
.as_ref()
.map_or_else(|| name.to_string(), syn::LitStr::value);
let return_type = get_child_type_name(parent_name, name, custom_type.as_ref());
if parent_is_dynamic_id {
quote! {
pub fn #method_name(&self) -> #return_type {
#return_type::new(self.path.join(#dirname), self.id.clone()).expect("Path validation already performed")
}
}
} else {
quote! {
pub fn #method_name(&self) -> #return_type {
#return_type::new(self.path.join(#dirname)).expect("Path validation already performed")
}
}
}
}
Child::DynamicId {
id_name,
id_type,
child_type,
..
} => {
let method_name = id_name;
let (param_type, conversion) = get_param_type_and_conversion(id_type);
if parent_is_dynamic_id {
quote! {
pub fn #method_name(&self, id: #param_type) -> #child_type {
let id_value = #conversion;
#child_type::new(self.path.join(id_value.to_string()), id_value, self.id.clone()).expect("Path validation already performed")
}
}
} else {
quote! {
pub fn #method_name(&self, id: #param_type) -> #child_type {
let id_value = #conversion;
#child_type::new(self.path.join(id_value.to_string()), id_value).expect("Path validation already performed")
}
}
}
}
}
}
fn generate_nav_method_for_dynamic_parent_children(
parent_name: &Ident,
child: &Child,
) -> proc_macro2::TokenStream {
match child {
Child::File {
name,
custom_filename,
custom_type,
..
} => {
let method_name = name;
let filename = custom_filename
.as_ref()
.map_or_else(|| name.to_string(), syn::LitStr::value);
let return_type = get_child_type_name(parent_name, name, custom_type.as_ref());
quote! {
pub fn #method_name(&self) -> #return_type {
#return_type::new(self.path.join(#filename), self.parent_id.clone()).expect("Path validation already performed")
}
}
}
Child::Directory {
name,
custom_filename,
custom_type,
..
} => {
let method_name = name;
let dirname = custom_filename
.as_ref()
.map_or_else(|| name.to_string(), syn::LitStr::value);
let return_type = get_child_type_name(parent_name, name, custom_type.as_ref());
quote! {
pub fn #method_name(&self) -> #return_type {
#return_type::new(self.path.join(#dirname), self.parent_id.clone()).expect("Path validation already performed")
}
}
}
Child::DynamicId {
id_name,
id_type,
child_type,
..
} => {
let method_name = id_name;
let param_type = quote! { impl std::fmt::Display };
let conversion =
quote! { id.to_string().parse::<#id_type>().expect("Failed to parse ID") };
quote! {
pub fn #method_name(&self, id: #param_type) -> #child_type {
let id_value = #conversion;
#child_type::new(self.path.join(id_value.to_string()), id_value, self.parent_id.clone()).expect("Path validation already performed")
}
}
}
}
}
fn generate_children_method(
children: &[Child],
parent_has_dynamic_id: bool,
current_is_dynamic_id: bool,
) -> Option<proc_macro2::TokenStream> {
let dynamic_children: Vec<_> = children
.iter()
.filter_map(|child| {
if let Child::DynamicId {
child_type,
is_directory,
id_type,
..
} = child
{
Some((child_type, *is_directory, id_type))
} else {
None
}
})
.collect();
if dynamic_children.is_empty() {
return None;
}
let (dynamic_child, is_directory, id_type) = &dynamic_children[0];
assert!(
dynamic_children.len() <= 1,
"Multiple dynamic ID types in the same parent directory are not supported. \
Found {} dynamic ID children. The children() method can only return a single type, \
making it impossible to determine which type any filesystem entry should be. \
Consider using separate subdirectories for different dynamic ID types.",
dynamic_children.len()
);
let file_type_check = if *is_directory {
quote! { entry_path.is_dir() }
} else {
quote! { entry_path.is_file() }
};
let constructor_call = if parent_has_dynamic_id {
quote! { #dynamic_child::new(entry_path, id, self.parent_id.clone()).map_err(std::io::Error::from) }
} else if current_is_dynamic_id {
quote! { #dynamic_child::new(entry_path, id, self.id.clone()).map_err(std::io::Error::from) }
} else {
quote! { #dynamic_child::new(entry_path, id).map_err(std::io::Error::from) }
};
Some(quote! {
pub fn children(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<#dynamic_child>>> {
let path = self.path.clone();
let read_dir = ::tree_type::fs::read_dir(&path)?;
Ok(read_dir.filter_map(move |entry| {
match entry {
Ok(entry) => {
let entry_path = entry.path();
if #file_type_check {
if let Some(filename) = entry_path.file_name() {
if let Some(id_str) = filename.to_str() {
match id_str.parse::<#id_type>() {
Ok(id) => Some(#constructor_call),
Err(_) => None, }
} else {
None }
} else {
None }
} else {
None
}
}
Err(e) => Some(Err(e))
}
}))
}
})
}
fn generate_parent_method(parent_type: Option<&Ident>) -> proc_macro2::TokenStream {
match parent_type {
Some(parent_type) => {
quote! {
pub fn parent(&self) -> #parent_type {
let parent_path = self.path.parent().expect("Non-root type must have parent");
#parent_type::new(parent_path).expect("Parent path should be valid")
}
}
}
None => {
quote! {
pub fn parent(&self) -> Option<::tree_type::GenericDir> {
self.path.parent().and_then(|parent_path| {
::tree_type::GenericDir::new(parent_path).ok()
})
}
}
}
}
}
fn get_child_type_name(
parent_name: &Ident,
child_name: &Ident,
custom_type: Option<&Ident>,
) -> Ident {
custom_type.cloned().unwrap_or_else(|| {
Ident::new(
&format!("{}{}", parent_name, capitalize(&child_name.to_string())),
child_name.span(),
)
})
}
fn generate_create_default_method(default_val: &DefaultValue) -> proc_macro2::TokenStream {
match default_val {
DefaultValue::DefaultTrait => {
quote! {
pub fn create_default<E>(&self) -> std::result::Result<::tree_type::CreateDefaultOutcome, E>
where
E: From<std::io::Error>,
{
if self.exists() {
return Ok(tree_type::CreateDefaultOutcome::AlreadyExists);
}
self.write(&String::default())?;
Ok(tree_type::CreateDefaultOutcome::Created)
}
}
}
DefaultValue::Literal(lit) => {
quote! {
pub fn create_default<E>(&self) -> std::result::Result<::tree_type::CreateDefaultOutcome, E>
where
E: From<std::io::Error>,
{
if self.exists() {
return Ok(tree_type::CreateDefaultOutcome::AlreadyExists);
}
self.write(&#lit.to_string())?;
Ok(tree_type::CreateDefaultOutcome::Created)
}
}
}
DefaultValue::Function(expr) => {
quote! {
pub fn create_default<E>(&self) -> std::result::Result<::tree_type::CreateDefaultOutcome, E>
where
E: From<std::io::Error>,
{
if self.exists() {
return Ok(tree_type::CreateDefaultOutcome::AlreadyExists);
}
let content = (#expr)(self)?;
self.write(&content)?;
Ok(tree_type::CreateDefaultOutcome::Created)
}
}
}
}
}
fn generate_file_struct_with_dynamic_parent(
name: &Ident,
attributes: &[Attribute],
parent_type: &Ident,
parent_id_type: &syn::Type,
parent_is_nested: bool,
) -> proc_macro2::TokenStream {
let default_method = attributes.iter().find_map(|attr| {
if let Attribute::Default(val) = attr {
Some(generate_create_default_method(val))
} else {
None
}
});
let parent_method = if parent_is_nested {
quote! {
pub fn parent(&self) -> #parent_type {
let parent_path = self.path.parent().expect("Non-root type must have parent");
let grandparent_path = parent_path.parent().expect("Parent should have a grandparent");
let grandparent_id = grandparent_path.file_name()
.expect("Grandparent path should have a filename")
.to_string_lossy()
.to_string();
#parent_type::new(parent_path, self.parent_id.clone(), grandparent_id).expect("Parent path should be valid")
}
}
} else {
quote! {
pub fn parent(&self) -> #parent_type {
let parent_path = self.path.parent().expect("Non-root type must have parent");
#parent_type::new(parent_path, self.parent_id.clone()).expect("Parent path should be valid")
}
}
};
let serde_derives = get_serde_derives();
let display_impl = generate_display_impl(name);
let debug_impl = generate_debug_impl(name);
quote! {
#serde_derives
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct #name {
path: std::path::PathBuf,
parent_id: #parent_id_type,
}
impl #name {
pub fn new(path: impl Into<std::path::PathBuf>, parent_id: #parent_id_type) -> std::io::Result<Self> {
let path_buf = path.into();
if path_buf.as_os_str().is_empty() {
return Err(std::io::Error::from(std::io::ErrorKind::InvalidFilename));
}
Ok(Self { path: path_buf, parent_id })
}
pub fn as_path(&self) -> &std::path::Path {
&self.path
}
pub fn exists(&self) -> bool {
self.path.exists()
}
pub fn as_generic(&self) -> ::tree_type::GenericFile {
::tree_type::GenericFile::new(self.path.clone()).expect("Path validation already performed")
}
pub fn read(&self) -> std::io::Result<Vec<u8>> {
::tree_type::fs::read(&self.path)
}
pub fn read_to_string(&self) -> std::io::Result<String> {
::tree_type::fs::read_to_string(&self.path)
}
pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
if let Some(parent) = self.path.parent() {
::tree_type::fs::create_dir_all(parent)?;
}
::tree_type::fs::write(&self.path, contents)
}
pub fn file_name(&self) -> Option<std::ffi::OsString> {
self.path.file_name().map(|name| name.to_os_string())
}
#[cfg(unix)]
pub fn secure(&self) -> std::io::Result<()> {
self.as_generic().secure()
}
pub fn from_generic(file: ::tree_type::GenericFile, parent_id: #parent_id_type) -> Self {
Self { path: file.as_path().to_path_buf(), parent_id }
}
#default_method
#parent_method
}
impl AsRef<std::path::Path> for #name {
fn as_ref(&self) -> &std::path::Path {
&self.path
}
}
#display_impl
#debug_impl
impl From<#name> for ::tree_type::GenericFile {
fn from(file: #name) -> Self {
Self::new(file.path).expect("Path validation already performed")
}
}
}
}
#[expect(clippy::too_many_lines)]
fn generate_dir_struct_with_dynamic_parent(
name: &Ident,
children: &[Child],
depth: usize,
root_children: &[Child],
parent_type: &Ident,
parent_id_type: &syn::Type,
) -> proc_macro2::TokenStream {
let nav_methods = children
.iter()
.map(|child| generate_nav_method_for_dynamic_parent_children(name, child));
let children_method = generate_children_method(children, true, false);
let parent_method = quote! {
pub fn parent(&self) -> #parent_type {
let parent_path = self.path.parent().expect("Non-root type must have parent");
#parent_type::new(parent_path, self.parent_id.clone()).expect("Parent path should be valid")
}
};
let validate_impl = generate_validate_method_with_parent_info(children, true, false);
let setup_impl = generate_setup_method(children, depth, root_children);
let ensure_impl = generate_ensure_method(children);
let sync_impl = generate_sync_method(children);
let serde_derives = get_serde_derives();
let display_impl = generate_display_impl(name);
let debug_impl = generate_debug_impl(name);
quote! {
#serde_derives
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct #name {
path: std::path::PathBuf,
parent_id: #parent_id_type,
}
impl #name {
pub fn new(path: impl Into<std::path::PathBuf>, parent_id: #parent_id_type) -> std::io::Result<Self> {
let path_buf = path.into();
if path_buf.as_os_str().is_empty() {
return Err(std::io::Error::from(std::io::ErrorKind::InvalidFilename));
}
Ok(Self { path: path_buf, parent_id })
}
pub fn as_path(&self) -> &std::path::Path {
&self.path
}
pub fn exists(&self) -> bool {
self.path.exists()
}
pub fn as_generic(&self) -> ::tree_type::GenericDir {
::tree_type::GenericDir::new(self.path.clone()).expect("Path validation already performed")
}
pub fn create(&self) -> std::io::Result<()> {
::tree_type::fs::create_dir(&self.path)
}
pub fn create_all(&self) -> std::io::Result<()> {
::tree_type::fs::create_dir_all(&self.path)
}
pub fn remove(&self) -> std::io::Result<()> {
::tree_type::fs::remove_dir(&self.path)
}
pub fn remove_all(&self) -> std::io::Result<()> {
::tree_type::fs::remove_dir_all(&self.path)
}
pub fn fs_metadata(&self) -> std::io::Result<::tree_type::fs::Metadata> {
::tree_type::fs::metadata(&self.path)
}
#[cfg(feature = "walk")]
pub fn walk_dir(&self) -> ::tree_type::deps::walk::WalkDir {
::tree_type::deps::walk::WalkDir::new(&self.path)
}
#[cfg(feature = "walk")]
pub fn walk(&self) -> impl Iterator<Item = std::io::Result<::tree_type::GenericPath>> {
::tree_type::deps::walk::WalkDir::new(&self.path)
.into_iter()
.map(|r| r.map_err(|e| e.into()).and_then(::tree_type::GenericPath::try_from))
}
#[cfg(feature = "walk")]
pub fn size_in_bytes(&self) -> std::io::Result<u64> {
let mut total = 0u64;
for entry in ::tree_type::deps::walk::WalkDir::new(&self.path) {
let entry = entry?;
if entry.file_type().is_file() {
total += entry.metadata()?.len();
}
}
Ok(total)
}
pub fn file_name(&self) -> String {
self.path.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default()
}
pub fn rename(self, new_path: impl AsRef<std::path::Path>) -> Result<Self, (std::io::Error, Self)> {
let new_path = new_path.as_ref();
if let Some(parent) = new_path.parent() {
if !parent.exists() {
if let Err(e) = std::fs::create_dir_all(parent) {
return Err((e, self));
}
}
}
match ::tree_type::fs::rename(&self.path, new_path) {
Ok(()) => Self::new(new_path, self.parent_id.clone()).map_err(|e| (e, self)),
Err(e) => Err((e, self)),
}
}
#[cfg(unix)]
pub fn secure(&self) -> std::io::Result<()> {
self.as_generic().secure()
}
pub fn from_generic(dir: ::tree_type::GenericDir, parent_id: #parent_id_type) -> Self {
Self { path: dir.as_path().to_path_buf(), parent_id }
}
#(#nav_methods)*
#children_method
#parent_method
#validate_impl
#setup_impl
#ensure_impl
#sync_impl
}
impl AsRef<std::path::Path> for #name {
fn as_ref(&self) -> &std::path::Path {
&self.path
}
}
#display_impl
#debug_impl
impl From<#name> for ::tree_type::GenericDir {
fn from(dir: #name) -> Self {
Self::new(dir.path).expect("Path validation already performed")
}
}
}
}
fn generate_file_struct(
name: &Ident,
attributes: &[Attribute],
parent_type: Option<&Ident>,
) -> proc_macro2::TokenStream {
let default_method = attributes.iter().find_map(|attr| {
if let Attribute::Default(val) = attr {
Some(generate_create_default_method(val))
} else {
None
}
});
let parent_method = generate_parent_method(parent_type);
let serde_derives = get_serde_derives();
let display_impl = generate_display_impl(name);
let debug_impl = generate_debug_impl(name);
quote! {
#serde_derives
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct #name {
path: std::path::PathBuf,
}
impl #name {
pub fn new(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
let path_buf = path.into();
if path_buf.as_os_str().is_empty() {
return Err(std::io::Error::from(std::io::ErrorKind::InvalidFilename));
}
Ok(Self { path: path_buf })
}
pub fn as_path(&self) -> &std::path::Path {
&self.path
}
pub fn exists(&self) -> bool {
self.path.exists()
}
pub fn as_generic(&self) -> ::tree_type::GenericFile {
::tree_type::GenericFile::new(self.path.clone()).expect("Path validation already performed")
}
pub fn read(&self) -> std::io::Result<Vec<u8>> {
::tree_type::fs::read(&self.path)
}
pub fn read_to_string(&self) -> std::io::Result<String> {
::tree_type::fs::read_to_string(&self.path)
}
pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
if let Some(parent) = self.path.parent() {
if !parent.exists() {
::tree_type::fs::create_dir_all(parent)?;
}
}
::tree_type::fs::write(&self.path, contents)
}
pub fn remove(&self) -> std::io::Result<()> {
::tree_type::fs::remove_file(&self.path)
}
pub fn fs_metadata(&self) -> std::io::Result<::tree_type::fs::Metadata> {
::tree_type::fs::metadata(&self.path)
}
pub fn file_name(&self) -> String {
self.path.file_name()
.expect("validated in new")
.to_string_lossy()
.to_string()
}
#[cfg(unix)]
pub fn secure(&self) -> std::io::Result<()> {
self.as_generic().secure()
}
pub fn from_generic(file: ::tree_type::GenericFile) -> Self {
Self { path: file.as_path().to_path_buf() }
}
#default_method
#parent_method
}
impl AsRef<std::path::Path> for #name {
fn as_ref(&self) -> &std::path::Path {
&self.path
}
}
#display_impl
#debug_impl
impl From<#name> for ::tree_type::GenericFile {
fn from(file: #name) -> Self {
Self::new(file.path).expect("Path validation already performed")
}
}
}
}
fn generate_dir_struct(
name: &Ident,
children: &[Child],
depth: usize,
root_children: &[Child],
parent_type: Option<&Ident>,
) -> proc_macro2::TokenStream {
let nav_methods = children
.iter()
.map(|child| generate_nav_method(name, child));
let children_method = generate_children_method(children, false, false);
let parent_method = generate_parent_method(parent_type);
let validate_impl = generate_validate_method(children);
let setup_impl = generate_setup_method(children, depth, root_children);
let ensure_impl = generate_ensure_method(children);
let sync_impl = generate_sync_method(children);
let serde_derives = get_serde_derives();
let walk_fns = build_walk_fns();
let display_impl = generate_display_impl(name);
let debug_impl = generate_debug_impl(name);
quote! {
#serde_derives
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct #name {
path: std::path::PathBuf,
}
impl #name {
pub fn new(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
let path_buf = path.into();
if path_buf.as_os_str().is_empty() {
return Err(std::io::Error::from(std::io::ErrorKind::InvalidFilename));
}
Ok(Self { path: path_buf })
}
pub fn as_path(&self) -> &std::path::Path {
&self.path
}
pub fn exists(&self) -> bool {
self.path.exists()
}
pub fn as_generic(&self) -> ::tree_type::GenericDir {
::tree_type::GenericDir::new(self.path.clone()).expect("Path validation already performed")
}
pub fn create(&self) -> std::io::Result<()> {
::tree_type::fs::create_dir(&self.path)
}
pub fn create_all(&self) -> std::io::Result<()> {
::tree_type::fs::create_dir_all(&self.path)
}
pub fn remove(&self) -> std::io::Result<()> {
::tree_type::fs::remove_dir(&self.path)
}
pub fn remove_all(&self) -> std::io::Result<()> {
::tree_type::fs::remove_dir_all(&self.path)
}
pub fn read_dir(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<::tree_type::GenericPath>>> {
::tree_type::fs::read_dir(&self.path)
.map(|read_dir| read_dir.map(|result| result.and_then(::tree_type::GenericPath::try_from)))
}
pub fn fs_metadata(&self) -> std::io::Result<::tree_type::fs::Metadata> {
::tree_type::fs::metadata(&self.path)
}
#[cfg(unix)]
pub fn secure(&self) -> std::io::Result<()> {
self.as_generic().secure()
}
#walk_fns
#validate_impl
#setup_impl
#ensure_impl
#sync_impl
pub fn file_name(&self) -> String {
self.path.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default()
}
pub fn from_generic(dir: ::tree_type::GenericDir) -> Self {
Self { path: dir.as_path().to_path_buf() }
}
#(#nav_methods)*
#children_method
#parent_method
}
impl AsRef<std::path::Path> for #name {
fn as_ref(&self) -> &std::path::Path {
&self.path
}
}
#display_impl
#debug_impl
impl From<#name> for ::tree_type::GenericDir {
fn from(dir: #name) -> Self {
Self::new(dir.path).expect("Path validation already performed")
}
}
}
}
fn capitalize(s: &str) -> String {
s.split('_')
.filter(|part| !part.is_empty())
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect()
}
fn generate_validate_method(children: &[Child]) -> proc_macro2::TokenStream {
generate_validate_method_with_parent_info(children, false, false)
}
fn generate_validate_method_with_parent_info(
children: &[Child],
parent_has_dynamic_id: bool,
current_is_dynamic_id: bool,
) -> proc_macro2::TokenStream {
let validations = children.iter().map(|child| {
generate_child_validate_method_with_parent_info(
parent_has_dynamic_id,
current_is_dynamic_id,
child,
)
});
let recursive_validations = children.iter().filter_map(|child| match child {
Child::Directory { name, .. } => Some(quote! {
if self.#name().exists() {
#[expect(deprecated)]
let child_report = self.#name().validate();
report.merge(child_report);
}
}),
_ => None,
});
quote! {
#[allow(clippy::regex_creation_in_loops)]
#[deprecated(since = "0.2.0", note = "Use `sync()` instead. This method will be removed in v0.6.0")]
#[expect(deprecated)]
pub fn validate(&self) -> ::tree_type::ValidationReport {
let mut report = ::tree_type::ValidationReport::new();
if !self.exists() {
report.errors.push(tree_type::ValidationError {
path: self.path.clone(),
message: "Directory does not exist".to_string(),
});
}
#(#validations)*
#(#recursive_validations)*
report
}
}
}
fn generate_child_validate_method_with_parent_info(
parent_has_dynamic_id: bool,
current_is_dynamic_id: bool,
child: &Child,
) -> proc_macro2::TokenStream {
if let Child::DynamicId {
child_type,
attributes,
is_directory,
id_type,
..
} = child
{
generate_child_dynamic_id_validate_method_with_parent_info(
parent_has_dynamic_id,
current_is_dynamic_id,
child_type,
attributes,
*is_directory,
id_type,
)
} else {
let name = child.name();
let pattern = child.attributes().iter().find_map(|attr| {
if let Attribute::Pattern(lit) = attr {
Some(lit)
} else {
None
}
});
let custom_validator = child.attributes().iter().find_map(|attr| {
if let Attribute::Validate(expr) = attr {
Some(expr)
} else {
None
}
});
if let Some(pattern_lit) = pattern {
quote! {
{
if self.#name().exists() {
match self.#name().read_to_string() {
Ok(content) => {
match ::tree_type::deps::pattern_validation::Regex::new(#pattern_lit) {
Ok(re) => {
if !re.is_match(&content) {
report.errors.push(tree_type::ValidationError {
path: self.#name().as_path().to_path_buf(),
message: format!("File content does not match pattern: {}", #pattern_lit),
});
}
}
Err(e) => {
report.errors.push(tree_type::ValidationError {
path: self.#name().as_path().to_path_buf(),
message: format!("Invalid regex pattern: {}", e),
});
}
}
}
Err(e) => {
report.errors.push(tree_type::ValidationError {
path: self.#name().as_path().to_path_buf(),
message: format!("Failed to read file: {}", e),
});
}
}
}
}
}
} else if let Some(validator) = custom_validator {
quote! {
let result = (#validator)(&self.#name());
for error in result.errors {
report.errors.push(tree_type::ValidationError {
path: self.#name().as_path().to_path_buf(),
message: error,
});
}
for warning in result.warnings {
report.warnings.push(tree_type::ValidationWarning {
path: self.#name().as_path().to_path_buf(),
message: warning,
});
}
}
} else if child.is_required() {
quote! {
if !self.#name().exists() {
report.errors.push(tree_type::ValidationError {
path: self.#name().as_path().to_path_buf(),
message: "Required path does not exist".to_string(),
});
}
}
} else {
quote! {}
}
}
}
fn generate_child_dynamic_id_validate_method_with_parent_info(
parent_has_dynamic_id: bool,
current_is_dynamic_id: bool,
child_type: &Ident,
attributes: &[Attribute],
is_directory: bool,
id_type: &syn::Type,
) -> proc_macro2::TokenStream {
let pattern = attributes.iter().find_map(|attr| {
if let Attribute::Pattern(lit) = attr {
Some(lit)
} else {
None
}
});
let custom_validator = attributes.iter().find_map(|attr| {
if let Attribute::Validate(expr) = attr {
Some(expr)
} else {
None
}
});
let constructor_call = if parent_has_dynamic_id {
quote! { #child_type::new(entry_path.clone(), id, self.parent_id.clone()).expect("Path validation already performed") }
} else if current_is_dynamic_id {
quote! { #child_type::new(entry_path.clone(), id, self.id.clone()).expect("Path validation already performed") }
} else {
quote! { #child_type::new(entry_path.clone(), id).expect("Path validation already performed") }
};
let pattern_validation =
pattern.map(generate_child_dynamic_id_pattern_validate_method_with_parent_info);
let validator_call = custom_validator
.map(generate_child_dynamic_id_custom_validator_validate_method_with_parent_info);
let recursive_validation = if is_directory {
quote! {
#[expect(deprecated)]
let child_report = child_instance.validate();
report.merge(child_report);
}
} else {
quote! {
}
};
quote! {
if self.exists() {
let read_result = ::tree_type::fs::read_dir(&self.path);
match read_result {
Ok(entries) => {
for entry in entries {
match entry {
Ok(entry) => {
let entry_path = entry.path();
let is_expected_type = if #is_directory {
entry_path.is_dir()
} else {
entry_path.is_file()
};
if !is_expected_type {
continue;
}
#pattern_validation
if let Some(filename) = entry_path.file_name() {
if let Some(id_str) = filename.to_str() {
match id_str.parse::<#id_type>() {
Ok(id) => {
let child_instance = #constructor_call;
#validator_call
#recursive_validation
}
Err(_) => {
continue;
}
}
}
}
}
Err(e) => {
report.errors.push(tree_type::ValidationError {
path: self.path.clone(),
message: format!("Failed to read directory entry: {}", e),
});
}
}
}
}
Err(e) => {
report.errors.push(tree_type::ValidationError {
path: self.path.clone(),
message: format!("Failed to read directory: {}", e),
});
}
}
}
}
}
fn generate_child_dynamic_id_custom_validator_validate_method_with_parent_info(
_validator: &syn::Expr,
) -> proc_macro2::TokenStream {
quote! {
let result = (#_validator)(&child_instance);
for error in result.errors {
report.errors.push(tree_type::ValidationError {
path: entry_path.clone(),
message: error,
});
}
for warning in result.warnings {
report.warnings.push(tree_type::ValidationWarning {
path: entry_path.clone(),
message: warning,
});
}
}
}
fn generate_child_dynamic_id_pattern_validate_method_with_parent_info(
pattern_lit: &syn::LitStr,
) -> proc_macro2::TokenStream {
quote! {
{
let dir_name = entry.file_name();
let name_str = dir_name.to_string_lossy();
match ::tree_type::deps::pattern_validation::Regex::new(#pattern_lit) {
Ok(re) => {
if !re.is_match(&name_str) {
report.errors.push(tree_type::ValidationError {
path: entry_path.clone(),
message: format!("Directory name '{}' does not match pattern: {}", name_str, #pattern_lit),
});
continue;
}
}
Err(e) => {
report.errors.push(tree_type::ValidationError {
path: entry_path.clone(),
message: format!("Invalid regex pattern: {}", e),
});
continue;
}
}
}
}
}
#[expect(clippy::too_many_lines)]
fn generate_setup_method(
children: &[Child],
depth: usize,
root_children: &[Child],
) -> proc_macro2::TokenStream {
let setups = children.iter().filter_map(|child| {
let name = child.name();
if let Some(target_path) = child.get_symlink_target() {
let target_str = target_path.value();
if !target_str.contains('/') {
let target_ident = syn::Ident::new(&target_str, target_path.span());
return Some(quote! {
if !self.#name().as_path().exists() {
let target_filename = self.#target_ident().as_path().file_name().unwrap().to_string_lossy().to_string();
#[cfg(unix)]
if let Err(e) = std::os::unix::fs::symlink(&target_filename, self.#name().as_path()) {
errors.push(tree_type::BuildError::File(
self.#name().as_path().to_path_buf(),
Box::new(e)
));
}
#[cfg(windows)]
{
let result = if self.#target_ident().as_path().is_dir() {
std::os::windows::fs::symlink_dir(&target_filename, self.#name().as_path())
} else {
std::os::windows::fs::symlink_file(&target_filename, self.#name().as_path())
};
if let Err(e) = result {
errors.push(tree_type::BuildError::File(
self.#name().as_path().to_path_buf(),
Box::new(e)
));
}
}
}
});
}
let up_dirs_str = "../".repeat(depth);
if let Some(resolved_code) = resolve_symlink_target_path(&target_str, root_children, &up_dirs_str, depth) {
return Some(quote! {
if !self.#name().as_path().exists() {
let relative_target = #resolved_code;
#[cfg(unix)]
if let Err(e) = std::os::unix::fs::symlink(&relative_target, self.#name().as_path()) {
errors.push(tree_type::BuildError::File(
self.#name().as_path().to_path_buf(),
Box::new(e)
));
}
#[cfg(windows)]
{
let result = std::os::windows::fs::symlink_file(&relative_target, self.#name().as_path());
if let Err(e) = result {
errors.push(tree_type::BuildError::File(
self.#name().as_path().to_path_buf(),
Box::new(e)
));
}
}
}
});
}
return Some(quote! {
if !self.#name().as_path().exists() {
let target_str = #target_str;
let relative_target = if let Some(path_without_slash) = target_str.strip_prefix('/') {
let up_dirs = #up_dirs_str;
format!("{}{}", up_dirs, path_without_slash)
} else if target_str.contains('.') {
let up_dirs = #up_dirs_str;
format!("{}{}", up_dirs, target_str)
} else {
let up_dirs = #up_dirs_str;
format!("{}{}", up_dirs, target_str)
};
#[cfg(unix)]
if let Err(e) = std::os::unix::fs::symlink(&relative_target, self.#name().as_path()) {
errors.push(tree_type::BuildError::File(
self.#name().as_path().to_path_buf(),
Box::new(e)
));
}
#[cfg(windows)]
{
let result = std::os::windows::fs::symlink_file(&relative_target, self.#name().as_path());
if let Err(e) = result {
errors.push(tree_type::BuildError::File(
self.#name().as_path().to_path_buf(),
Box::new(e)
));
}
}
}
});
}
match child {
Child::Directory { .. } => {
Some(quote! {
#[expect(deprecated)]
if let Err(child_errors) = self.#name().setup() {
errors.extend(child_errors);
}
})
}
Child::File { attributes, .. } => {
let has_default = attributes.iter().any(|attr| matches!(attr, Attribute::Default(_)));
if has_default {
Some(quote! {
if let Err(e) = self.#name().create_default::<std::io::Error>() {
errors.push(tree_type::BuildError::File(
self.#name().as_path().to_path_buf(),
Box::new(e)
));
}
})
} else {
None
}
}
Child::DynamicId { .. } => None
}
});
let identity_symlinks = children.iter().filter_map(|child| {
match child {
Child::File { name, custom_filename, attributes, .. } => {
let has_symlink = attributes.iter().any(|attr| matches!(attr, Attribute::Symlink(_)));
if has_symlink {
return None;
}
if let Some(filename_lit) = custom_filename {
let filename_str = filename_lit.value();
let identity_name = name.to_string();
if identity_name != filename_str {
return Some(quote! {
let identity_path = self.as_path().join(#identity_name);
if !identity_path.exists() {
#[cfg(unix)]
if let Err(e) = std::os::unix::fs::symlink(#filename_str, &identity_path) {
errors.push(tree_type::BuildError::File(
identity_path,
Box::new(e)
));
}
#[cfg(windows)]
{
let result = std::os::windows::fs::symlink_file(#filename_str, &identity_path);
if let Err(e) = result {
errors.push(tree_type::BuildError::File(
identity_path,
Box::new(e)
));
}
}
}
});
}
}
None
}
_ => None
}
});
quote! {
#[deprecated(since = "0.2.0", note = "Use `sync()` instead. This method will be removed in v0.6.0")]
#[expect(deprecated)]
pub fn setup(&self) -> std::result::Result<Vec<::tree_type::BuildError>, Vec<::tree_type::BuildError>> {
let mut errors = Vec::new();
if !self.path.exists() {
let create_result = ::tree_type::fs::create_dir_all(&self.path);
if let Err(e) = create_result {
errors.push(tree_type::BuildError::Directory(
self.path.clone(),
e
));
return Err(errors);
}
}
#(#setups)*
#(#identity_symlinks)*
if errors.is_empty() {
Ok(Vec::new())
} else {
Err(errors)
}
}
}
}
fn generate_ensure_method(_children: &[Child]) -> proc_macro2::TokenStream {
quote! {
#[deprecated(since = "0.2.0", note = "Use `sync()` instead. This method will be removed in v0.6.0")]
#[expect(deprecated)]
pub fn ensure(&self) -> std::result::Result<::tree_type::ValidationReport, Vec<::tree_type::BuildError>> {
self.setup()?;
Ok(self.validate())
}
}
}
fn generate_sync_method(_children: &[Child]) -> proc_macro2::TokenStream {
quote! {
#[expect(deprecated)]
pub fn sync(&self) -> std::result::Result<::tree_type::ValidationReport, Vec<::tree_type::BuildError>> {
self.setup()?;
Ok(self.validate())
}
}
}
fn generate_display_impl(name: &syn::Ident) -> proc_macro2::TokenStream {
quote! {
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path.display())
}
}
}
}
fn generate_debug_impl(name: &syn::Ident) -> proc_macro2::TokenStream {
quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}({})", stringify!(#name), self.path.display())
}
}
}
}
fn generate_dynamic_file_struct(
name: &Ident,
attributes: &[Attribute],
parent_type: Option<&Ident>,
id_name: &Ident,
id_type: &syn::Type,
) -> proc_macro2::TokenStream {
let default_method = attributes.iter().find_map(|attr| {
if let Attribute::Default(val) = attr {
Some(generate_create_default_method(val))
} else {
None
}
});
let parent_method = generate_parent_method(parent_type);
let serde_derives = get_serde_derives();
let display_impl = generate_display_impl(name);
let debug_impl = generate_debug_impl(name);
let id_getter = quote! {
pub fn #id_name(&self) -> &#id_type {
&self.id
}
};
quote! {
#serde_derives
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct #name {
path: std::path::PathBuf,
id: #id_type,
}
impl #name {
pub fn new(path: impl Into<std::path::PathBuf>, id: #id_type) -> std::io::Result<Self> {
let path_buf = path.into();
if path_buf.as_os_str().is_empty() {
return Err(std::io::Error::from(std::io::ErrorKind::InvalidFilename));
}
Ok(Self { path: path_buf, id })
}
pub fn as_path(&self) -> &std::path::Path {
&self.path
}
pub fn exists(&self) -> bool {
self.path.exists()
}
pub fn as_generic(&self) -> ::tree_type::GenericFile {
::tree_type::GenericFile::new(self.path.clone()).expect("Path validation already performed")
}
pub fn read(&self) -> std::io::Result<Vec<u8>> {
::tree_type::fs::read(&self.path)
}
pub fn read_to_string(&self) -> std::io::Result<String> {
::tree_type::fs::read_to_string(&self.path)
}
pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
if let Some(parent) = self.path.parent() {
if !parent.exists() {
::tree_type::fs::create_dir_all(parent)?;
}
}
::tree_type::fs::write(&self.path, contents)
}
pub fn remove(&self) -> std::io::Result<()> {
::tree_type::fs::remove_file(&self.path)
}
pub fn fs_metadata(&self) -> std::io::Result<::tree_type::fs::Metadata> {
::tree_type::fs::metadata(&self.path)
}
pub fn file_name(&self) -> String {
self.path.file_name()
.expect("validated in new")
.to_string_lossy()
.to_string()
}
#[cfg(unix)]
pub fn secure(&self) -> std::io::Result<()> {
self.as_generic().secure()
}
pub fn from_generic(file: ::tree_type::GenericFile, id: #id_type) -> Self {
Self { path: file.as_path().to_path_buf(), id }
}
#id_getter
#default_method
#parent_method
}
impl AsRef<std::path::Path> for #name {
fn as_ref(&self) -> &std::path::Path {
&self.path
}
}
#display_impl
#debug_impl
impl From<#name> for ::tree_type::GenericFile {
fn from(file: #name) -> Self {
Self::new(file.path).expect("Path validation already performed")
}
}
}
}
fn generate_dynamic_file_struct_with_dynamic_parent(
name: &Ident,
attributes: &[Attribute],
parent_type: &Ident,
id_name: &Ident,
id_type: &syn::Type,
parent_id_type: &syn::Type,
parent_is_nested: bool,
) -> proc_macro2::TokenStream {
let default_method = attributes.iter().find_map(|attr| {
if let Attribute::Default(val) = attr {
Some(generate_create_default_method(val))
} else {
None
}
});
let serde_derives = get_serde_derives();
let display_impl = generate_display_impl(name);
let debug_impl = generate_debug_impl(name);
let id_getter = quote! {
pub fn #id_name(&self) -> &#id_type {
&self.id
}
};
let parent_method = if parent_is_nested {
generate_dynamic_file_with_nested_parent_struct_with_dynamic_parent(parent_type)
} else {
generate_dynamic_file_with_non_nested_parent_struct_with_dynamic_parent(parent_type)
};
quote! {
#serde_derives
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct #name {
path: std::path::PathBuf,
id: #id_type,
parent_id: #parent_id_type,
}
impl #name {
pub fn new(path: impl Into<std::path::PathBuf>, id: #id_type, parent_id: #parent_id_type) -> std::io::Result<Self> {
let path_buf = path.into();
if path_buf.as_os_str().is_empty() {
return Err(std::io::Error::from(std::io::ErrorKind::InvalidFilename));
}
Ok(Self { path: path_buf, id, parent_id })
}
pub fn as_path(&self) -> &std::path::Path {
&self.path
}
pub fn exists(&self) -> bool {
self.path.exists()
}
pub fn as_generic(&self) -> ::tree_type::GenericFile {
::tree_type::GenericFile::new(self.path.clone()).expect("Path validation already performed")
}
pub fn read(&self) -> std::io::Result<Vec<u8>> {
::tree_type::fs::read(&self.path)
}
pub fn read_to_string(&self) -> std::io::Result<String> {
::tree_type::fs::read_to_string(&self.path)
}
pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
if let Some(parent) = self.path.parent() {
if !parent.exists() {
::tree_type::fs::create_dir_all(parent)?;
}
}
::tree_type::fs::write(&self.path, contents)
}
pub fn remove(&self) -> std::io::Result<()> {
::tree_type::fs::remove_file(&self.path)
}
pub fn fs_metadata(&self) -> std::io::Result<::tree_type::fs::Metadata> {
::tree_type::fs::metadata(&self.path)
}
pub fn file_name(&self) -> String {
self.path.file_name()
.expect("validated in new")
.to_string_lossy()
.to_string()
}
#[cfg(unix)]
pub fn secure(&self) -> std::io::Result<()> {
self.as_generic().secure()
}
pub fn from_generic(file: ::tree_type::GenericFile, id: #id_type, parent_id: #parent_id_type) -> Self {
Self { path: file.as_path().to_path_buf(), id, parent_id }
}
#id_getter
#default_method
#parent_method
}
impl AsRef<std::path::Path> for #name {
fn as_ref(&self) -> &std::path::Path {
&self.path
}
}
#display_impl
#debug_impl
impl From<#name> for ::tree_type::GenericFile {
fn from(file: #name) -> Self {
Self::new(file.path).expect("Path validation already performed")
}
}
}
}
fn generate_dynamic_file_with_non_nested_parent_struct_with_dynamic_parent(
parent_type: &Ident,
) -> proc_macro2::TokenStream {
quote! {
pub fn parent(&self) -> #parent_type {
let parent_path = self.path.parent().expect("Path should have a parent");
#parent_type::new(parent_path, self.parent_id.clone()).expect("Path validation already performed")
}
}
}
fn generate_dynamic_file_with_nested_parent_struct_with_dynamic_parent(
parent_type: &Ident,
) -> proc_macro2::TokenStream {
quote! {
pub fn parent(&self) -> #parent_type {
let parent_path = self.path.parent().expect("Path should have a parent");
let grandparent_path = parent_path.parent().expect("Parent should have a grandparent");
let grandparent_id = grandparent_path.file_name()
.expect("Grandparent path should have a filename")
.to_string_lossy()
.to_string();
#parent_type::new(parent_path, self.parent_id.clone(), grandparent_id).expect("Path validation already performed")
}
}
}
fn generate_dynamic_dir_struct(
name: &Ident,
children: &[Child],
depth: usize,
root_children: &[Child],
parent_type: Option<&Ident>,
id_name: &Ident,
id_type: &syn::Type,
) -> proc_macro2::TokenStream {
let nav_methods = children
.iter()
.map(|child| generate_nav_method_for_dynamic_id_parent(name, child, id_name));
let children_method = generate_children_method(children, false, true);
let parent_method = generate_parent_method(parent_type);
let validate_impl = generate_validate_method_with_parent_info(children, false, true);
let setup_impl = generate_setup_method(children, depth, root_children);
let ensure_impl = generate_ensure_method(children);
let sync_impl = generate_sync_method(children);
let serde_derives = get_serde_derives();
let walk_fns = build_walk_fns();
let display_impl = generate_display_impl(name);
let debug_impl = generate_debug_impl(name);
let id_getter = quote! {
pub fn #id_name(&self) -> &#id_type {
&self.id
}
};
quote! {
#serde_derives
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct #name {
path: std::path::PathBuf,
id: #id_type,
}
impl #name {
pub fn new(path: impl Into<std::path::PathBuf>, id: #id_type) -> std::io::Result<Self> {
let path_buf = path.into();
if path_buf.as_os_str().is_empty() {
return Err(std::io::Error::from(std::io::ErrorKind::InvalidFilename));
}
Ok(Self { path: path_buf, id })
}
pub fn as_path(&self) -> &std::path::Path {
&self.path
}
pub fn exists(&self) -> bool {
self.path.exists()
}
pub fn as_generic(&self) -> ::tree_type::GenericDir {
::tree_type::GenericDir::new(self.path.clone()).expect("Path validation already performed")
}
pub fn create(&self) -> std::io::Result<()> {
::tree_type::fs::create_dir(&self.path)
}
pub fn create_all(&self) -> std::io::Result<()> {
::tree_type::fs::create_dir_all(&self.path)
}
pub fn remove(&self) -> std::io::Result<()> {
::tree_type::fs::remove_dir(&self.path)
}
pub fn remove_all(&self) -> std::io::Result<()> {
::tree_type::fs::remove_dir_all(&self.path)
}
pub fn read_dir(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<::tree_type::GenericPath>>> {
::tree_type::fs::read_dir(&self.path)
.map(|read_dir| read_dir.map(|result| result.and_then(::tree_type::GenericPath::try_from)))
}
pub fn fs_metadata(&self) -> std::io::Result<::tree_type::fs::Metadata> {
::tree_type::fs::metadata(&self.path)
}
#[cfg(unix)]
pub fn secure(&self) -> std::io::Result<()> {
self.as_generic().secure()
}
#walk_fns
#validate_impl
#setup_impl
#ensure_impl
#sync_impl
pub fn file_name(&self) -> String {
self.path.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default()
}
pub fn from_generic(dir: ::tree_type::GenericDir, id: #id_type) -> Self {
Self { path: dir.as_path().to_path_buf(), id }
}
#id_getter
#(#nav_methods)*
#children_method
#parent_method
}
impl AsRef<std::path::Path> for #name {
fn as_ref(&self) -> &std::path::Path {
&self.path
}
}
#display_impl
#debug_impl
impl From<#name> for ::tree_type::GenericDir {
fn from(dir: #name) -> Self {
Self::new(dir.path).expect("Path validation already performed")
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn generate_dynamic_dir_struct_with_dynamic_parent(
name: &Ident,
children: &[Child],
depth: usize,
root_children: &[Child],
parent_type: &Ident,
id_name: &Ident,
id_type: &syn::Type,
parent_id_type: &syn::Type,
parent_is_nested: bool,
) -> proc_macro2::TokenStream {
let nav_methods = children
.iter()
.map(|child| generate_nav_method_for_dynamic_id_parent(name, child, id_name));
let children_method = generate_children_method(children, false, true);
let validate_impl = generate_validate_method_with_parent_info(children, false, true);
let setup_impl = generate_setup_method(children, depth, root_children);
let ensure_impl = generate_ensure_method(children);
let sync_impl = generate_sync_method(children);
let serde_derives = get_serde_derives();
let walk_fns = build_walk_fns();
let display_impl = generate_display_impl(name);
let debug_impl = generate_debug_impl(name);
let id_getter = generate_id_getter_method(id_name, id_type);
let parent_method =
generate_dynamic_dir_parent_struct_with_dynamic_parent(parent_type, parent_is_nested);
quote! {
#serde_derives
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct #name {
path: std::path::PathBuf,
id: #id_type,
parent_id: #parent_id_type,
}
impl #name {
pub fn new(path: impl Into<std::path::PathBuf>, id: #id_type, parent_id: #parent_id_type) -> std::io::Result<Self> {
let path_buf = path.into();
if path_buf.as_os_str().is_empty() {
return Err(std::io::Error::from(std::io::ErrorKind::InvalidFilename));
}
Ok(Self { path: path_buf, id, parent_id })
}
pub fn as_path(&self) -> &std::path::Path {
&self.path
}
pub fn exists(&self) -> bool {
self.path.exists()
}
pub fn as_generic(&self) -> ::tree_type::GenericDir {
::tree_type::GenericDir::new(self.path.clone()).expect("Path validation already performed")
}
pub fn create(&self) -> std::io::Result<()> {
::tree_type::fs::create_dir(&self.path)
}
pub fn create_all(&self) -> std::io::Result<()> {
::tree_type::fs::create_dir_all(&self.path)
}
pub fn remove(&self) -> std::io::Result<()> {
::tree_type::fs::remove_dir(&self.path)
}
pub fn remove_all(&self) -> std::io::Result<()> {
::tree_type::fs::remove_dir_all(&self.path)
}
pub fn read_dir(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<::tree_type::GenericPath>>> {
::tree_type::fs::read_dir(&self.path)
.map(|read_dir| read_dir.map(|result| result.and_then(::tree_type::GenericPath::try_from)))
}
pub fn fs_metadata(&self) -> std::io::Result<::tree_type::fs::Metadata> {
::tree_type::fs::metadata(&self.path)
}
#[cfg(unix)]
pub fn secure(&self) -> std::io::Result<()> {
self.as_generic().secure()
}
#walk_fns
#validate_impl
#setup_impl
#ensure_impl
#sync_impl
pub fn file_name(&self) -> String {
self.path.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default()
}
pub fn from_generic(dir: ::tree_type::GenericDir, id: #id_type, parent_id: #parent_id_type) -> Self {
Self { path: dir.as_path().to_path_buf(), id, parent_id }
}
#id_getter
#(#nav_methods)*
#children_method
#parent_method
}
impl AsRef<std::path::Path> for #name {
fn as_ref(&self) -> &std::path::Path {
&self.path
}
}
#display_impl
#debug_impl
impl From<#name> for ::tree_type::GenericDir {
fn from(dir: #name) -> Self {
Self::new(dir.path).expect("Path validation already performed")
}
}
}
}
fn generate_dynamic_dir_parent_struct_with_dynamic_parent(
parent_type: &Ident,
parent_is_nested: bool,
) -> proc_macro2::TokenStream {
if parent_is_nested {
generate_dynamic_dir_with_nested_parent_struct_with_dynamic_parent(parent_type)
} else {
generate_dynamic_dir_with_non_nested_parent_struct_with_dynamic_parent(parent_type)
}
}
fn generate_id_getter_method(id_name: &Ident, id_type: &syn::Type) -> proc_macro2::TokenStream {
quote! {
pub fn #id_name(&self) -> &#id_type {
&self.id
}
}
}
fn generate_dynamic_dir_with_non_nested_parent_struct_with_dynamic_parent(
parent_type: &Ident,
) -> proc_macro2::TokenStream {
quote! {
pub fn parent(&self) -> #parent_type {
let parent_path = self.path.parent().expect("Path should have a parent");
#parent_type::new(parent_path, self.parent_id.clone()).expect("Path validation already performed")
}
}
}
fn generate_dynamic_dir_with_nested_parent_struct_with_dynamic_parent(
parent_type: &Ident,
) -> proc_macro2::TokenStream {
quote! {
pub fn parent(&self) -> #parent_type {
let parent_path = self.path.parent().expect("Path should have a parent");
let grandparent_path = parent_path.parent().expect("Parent should have a grandparent");
let grandparent_id = grandparent_path.file_name()
.expect("Grandparent path should have a filename")
.to_string_lossy()
.to_string();
#parent_type::new(parent_path, self.parent_id.clone(), grandparent_id).expect("Path validation already performed")
}
}
}
#[allow(dead_code)]
fn generate_child_structs_with_path(
parent_name: &Ident,
children: &[Child],
structs: &mut Vec<proc_macro2::TokenStream>,
root_children: &[Child],
) {
for child in children {
match child {
Child::File {
name,
custom_type,
attributes,
..
} => {
let struct_name = get_child_type_name(parent_name, name, custom_type.as_ref());
let mut path = Vec::new();
let mut result = None;
collect_dynamic_ancestors(
root_children,
&struct_name.to_string(),
&mut path,
&mut result,
);
let ancestors = result.unwrap_or_default();
structs.push(generate_file_struct_generic(
&struct_name,
attributes,
parent_name,
&ancestors,
));
}
Child::Directory {
name,
custom_type,
children,
..
} => {
let struct_name = get_child_type_name(parent_name, name, custom_type.as_ref());
structs.push(quote! {
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct #struct_name {
path: std::path::PathBuf,
}
impl #struct_name {
pub fn new(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
Ok(Self { path: path.into() })
}
}
});
generate_child_structs_with_path(&struct_name, children, structs, root_children);
}
Child::DynamicId {
child_type,
children,
..
} => {
generate_child_structs_with_path(child_type, children, structs, root_children);
}
}
}
}
fn generate_file_struct_generic(
struct_name: &Ident,
#[expect(unused)] attributes: &[Attribute],
parent_name: &Ident,
ancestors: &[DynamicIdInfo],
) -> proc_macro2::TokenStream {
#[expect(unused)]
let parent_name_lower = parent_name.to_string().to_lowercase();
let constructor_params = match ancestors.len() {
0 => quote! { path: impl Into<std::path::PathBuf>, parent_id: impl std::fmt::Display },
1 => quote! {
path: impl Into<std::path::PathBuf>,
parent_id: impl std::fmt::Display,
grandparent_id: impl std::fmt::Display
},
_ => {
let mut params = vec![
quote! { path: impl Into<std::path::PathBuf> },
quote! { parent_id: impl std::fmt::Display },
];
for i in 0..ancestors.len() {
let param_name = format_ident!("ancestor_{}", i);
params.push(quote! { #param_name: impl std::fmt::Display });
}
quote! { #(#params),* }
}
};
let constructor_body = quote! {
let path = path.into();
Ok(Self { path })
};
let parent_method = generate_nav_method_generic(parent_name, ancestors);
quote! {
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct #struct_name {
path: std::path::PathBuf,
}
impl #struct_name {
pub fn new(#constructor_params) -> std::io::Result<Self> {
#constructor_body
}
#parent_method
}
}
}
fn generate_nav_method_generic(
parent_name: &Ident,
ancestors: &[DynamicIdInfo],
) -> proc_macro2::TokenStream {
let parent_name_lower = parent_name.to_string().to_lowercase();
let method_name = format_ident!("{}", parent_name_lower);
let constructor_call = match ancestors.len() {
0 => quote! { #parent_name::new(parent_path) },
1 => quote! { #parent_name::new(parent_path, self.parent_id) },
_ => {
let mut args = vec![quote! { parent_path }];
for i in 0..ancestors.len() {
let field_name = format_ident!("ancestor_{}", i);
args.push(quote! { self.#field_name });
}
quote! { #parent_name::new(#(#args),*) }
}
};
quote! {
pub fn #method_name(&self) -> std::io::Result<#parent_name> {
let parent_path = self.path.parent()
.ok_or_else(|| std::io::Error::new(
std::io::ErrorKind::NotFound,
"No parent directory"
))?;
#constructor_call
}
}
}
#[allow(dead_code)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_dynamic_ancestors() {
let mut path = Vec::new();
let mut result = None;
collect_dynamic_ancestors(&[], "NonExistent", &mut path, &mut result);
assert!(result.is_none());
}
#[test]
fn test_generic_vs_hardcoded_output_comparison() {
let struct_name = syn::Ident::new("TestFile", proc_macro2::Span::call_site());
let parent_name = syn::Ident::new("TestParent", proc_macro2::Span::call_site());
let ancestors_empty = vec![];
let generic_output =
generate_file_struct_generic(&struct_name, &[], &parent_name, &ancestors_empty);
let generic_str = generic_output.to_string();
println!("Generated output: {}", generic_str);
assert!(generic_str.contains("pub struct TestFile"));
assert!(generic_str.contains("path")); assert!(generic_str.contains("pub fn new"));
assert!(generic_str.contains("parent_id"));
let ancestors_one = vec![DynamicIdInfo {
id_type: quote! { String },
}];
let generic_output_one =
generate_file_struct_generic(&struct_name, &[], &parent_name, &ancestors_one);
let generic_str_one = generic_output_one.to_string();
assert!(generic_str_one.contains("grandparent_id"));
let ancestors_two = vec![
DynamicIdInfo {
id_type: quote! { u32 },
},
DynamicIdInfo {
id_type: quote! { Uuid },
},
];
let generic_output_two =
generate_file_struct_generic(&struct_name, &[], &parent_name, &ancestors_two);
let generic_str_two = generic_output_two.to_string();
assert!(generic_str_two.contains("ancestor_0"));
assert!(generic_str_two.contains("ancestor_1"));
}
}