rfind 0.2.0

A simple clone of the `find` command
Documentation
use std::{path::{PathBuf, Path}, fs};

use clap::{Arg, Command, crate_authors, crate_description, crate_name, crate_version};
use regex::Regex;

pub struct Config {
    starting_points: Vec<PathBuf>,
    options: Vec<(String, String)>,
}

impl Config {
    pub fn new() -> Config {
        let matches = Command::new(crate_name!())
            .author(crate_authors!())
            .version(crate_version!())
            .about(crate_description!())
            .arg(
                Arg::new("starting-point")
                    .value_name("STARTING-POINT")
                    .help("The starting-point of the program")
                    .default_value(".")
                    .multiple_values(true),
            )
            .arg(
                Arg::new("name")
                    .value_name("NAME")
                    .help("The regex to match")
                    .short('n')
                    .long("name")
                    .takes_value(true),
            )
            .arg(
                Arg::new("type")
                    .value_name("TYPE")
                    .help("The type of the file")
                    .short('t')
                    .long("type")
                    .takes_value(true)
                    .possible_values(["f", "d", "l"]),
            )
            .get_matches();

        let starting_points = matches
            .values_of("starting-point")
            .unwrap()
            .map(|value| PathBuf::from(value))
            .collect();

        let mut options = Vec::new();

        if let Some(value) = matches.value_of("name") {
            options.push(("name".to_string(), value.to_owned()));
        }

        if let Some(value) = matches.value_of("type") {
            options.push(("type".to_string(), value.to_owned()));
        }

        Config {
            starting_points,
            options,
        }
    }
}

pub struct App {
    config: Config,
}

impl App {
    pub fn new(config: Config) -> App {
        App { config }
    }

    pub fn run(&self) {
        for starting_point in &self.config.starting_points {
            self.visit_path(starting_point);
        }
    }

    fn visit_path(&self, path: &Path) {
        let entries = fs::read_dir(path).expect("invalid path");

        for entry in entries {
            let path = entry.unwrap().path();

            if self.is_match(&path) {
                println!("{}", path.display());
            }

            if path.is_dir() {
                self.visit_path(&path);
            }
        }
    }

    fn is_match(&self, path: &Path) -> bool {
        self.config.options.iter().all(|(option, value)| {
            match option.as_str() {
                "name" => self.name_option(value, path.file_name().unwrap().to_str().unwrap()),
                "type" => self.type_option(value, path),
                _ => panic!("invalid option"),
            }
        })
    }

    fn name_option(&self, value: &str, filename: &str) -> bool {
        let re = Regex::new(value).expect("invalid regex");

        re.is_match(filename)
    }

    fn type_option(&self, value: &str, path: &Path) -> bool {
        match value {
            "f" => path.is_file(),
            "d" => path.is_dir(),
            "l" => path.is_symlink(),
            _ => panic!("invalid file type"),
        }
    }
}