diff --git a/src/commands/run.rs b/src/commands/run.rs index f4a2148..1f96359 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -1,41 +1,215 @@ +use std::collections::HashMap; +use std::fmt::Debug; +use std::fs::{canonicalize, read_to_string}; +use std::path::{Path, PathBuf}; use clap::Args; -use crate::utils::config::{parse_config, validate_config}; -use crate::utils::file_resolvers::resolve_configuration_file; -use crate::utils::tasks::run_task; +use glob::glob; +use serde::Deserialize; #[derive(Args, Debug)] pub struct Arguments { - #[arg()] - command: String, - #[arg(long, default_value = ".")] + #[arg(help = "Which task to run")] + task_name: String, + #[arg(long, default_value = ".", help = "Which directory to use as entry, defaults to the current directory")] entry: String, } pub fn run (arguments: &Arguments) -> Result<(), String> { - let Arguments { entry, command } = arguments; + let Arguments { entry, task_name: _task_name } = arguments; - let target = resolve_configuration_file(entry)?; + // Resolve the entry path + let entry_config_path: PathBuf = resolve_config_path(entry)?; + println!("entry_config_path: {:?}", entry_config_path); - let config = parse_config(&target)?; + // Discover all config paths + let config_paths: Vec = discover_config_paths(entry_config_path)?; + println!("config_paths: {:?}", config_paths); - match validate_config(config.clone()) { - Ok(_) => {} - Err(err) => return Err(err) + // Parse config file content + let config_files: Vec = read_config_files(config_paths)?; + println!("config_files: {:?}", config_files); + + // Parse config files + let configs: Vec = parse_config_files(config_files)?; + println!("configs: {:?}", configs); + + // Resolve dependencies based on the directory structure + // (In the future this will be configurable based on a dependency config field) + // let config_structure: ConfigStructure = resolve_config_structure(configs); + + // Gather the tasks from the config + // let task_structure: TaskStructure = resolve_task_structure(config_structure, task_name); + + // Run the commands, one by one + // > In the future this is configurable on the rask level and maybe on the config file level + // > Initially it fails the whole command if one task fails, but will also be configurable in the future + // let task_exit: TaskExit = run_task_structure(task_structure); + + Ok(()) +} + +#[derive(Debug, Clone)] +enum TaskType { + SHELL +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +struct ConfigTask { + task_type: TaskType, + content: String +} + +type ConfigTasks = HashMap; + +#[derive(Debug, Clone)] +#[allow(dead_code)] +struct Config { + name: String, + tasks: HashMap, + path: PathBuf, +} + +fn parse_config_files(config_files: Vec) -> Result, String> { + let mut configs: Vec = vec![]; + + for config_file in config_files { + let config = parse_config_file(config_file)?; + configs.push(config); } - for config in config.clone().iter() { - match config.get_task(command) { - None => {} - Some(task) => { - match run_task(&task) { - Ok(_) => {} - Err(err) => return Err(err) + Ok(configs) +} + +fn parse_config_file(config_file: ConfigFile) -> Result { + let ConfigFile { name, tasks: config_file_tasks, _file_path: path, .. } = config_file; + + let tasks = parse_config_tasks(config_file_tasks)?; + + let config: Config = Config {name, tasks, path}; + + Ok(config) +} + +fn parse_config_tasks(tasks: ConfigFileTasks) -> Result { + let mut config_tasks: ConfigTasks = HashMap::new(); + + for (key, value) in tasks { + let config_task: ConfigTask = ConfigTask { + task_type: TaskType::SHELL, + content: value + }; + + config_tasks.insert(key, config_task); + } + + Ok(config_tasks) +} + +fn read_config_files(paths: Vec) -> Result, String> { + let mut configs_files: Vec = vec![]; + + for path in paths { + let config_file = read_config_file(path)?; + configs_files.push(config_file); + } + + Ok(configs_files) +} + +fn discover_config_paths(path: PathBuf) -> Result, String> { + let mut found_config_paths: Vec = vec![path.clone()]; + + // Read config + let mut path_stack: Vec = vec![path.clone()]; + while !path_stack.is_empty() { + let ConfigFile { directories, _file_path, .. } = read_config_file(path_stack.pop().unwrap())?; + + // Extract directories + let config_directory = _file_path.parent().ok_or("Failed to get parent directory")?; + for directory in directories { + let mut pattern: PathBuf = config_directory.to_path_buf(); + pattern.push(&directory); + if !pattern.ends_with(".yaml") { + pattern.push("rask.yaml"); + } + + // Find config files based on the pattern in the directories value + let pattern_string: &str = pattern.to_str().unwrap(); + for pattern_results in glob(pattern_string).map_err(|e| format!("Failed to read glob pattern: {}", e))? { + if let Ok(found_config_path) = pattern_results { + // Only add if the path was not already processed, preventing loops. + if !found_config_paths.contains(&found_config_path) { + found_config_paths.push(found_config_path.clone()); + path_stack.push(found_config_path.clone()); + } } } } } - println!("{:?}", config); + Ok(found_config_paths) +} - Ok(()) +fn read_file_content (path: PathBuf) -> Result { + match read_to_string(path) { + Ok(content) => Ok(content), + Err(err) => Err(format!("Failed to read file: {}", err)), + } +} + +type ConfigFileTasks = HashMap; + +#[derive(Debug, Deserialize, Default)] +struct ConfigFile { + name: String, + #[serde(default)] + directories: Vec, + #[serde(default)] + tasks: ConfigFileTasks, + #[serde(default)] + _file_path: PathBuf, +} + +fn read_config_file(path: PathBuf) -> Result { + let content = read_file_content(path.clone())?; + + let mut config_file: ConfigFile = serde_yaml::from_str(&content).expect(format!("Failed to parse YAML from \"{:?}\"", path).as_str()); + + config_file._file_path = path.clone(); + + Ok(config_file) +} + +fn resolve_config_path + Debug + Clone + Copy>(path: P) -> Result { + let full_path = match canonicalize(path) { + Ok(full_path) => full_path, + Err(_) => return Err(format!("Target does not exists: {:?}", path.clone())) + }; + + if full_path.is_dir() { + let config_file = find_config_file(full_path)?; + return Ok(config_file) + } + + Ok(full_path) +} + +const CONFIG_FILENAMES: [&str; 1] = ["rask.yaml"]; + +fn find_config_file(directory_path: PathBuf) -> Result { + if !directory_path.is_dir() { + return Err(format!("\"{:?}\" is not a directory", directory_path)) + } + + for filename in CONFIG_FILENAMES { + let mut possible_config_file = directory_path.clone(); + possible_config_file.push(filename); + match possible_config_file.exists() { + true => return Ok(possible_config_file), + false => {} + } + } + + Err(format!("Unable to find a config file (\"{:?}\") in {:?}", CONFIG_FILENAMES, directory_path)) } \ No newline at end of file diff --git a/src/utils/config.rs b/src/utils/config.rs deleted file mode 100644 index 7e2c0f2..0000000 --- a/src/utils/config.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::path::PathBuf; -use std::fs::File; -use std::io::BufReader; -use serde::Deserialize; -use std::collections::HashMap; - - -pub fn validate_config(config: Config) -> Result { - let mut names: Vec = vec![]; - - for config in config.iter() { - let Config { name, path, .. } = config; - - if names.contains(&name) { - return Err(format!("Duplicate config name {} found: {:?}", name, path)); - } - - names.push(name); - } - - Ok(true) -} - -#[derive(Debug, Deserialize, Default)] -struct ConfigFile { - name: String, - #[serde(default)] - directories: Vec, - #[serde(default)] - tasks: HashMap, -} - -fn read_config_file(path_buf: &PathBuf) -> Result { - let file = File::open(path_buf).expect("Failed to read file"); - - let reader = BufReader::new(file); - - let config: ConfigFile = serde_yaml::from_reader(reader).expect("Failed to parse YAML"); - - Ok(config) -} - -#[derive(Debug, Clone)] -pub struct ConfigTask { - tag: String, - command: String -} - -#[derive(Debug, Clone)] -pub struct Config { - name: String, - tasks: Vec, - path: PathBuf, - sub_configs: Vec, -} - -impl Config { - pub fn iter(self) -> ConfigIterator { - return ConfigIterator::new(self.clone()); - } - - pub fn get_task(&self, task_name: &String) -> Option { - for task in self.clone().tasks { - if task.tag == *task_name { - return Some(task.command) - } - } - - None - } -} - -pub struct ConfigIterator { - stack: Vec -} - -impl ConfigIterator { - pub fn new(config: Config) -> ConfigIterator { - let mut stack = vec![config]; - ConfigIterator{ stack } - } -} - -impl Iterator for ConfigIterator { - type Item = Config; - - fn next(&mut self) -> Option { - let next_config = self.stack.pop()?; - - self.stack.extend(next_config.sub_configs.iter().rev().map(|sub_config| sub_config.clone())); - - Some(next_config) - } -} - -pub fn parse_config(path: &PathBuf) -> Result { - let config_file = read_config_file(path)?; - - let name = config_file.name; - - let tasks = config_file.tasks - .iter() - .map(|(tag, command)| ConfigTask{tag: tag.clone(), command: command.clone()}) - .collect(); - - let mut sub_configs: Vec = Vec::new(); - - let parent_dir = path.parent().ok_or("Failed to get parent directory")?; - - for directory in config_file.directories { - let mut pattern: PathBuf = parent_dir.to_path_buf(); - pattern.push(&directory); - if !pattern.ends_with(".yaml") { - pattern.push("rask.yaml"); - } - - for entry in glob::glob(pattern.to_str().unwrap()).map_err(|e| format!("Failed to read glob pattern: {}", e))? { - if let Ok(config_path) = entry { - let sub_config = parse_config(&config_path)?; - sub_configs.push(sub_config); - } - } - } - - let config = Config{name, tasks, sub_configs, path: path.clone()}; - - Ok(config) -} diff --git a/src/utils/file_resolvers.rs b/src/utils/file_resolvers.rs deleted file mode 100644 index 06eadf7..0000000 --- a/src/utils/file_resolvers.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::fs::canonicalize; - -const DEFAULT_FILENAME: &str = "rask.yaml"; - -pub fn resolve_configuration_file(target: &String) -> Result { - let target_path = Path::new(target); - - let mut target = match canonicalize(target_path) { - Ok(target) => target, - Err(_) => return Err(format!("Target does not exists: {:?}", target)) - }; - - if target.is_dir() { - target.push(DEFAULT_FILENAME); - } - - if !target.exists() { - return Err(format!("Target does not exists: {:?}", target)) - } - - Ok(target) -} diff --git a/src/utils/tasks.rs b/src/utils/tasks.rs deleted file mode 100644 index de58d95..0000000 --- a/src/utils/tasks.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::process::Command; - -pub fn run_task(task: &String) -> Result<(), String> { - let mut command = Command::new(task); - match command.spawn().unwrap().wait() { - Ok(_) => Ok(()), - Err(_) => Err(format!("Task failed: {}", task)) - } -} \ No newline at end of file