1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// Copyright (c) 2021 Ethan Lerner, Caleb Cushing, and the Brix contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

//! # Brix Config Loader
//! The config loader is responsible for loading the declaration file and dispatching it
//! to the appropriate parser depending on the extension. It is also responsible for converting
//! the declaration file into a parsed list of [commands](`brix_commands::Command`) and arguments.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;

mod parsers;
mod process;
use parsers::ConfigParser;
pub use parsers::YamlConfigParser;

use brix_cli::select::do_select;
use brix_commands::{Command, ProcessedCommandParams};
use brix_common::AppContext;
use brix_errors::BrixError;

#[allow(rustdoc::private_intra_doc_links)]
/// Defines a parser list as a vec of trait objects implementing [ConfigParser].
pub type ParserList = Vec<Box<dyn ConfigParser>>;
/// Defines a command list as a vec of tuples containing a [Command] trait object and [ProcessedCommandParams].
type CommandList = Vec<(Box<dyn Command>, ProcessedCommandParams)>;

/// Struct that holds current information about the loaded configs and parsers.
pub struct ConfigLoader<'a> {
    parsers: ParserList,
    config_file: Option<PathBuf>,
    config_dir: Option<PathBuf>,
    cli_config: &'a brix_cli::Config,
}

impl<'a> ConfigLoader<'a> {
    pub fn new(parsers: ParserList, cli_config: &'a brix_cli::Config) -> Self {
        Self {
            parsers,
            cli_config,
            config_file: None,
            config_dir: None,
        }
    }

    /// Selects the proper configuration file knowing the supported extension for config files.
    /// May possibly use a [select prompt](`brix_cli::select::do_select`) to ask the user to choose a config file.
    pub fn load(&mut self, config_files: Vec<PathBuf>) -> Result<PathBuf, BrixError> {
        self.config_dir = Some(config_files[0].parent().unwrap().to_path_buf());
        let mut all_extensions = Vec::new();

        for supported_parser in self.parsers.iter() {
            all_extensions.extend(supported_parser.extensions())
        }

        let possible_config_files: Vec<&PathBuf> = config_files
            .iter()
            .filter(|f| all_extensions.contains(&f.extension().unwrap().to_str().unwrap()))
            .collect();

        if possible_config_files.len() == 0 {
            return Err(BrixError::with(&format!(
                "the file extension '.{}' is not supported for configs",
                config_files[0].extension().unwrap().to_string_lossy()
            )));
        }

        self.config_file = Some(possible_config_files[0].to_path_buf());

        if possible_config_files.len() > 1 {
            let names: Vec<&str> = possible_config_files
                .clone()
                .into_iter()
                .map(|f| f.file_name().unwrap().to_str().unwrap())
                .collect();

            println!("found multiple config files, which one would you like to use?");
            let result = do_select(names)?;

            self.config_file = Some(possible_config_files.get(result).unwrap().to_path_buf());
        }

        Ok(self.config_file.as_ref().unwrap().clone())
    }

    #[allow(rustdoc::private_intra_doc_links)]
    /// Actually parsers the config file for errors depending on the correct parser for the file.
    /// Sends resulting parsed output to [process] to be processed into commands.
    pub fn run(&self, app_context: &AppContext) -> Result<CommandList, BrixError> {
        let mut parser: Option<&Box<dyn ConfigParser>> = None;

        // Loop over each of the valid parsers this instance is configured with and see
        // if the extension for the config file matches with the parser we are testing
        for parser_opt in self.parsers.iter() {
            if parser_opt.matches(self.config_file.as_ref().unwrap()) {
                parser = Some(parser_opt);
                break;
            }
        }

        // Read the contents of the file to a string and parse it into the raw struct
        let contents = fs::read_to_string(self.config_file.as_ref().unwrap())?;
        let config = parser.unwrap().parse(&contents)?;
        // Send it over to be processed (./process.rs)
        self.process(&config, app_context)
    }
}

/// The preferred config with a Vec of command tuples instead
/// of a Vec of HashMaps.
#[derive(Debug)]
struct Config {
    context: Option<HashMap<String, String>>,
    commands: Vec<(String, RawCommandParams)>,
}

/// The raw struct used for an entire config.
/// Accurately describes the fields in a valid config file.
#[derive(Serialize, Deserialize, Debug)]
pub struct RawConfig {
    context: Option<HashMap<String, String>>,
    commands: Vec<HashMap<String, RawCommandParams>>,
}

/// The raw output for any config parser.
/// Defines all fields and their inital (not preferred) types for all commands.
#[derive(Serialize, Deserialize, Debug)]
struct RawCommandParams {
    source: Option<String>,
    destination: Option<String>,
    overwrite: Option<bool>,
    search: Option<String>,
    replace: Option<String>,
    commands: Option<Vec<String>>,
    stdout: Option<bool>,
    context: Option<HashMap<String, String>>,
}