tree-type-proc-macro 0.4.5

Procedural macros for tree-type crate
Documentation
//! Simple file and directory type generators

use quote::quote;
use syn::Ident;

use crate::codegen::build_walk_fns;
use crate::codegen::get_serde_derives;
use crate::codegen::get_serde_derives_transparent;

/// Generate a simple file type with proper feature detection
#[expect(clippy::too_many_lines)]
pub fn generate_file_type(name: &Ident) -> proc_macro2::TokenStream {
    let serde_derives = get_serde_derives_transparent();

    // Special handling for GenericFile to match existing implementation
    if name == "GenericFile" {
        quote! {
            #serde_derives
            #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
            pub struct #name(std::path::PathBuf);

            impl #name {
                pub fn new(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
                    let path_buf = path.into();
                    let file_name = path_buf.file_name();
                    if file_name.is_none() {
                        return Err(std::io::Error::from(std::io::ErrorKind::InvalidFilename));
                    }
                    Ok(Self(path_buf))
                }

                pub fn as_path(&self) -> &std::path::Path {
                    &self.0
                }

                pub fn exists(&self) -> bool {
                    self.0.exists()
                }

                pub fn as_generic(&self) -> #name {
                    self.clone()
                }

                pub fn read(&self) -> std::io::Result<Vec<u8>> {
                    ::tree_type::fs::read(&self.0)
                }

                pub fn read_to_string(&self) -> std::io::Result<String> {
                    ::tree_type::fs::read_to_string(&self.0)
                }

                pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
                    if let Some(parent) = self.0.parent() {
                        if !parent.exists() {
                            ::tree_type::fs::create_dir_all(parent)?;
                        }
                    }
                    ::tree_type::fs::write(&self.0, contents)
                }

                pub fn create(&self) -> std::io::Result<()> {
                    if let Some(parent) = self.0.parent() {
                        if !parent.exists() {
                            ::tree_type::fs::create_dir_all(parent)?;
                        }
                    }
                    std::fs::File::create(&self.0)?;
                    Ok(())
                }

                pub fn remove(&self) -> std::io::Result<()> {
                    ::tree_type::fs::remove_file(&self.0)
                }

                pub fn fs_metadata(&self) -> std::io::Result<::tree_type::fs::Metadata> {
                    ::tree_type::fs::metadata(&self.0)
                }

                /// Returns the final component of the path as a String.
                /// See [`std::path::Path::file_name`] for more details.
                pub fn file_name(&self) -> String {
                    self.0.file_name()
                        .expect("validated in new")
                        .to_string_lossy()
                        .to_string()
                }

                /// Get the parent directory as GenericDir.
                /// Returns None for files at the root level.
                pub fn parent(&self) -> Option<::tree_type::GenericDir> {
                    self.0.parent().and_then(|parent_path| {
                        ::tree_type::GenericDir::new(parent_path).ok()
                    })
                }
            }

            impl AsRef<std::path::Path> for #name {
                fn as_ref(&self) -> &std::path::Path {
                    &self.0
                }
            }

            impl std::fmt::Display for #name {
                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                    write!(f, "{}", self.0.display())
                }
            }
        }
    } else {
        quote! {
            #serde_derives
            #[derive(Clone, Debug, 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();
                    let file_name = path_buf.file_name();
                    if file_name.is_none() {
                        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 create(&self) -> std::io::Result<()> {
                    if let Some(parent) = self.path.parent() {
                        if !parent.exists() {
                            ::tree_type::fs::create_dir_all(parent)?;
                        }
                    }
                    std::fs::File::create(&self.path)?;
                    Ok(())
                }

                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 from_generic(file: ::tree_type::GenericFile) -> Self {
                    Self { path: file.as_path().to_path_buf() }
                }

                /// Returns the final component of the path as a String.
                /// See [`std::path::Path::file_name`] for more details.
                pub fn file_name(&self) -> String {
                    self.path.file_name()
                        .expect("validated in new")
                        .to_string_lossy()
                        .to_string()
                }

                /// Set file permissions to 0o600 (read/write for owner only).
                ///
                /// This method is only available on Unix systems.
                #[cfg(unix)]
                pub fn secure(&self) -> std::io::Result<()> {
                    self.as_generic().secure()
                }

                /// Get the parent directory as GenericDir.
                /// Returns None for files at the root level.
                pub fn parent(&self) -> Option<::tree_type::GenericDir> {
                    self.path.parent().and_then(|parent_path| {
                        ::tree_type::GenericDir::new(parent_path).ok()
                    })
                }
            }

            impl AsRef<std::path::Path> for #name {
                fn as_ref(&self) -> &std::path::Path {
                    &self.path
                }
            }

            impl std::fmt::Display for #name {
                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                    write!(f, "{}", self.path.display())
                }
            }

            impl From<#name> for ::tree_type::GenericFile {
                fn from(file: #name) -> Self {
                    Self::new(file.path).expect("Path validation already performed")
                }
            }
        }
    }
}

