techscan 1.0.2

A tool for analyzing and visualizing technology stacks in codes.
Documentation
use crate::entity::AppConfig;
use config::{Config, ConfigError, File};

#[derive(Debug)]
pub struct ConfigBuilder {
    pub exclude: Option<Vec<String>>,
    pub reporter: Option<String>,
}

impl ConfigBuilder {
    pub fn from_cli_args(exclude: &Option<Vec<String>>, reporter: &Option<String>) -> Self {
        Self {
            exclude: exclude.clone(),
            reporter: reporter.clone(),
        }
    }

    pub fn merge_file_config(mut self, config_path: &Option<String>) -> Result<Self, String> {
        if let Some(path) = config_path {
            let file_config = self
                .load_config_file(path)
                .map_err(|err| format!("Failed to load config file: {}: {}", path, err))?;

            if self.exclude.is_none() {
                self.exclude = file_config.exclude;
            }
            if self.reporter.is_none() {
                self.reporter = file_config.reporter;
            }
        }
        Ok(self)
    }

    pub fn build(self) -> AppConfig {
        AppConfig {
            exclude: self.exclude,
            reporter: self.reporter,
        }
    }

    fn load_config_file(&self, path: &str) -> Result<AppConfig, ConfigError> {
        let config = Config::builder()
            .add_source(File::with_name(path))
            .build()?;

        config.try_deserialize()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    mod from_cli_args {
        use super::*;

        #[test]
        fn test_from_cli_args_with_values() {
            let exclude = Some(vec!["*.log".to_string(), "exclude_dir".to_string()]);
            let reporter = Some("json".to_string());

            let builder = ConfigBuilder::from_cli_args(&exclude, &reporter);

            assert_eq!(builder.exclude, exclude);
            assert_eq!(builder.reporter, reporter);
        }

        #[test]
        fn test_from_cli_args_with_none() {
            let builder = ConfigBuilder::from_cli_args(&None, &None);

            assert_eq!(builder.exclude, None);
            assert_eq!(builder.reporter, None);
        }
    }

    mod build {
        use super::*;

        #[test]
        fn test_build_with_values() {
            let exclude = Some(vec!["*.tmp".to_string()]);
            let reporter = Some("json".to_string());

            let builder = ConfigBuilder {
                exclude: exclude.clone(),
                reporter: reporter.clone(),
            };

            let config = builder.build();

            assert_eq!(config.exclude, exclude);
            assert_eq!(config.reporter, reporter);
        }

        #[test]
        fn test_build_with_none() {
            let builder = ConfigBuilder {
                exclude: None,
                reporter: None,
            };

            let config = builder.build();

            assert_eq!(config.exclude, None);
            assert_eq!(config.reporter, None);
        }
    }

    mod merge_file_config {
        use super::*;

        #[test]
        fn test_merge_file_config_with_none() {
            let builder = ConfigBuilder::from_cli_args(&None, &None);
            let result = builder.merge_file_config(&None);

            assert!(result.is_ok());

            let builder = result.unwrap();
            assert_eq!(builder.exclude, None);
            assert_eq!(builder.reporter, None);
        }

        #[test]
        fn test_merge_file_config_with_valid_json() {
            let builder = ConfigBuilder::from_cli_args(&None, &None);
            let result = builder
                .merge_file_config(&Some("tests/fixtures/cli/config/complete.json".to_string()));

            assert!(result.is_ok());
            let builder = result.unwrap();

            assert_eq!(
                builder.exclude,
                Some(vec!["*.test.*".to_string(), "exclude_dir".to_string()])
            );
            assert_eq!(builder.reporter, Some("json".to_string()));
        }

        #[test]
        fn test_merge_file_config_with_valid_json5() {
            let builder = ConfigBuilder::from_cli_args(&None, &None);
            let result = builder.merge_file_config(&Some(
                "tests/fixtures/cli/config/complete.json5".to_string(),
            ));

            assert!(result.is_ok());
            let builder = result.unwrap();

            assert_eq!(
                builder.exclude,
                Some(vec!["*.test.*".to_string(), "exclude_dir".to_string()])
            );
            assert_eq!(builder.reporter, Some("json".to_string()));
        }

        #[test]
        fn test_merge_file_config_with_valid_yaml() {
            let builder = ConfigBuilder::from_cli_args(&None, &None);
            let result = builder
                .merge_file_config(&Some("tests/fixtures/cli/config/complete.yaml".to_string()));

            assert!(result.is_ok());
            let builder = result.unwrap();

            assert_eq!(
                builder.exclude,
                Some(vec!["*.test.*".to_string(), "exclude_dir".to_string()])
            );
            assert_eq!(builder.reporter, Some("json".to_string()));
        }

        #[test]
        fn test_merge_file_config_with_valid_yml() {
            let builder = ConfigBuilder::from_cli_args(&None, &None);
            let result = builder
                .merge_file_config(&Some("tests/fixtures/cli/config/complete.yml".to_string()));

            assert!(result.is_ok());
            let builder = result.unwrap();

            assert_eq!(
                builder.exclude,
                Some(vec!["*.test.*".to_string(), "exclude_dir".to_string()])
            );
            assert_eq!(builder.reporter, Some("json".to_string()));
        }

        #[test]
        fn test_merge_file_config_with_valid_toml() {
            let builder = ConfigBuilder::from_cli_args(&None, &None);
            let result = builder
                .merge_file_config(&Some("tests/fixtures/cli/config/complete.toml".to_string()));

            assert!(result.is_ok());
            let builder = result.unwrap();

            assert_eq!(
                builder.exclude,
                Some(vec!["*.test.*".to_string(), "exclude_dir".to_string()])
            );
            assert_eq!(builder.reporter, Some("json".to_string()));
        }

        #[test]
        fn test_merge_file_config_cli_args_take_priority() {
            let cli_exclude = Some(vec!["*.cli".to_string()]);
            let cli_reporter = Some("table".to_string());

            let builder = ConfigBuilder::from_cli_args(&cli_exclude, &cli_reporter);
            let result = builder
                .merge_file_config(&Some("tests/fixtures/cli/config/complete.json".to_string()));

            assert!(result.is_ok());

            let builder = result.unwrap();
            assert_eq!(builder.exclude, cli_exclude);
            assert_eq!(builder.reporter, cli_reporter);
        }

        #[test]
        fn test_merge_file_config_partial_cli_args() {
            let cli_exclude = Some(vec!["*.cli".to_string()]);
            let cli_reporter = None;

            let builder = ConfigBuilder::from_cli_args(&cli_exclude, &cli_reporter);
            let result = builder
                .merge_file_config(&Some("tests/fixtures/cli/config/complete.json".to_string()));

            assert!(result.is_ok());

            let builder = result.unwrap();
            assert_eq!(builder.exclude, cli_exclude);
            assert_eq!(builder.reporter, Some("json".to_string()));
        }

        #[test]
        fn test_merge_file_config_with_partial_config_file() {
            let builder = ConfigBuilder::from_cli_args(&None, &None);
            let result = builder.merge_file_config(&Some(
                "tests/fixtures/cli/config/reporter_only.yaml".to_string(),
            ));

            assert!(result.is_ok());

            let builder = result.unwrap();
            assert_eq!(builder.exclude, None);
            assert_eq!(builder.reporter, Some("json".to_string()));
        }

        #[test]
        fn test_merge_file_config_with_nonexistent_file() {
            let builder = ConfigBuilder::from_cli_args(&None, &None);
            let result = builder.merge_file_config(&Some("nonexistent/config.json".to_string()));

            assert!(result.is_err());

            let error_msg = result.unwrap_err();
            assert!(error_msg.contains("Failed to load config file: nonexistent/config.json:"));
        }
    }
}