diff --git a/examples/full_spec/nested/folder/rask.yaml b/examples/full_spec/nested/folder/rask.yaml index fded041..72bb17e 100644 --- a/examples/full_spec/nested/folder/rask.yaml +++ b/examples/full_spec/nested/folder/rask.yaml @@ -1,4 +1,5 @@ name: nested/folder tasks: - dev: echo 'Hello from nested/folder?' \ No newline at end of file + dev: echo 'Hello from nested/folder?' + meme: echo 'Meme from nested/folder?' \ No newline at end of file diff --git a/examples/init/.gitignore b/examples/init/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/examples/init/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/src/commands/init.rs b/src/commands/init.rs new file mode 100644 index 0000000..4067e9c --- /dev/null +++ b/src/commands/init.rs @@ -0,0 +1,51 @@ +use clap::Args; +use crate::utils::file::{ConfigFile, parse_path_string, write_config_file}; + +#[derive(Args, Debug)] +pub struct Arguments { + #[arg(long, default_value = ".", help = "Which directory to use as entry, defaults to the current directory")] + entry: String, + #[arg(help = "then name of the config file, defaults to the directory name", default_value = "")] + name: String, +} + +pub fn execute(arguments: &Arguments) -> Result<(), String> { + let Arguments { entry, name } = arguments; + + let mut path = parse_path_string(entry)?; + if path.is_dir() { + path.push("rask.yaml") + } + + if path.exists() { + return Err(format!("Rask already initialised at {:?}", path)); + } + + let mut config_name = name.clone(); // Use clone to avoid modifying the original input + if config_name.is_empty() { + config_name = path.parent() + .unwrap() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(); + } + + let config_file: ConfigFile = ConfigFile { + name: config_name, + task_engine: Default::default(), + directories: vec![], + tasks: Default::default(), + __file_path: Default::default(), + __dir_path: Default::default(), + }; + + write_config_file(path.clone(), config_file)?; + + // NOTE: We could improve the init command by adding a reverse search for a parent rake file + + println!("Rask initialised: {:?}", path); + + Ok(()) +} \ No newline at end of file diff --git a/src/commands/list.rs b/src/commands/list.rs index 79e89a9..7d44717 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -40,8 +40,8 @@ fn get_config_tasks(configs: &Vec) -> Result, String> { let mut tasks: Vec = vec![]; for config in configs { - for configTask in &config.tasks { - let ConfigTask { key, .. } = configTask; + for config_task in &config.tasks { + let ConfigTask { key, .. } = config_task; if !tasks.contains(&key) { tasks.push(key.clone()); } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index bfb5fb2..f41576a 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,2 +1,3 @@ pub mod run; -pub mod list; \ No newline at end of file +pub mod list; +pub mod init; \ No newline at end of file diff --git a/src/commands/run.rs b/src/commands/run.rs index 3b0185a..7c5bf4a 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -113,7 +113,7 @@ fn execute_task(task: &Task) -> Result, String> { .arg(command) .current_dir(directory) .status() - .expect("Failed to execute command"); + .map_err(|err| err.to_string()); status.success() } }); diff --git a/src/main.rs b/src/main.rs index 6a4e282..67da40a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use std::process::exit; use clap::{Parser, Subcommand}; use commands::run; use commands::list; +use commands::init; mod commands; mod utils; @@ -10,6 +11,7 @@ mod utils; enum Command { Run(run::Arguments), List(list::Arguments), + Init(init::Arguments), } #[derive(Parser, Debug)] @@ -25,6 +27,7 @@ fn main() { let result = match &arguments.command { Command::Run(arguments) => { run::execute(arguments) }, Command::List(arguments) => { list::execute(arguments) }, + Command::Init(arguments) => { init::execute(arguments) }, }; match result { diff --git a/src/utils/config.rs b/src/utils/config.rs index 62ad383..bdb422a 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -1,6 +1,5 @@ use std::path::{Path, PathBuf}; use std::fmt::Debug; -use std::fs::canonicalize; use std::collections::HashMap; use globset::{Glob, GlobSetBuilder}; use serde::Deserialize; @@ -172,7 +171,7 @@ fn parse_config_file(config_file: ConfigFile) -> Result { TaskEngine::NPM => parse_package_json_tasks(&dir_path, TaskType::NPM)?, TaskEngine::YARN => parse_package_json_tasks(&dir_path, TaskType::YARN)?, TaskEngine::NONE => parse_config_tasks(config_file_tasks)?, - TaskEngine::AUTO => parse_discovered_tasks(&dir_path)?, + TaskEngine::AUTO => parse_discovered_tasks(&dir_path, config_file_tasks)?, }; let config: Config = Config { name, tasks, file_path, dir_path, directories }; @@ -181,12 +180,11 @@ fn parse_config_file(config_file: ConfigFile) -> Result { } const PACKAGE_JSON_FILE: &str = "package.json"; -const NPM_LOCK_FILE: &str = "package.lock"; const YARN_LOCK_FILE: &str = "yarn.lock"; const COMPOSER_JSON_FILE: &str = "composer.json"; -fn parse_discovered_tasks(dir_path: &PathBuf) -> Result { - let mut config_tasks: ConfigTasks = vec![]; +fn parse_discovered_tasks(dir_path: &PathBuf, config_file_tasks: ConfigFileTasks) -> Result { + let mut config_tasks: ConfigTasks = parse_config_tasks(config_file_tasks)?; // Gathering facts let has_composer_json = dir_path.join(COMPOSER_JSON_FILE).exists(); @@ -200,7 +198,7 @@ fn parse_discovered_tasks(dir_path: &PathBuf) -> Result { } if has_package_json { - let mut package_config_tasks: ConfigTasks; + let package_config_tasks: ConfigTasks; if has_yarn_lock { package_config_tasks = parse_package_json_tasks(dir_path, TaskType::YARN)?; @@ -331,10 +329,7 @@ fn get_config_glob_pattern(root_path: &Path, glob_pattern: &String) -> PathBuf { } pub 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())) - }; + let full_path = file::parse_path_string(path)?; if full_path.is_dir() { let config_file = find_config_file(full_path)?; diff --git a/src/utils/file.rs b/src/utils/file.rs index 09f1eec..0a496ff 100644 --- a/src/utils/file.rs +++ b/src/utils/file.rs @@ -1,7 +1,8 @@ -use std::path::PathBuf; -use std::fs::read_to_string; +use std::path::{Path, PathBuf}; +use std::fs::{canonicalize, read_to_string, write}; use std::collections::HashMap; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; pub fn read_file_content (path: PathBuf) -> Result { match read_to_string(path) { @@ -10,10 +11,16 @@ pub fn read_file_content (path: PathBuf) -> Result { } } +pub fn write_file_content(file_path: &PathBuf, content: &str) -> Result<(), String> { + write(file_path, content).map_err(|err| format!("Failed to write to file: {}", err))?; + + Ok(()) +} + pub fn read_json_file Deserialize<'a>>(file_path: &PathBuf) -> Result { let content = read_file_content(file_path.clone())?; - let file_content: T = serde_json::from_str::(&content).expect(format!("Failed to read the file: \"{:?}\"", file_path).as_str()); + let file_content: T = serde_json::from_str::(&content).map_err(|err| err.to_string()); Ok(file_content) } @@ -21,44 +28,57 @@ pub fn read_json_file Deserialize<'a>>(file_path: &PathBuf) -> Result fn read_yaml_file Deserialize<'a>>(file_path: &PathBuf) -> Result { let content = read_file_content(file_path.clone())?; - let file_content: T = serde_yaml::from_str::(&content).expect(format!("Failed to read the file: \"{:?}\"", file_path).as_str()); + let file_content: T = serde_yaml::from_str::(&content).map_err(|err| err.to_string())?; Ok(file_content) } -#[derive(Debug, Clone, Default, Deserialize)] +pub fn write_yaml_file(file_path: &PathBuf, data: &T) -> Result<(), String> { + let yaml_content = serde_yaml::to_string(data).map_err(|err| err.to_string())?; + + write_file_content(file_path, &yaml_content) +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] pub enum TaskEngine { - #[serde(rename = "composer")] COMPOSER, - #[serde(rename = "npm")] NPM, - #[serde(rename = "yarn")] YARN, - #[serde(rename = "none")] NONE, - #[serde(rename = "auto")] #[default] AUTO, } pub type ConfigFileTasks = HashMap; -#[derive(Debug, Deserialize, Clone, Default)] +#[derive(Debug, Clone, Default, Deserialize, Serialize)] pub struct ConfigFile { pub(crate) name: String, - #[serde(default)] + #[serde(default, skip_serializing_if = "is_auto_task_engine")] pub(crate) task_engine: TaskEngine, - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub(crate) directories: Vec, - #[serde(default)] + #[serde(default, skip_serializing_if = "ConfigFileTasks::is_empty")] pub(crate) tasks: ConfigFileTasks, // The following fields are not part of the yaml file. - #[serde(default)] + #[serde(default, skip_serializing_if = "skip_path")] pub(crate) __file_path: PathBuf, - #[serde(default)] + #[serde(default, skip_serializing_if = "skip_path")] pub(crate) __dir_path: PathBuf, } +fn is_auto_task_engine(value: &TaskEngine) -> bool { + match value { + TaskEngine::AUTO => true, + _ => false, + } +} + +fn skip_path(path: &PathBuf) -> bool { + path.to_str().map_or(true, |s| s.is_empty()) +} + pub fn read_config_file(config_file_path: PathBuf) -> Result { let mut config_file = read_yaml_file::(&config_file_path)?; @@ -68,3 +88,16 @@ pub fn read_config_file(config_file_path: PathBuf) -> Result Ok(config_file) } +pub fn write_config_file(config_file_path: PathBuf, config_file: ConfigFile) -> Result<(), String> { + write_yaml_file::(&config_file_path, &config_file) +} + +pub fn parse_path_string + 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())) + }; + + Ok(full_path) +} +