/// Generate a simple directory type with proper feature detection
#[expect(clippy::too_many_lines)]
pub fn generate_dir_type(name: &Ident) -> proc_macro2::TokenStream {
    let serde_derives = get_serde_derives();
    let walk_fns = build_walk_fns();

    quote! {
        #serde_derives
        #[derive(Clone, Debug, 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();
                // For directories, allow root paths and paths with filename components
                // Only reject empty paths or invalid paths like ".."
                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)
            }

            #walk_fns

            pub fn validate(&self) -> ::tree_type::ValidationReport {
                let mut report = ::tree_type::ValidationReport::new();

                // Validate root directory exists
                if !self.exists() {
                    report.errors.push(tree_type::ValidationError {
                        path: self.path.clone(),
                        message: "Directory does not exist".to_string(),
                    });
                }

                report
            }

            pub fn setup(&self) -> std::result::Result<Vec<::tree_type::BuildError>, Vec<::tree_type::BuildError>> {
                let errors = Vec::new();

                if !self.path.exists() {
                    let create_result = ::tree_type::fs::create_dir_all(&self.path);

                    if let Err(e) = create_result {
                        return Err(vec![tree_type::BuildError::Directory(
                            self.path.clone(),
                            e
                        )]);
                    }
                }

                Ok(errors)
            }

            pub fn ensure(&self) -> std::result::Result<::tree_type::ValidationReport, Vec<::tree_type::BuildError>> {
                self.setup()?;
                Ok(self.validate())
            }

            pub fn from_generic(dir: ::tree_type::GenericDir) -> Self {
                Self { path: dir.as_path().to_path_buf() }
            }

            /// Returns the final component of the path as a String.
            /// For root paths like "/", returns an empty string.
            /// See [`std::path::Path::file_name`] for more details.
            pub fn file_name(&self) -> String {
                self.path.file_name()
                    .map(|name| name.to_string_lossy().to_string())
                    .unwrap_or_default()
            }

            /// Set directory permissions to 0o700 (read/write/execute for owner only).
            ///
            /// This method is only available on Unix systems.
            #[cfg(unix)]
            pub fn secure(&self) -> std::io::Result<()> {
                self.as_generic().secure()
            }

            /// Get the parent directory as GenericDir.
            /// Returns None for directories at the root level.
            pub fn parent(&self) -> Option<::tree_type::GenericDir> {
                self.path.parent().and_then(|parent_path| {
                    ::tree_type::GenericDir::new(parent_path).ok()
                })
            }
        }

        impl AsRef<std::path::Path> for #name {
            fn as_ref(&self) -> &std::path::Path {
                &self.path
            }
        }

        impl std::fmt::Display for #name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{}", self.path.display())
            }
        }

        impl From<#name> for ::tree_type::GenericDir {
            fn from(dir: #name) -> Self {
                Self::new(dir.path).expect("Path validation already performed")
            }
        }
    }
}