diff --git a/src/cli/args.rs b/src/cli/args.rs index d625edf..3bd3118 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -5,7 +5,7 @@ pub struct Cli {} impl Cli { pub fn parse() -> clap::ArgMatches { App::new("idkmng") - .about("TOML based project initializer") + .about("A fast and flexible project initializer using TOML-based templates. Automate project setup, file generation, and reporting workflows with JSON input, dynamic placeholders, and optional Liquid support.") .version("2.0") .author("Mohamed Tarek @pwnxpl0it") .arg( @@ -35,6 +35,13 @@ impl Cli { .takes_value(true) .requires("template"), ) + .arg( + Arg::with_name("git") + .help("Initialize a git repo, this works regardless of template options") + .long("git") + .takes_value(false) + .requires("template"), + ) .subcommand(Command::new("init").about("Creates a template for the current directory")) .get_matches() } diff --git a/src/core/config.rs b/src/cli/config.rs similarity index 87% rename from src/core/config.rs rename to src/cli/config.rs index 934f6d6..66c115c 100644 --- a/src/core/config.rs +++ b/src/cli/config.rs @@ -1,12 +1,16 @@ -use crate::Config; -use crate::Keywords; -use crate::Template; +use idkmng::Keywords; +use idkmng::Template; use std::collections::HashMap; use std::fs; use toml::Value; -pub const KEYWORDS_FORMAT: &str = "{{$%s:f}}"; -pub const KEYWORDS_REGEX: &str = r"\{\{\$.*?\}\}"; + +#[derive(Debug, Clone)] +pub struct Config { + pub path: String, + pub templates_path: String, +} + impl Config { pub fn new(path: &str) -> Self { @@ -28,7 +32,7 @@ impl Config { // this sample is just a template that create config.toml and the new.toml template for the // first time, Now something maybe confusing is the "initPJNAME" wtf is it ? // That's just a way to workaround auto replacing PROJECTNAME in templates - let sample = r#" + let conf_template= r#" [[files]] path = 'TEMPLATES_PATH/new.toml' content = ''' @@ -62,7 +66,9 @@ content = ''' .replace("CONFIGPATH", &self.path) .replace("TEMPLATES_PATH", &self.templates_path); - Template::extract(sample, false, &mut keywords, serde_json::Value::Null); + let template: Template = toml::from_str(&conf_template).unwrap(); + + Template::extract(template, &mut keywords); } pub fn get_keywords(self) -> HashMap { @@ -86,3 +92,4 @@ content = ''' keywords } } + diff --git a/src/cli/main.rs b/src/cli/main.rs index ca95566..29a13bc 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -1,21 +1,18 @@ -use idkmng::Config; +use crate::config::Config; use idkmng::Keywords; use idkmng::Template; use std::fs; mod args; use args::Cli; +mod config; use colored::*; fn main() { let args = Cli::parse(); let config = Config::new(args.value_of("config").unwrap()); - let mut keywords = Keywords::init(config.clone()); - let mut json_data: serde_json::Value = Default::default(); + let mut keywords = Keywords::init(); - if args.is_present("json") { - let json_file = fs::read_to_string(args.value_of("json").unwrap()); - json_data = serde_json::from_str(&json_file.unwrap()).unwrap(); - } + keywords.extend(config.clone().get_keywords()); if args.subcommand_matches("init").is_some() { let dest = format!( @@ -29,14 +26,53 @@ fn main() { ); println!("{}: {}", "Creating Template".bold().green(), &dest.yellow()); Template::generate(&dest); - } else if let Some(filename) = args.value_of("template") { - let template = Template::validate(filename.to_string(), config.templates_path.clone()); - println!("\n{}: {}", "Using Template".blue(), &template.magenta()); + } else if let Some(temp) = args.value_of("template") { + let mut template = temp.to_string(); + + if !template.ends_with(".toml") { + template += ".toml"; + } + + let full_template_path = if fs::read_to_string(&template).is_err() { + format!("{}{}", config.templates_path, template) + } else { + template + }; + + let template_content = fs::read_to_string(&full_template_path).unwrap_or_else(|_| { + panic!( + "{}: {}", + "Failed to read template".red().bold(), + full_template_path + ) + }); + + let mut parsed_template: Template = toml::from_str(&template_content).unwrap(); + + let mut options = parsed_template.dump_options().unwrap_or_default(); + if !args.is_present("quiet") { - Template::show_info(&Template::parse(&template, true)); + println!( + "\n{}: {}", + "Using Template".blue(), + full_template_path.magenta() + ); + Template::show_info(&parsed_template); + } + + if args.is_present("json") { + let json_file = fs::read_to_string(args.value_of("json").unwrap()); + let json_data = serde_json::from_str(&json_file.unwrap()).unwrap(); + options.set_json(json_data); + } + + if args.is_present("git") { + options.set_git(true); + options.set_project_root("{{$PROJECTNAME}}"); } - Template::extract(template, true, &mut keywords, json_data); + parsed_template.set_options(options); + parsed_template.extract(&mut keywords); } else { println!( "{} {}", diff --git a/src/core/file.rs b/src/core/file.rs index 7139e90..2d8b995 100644 --- a/src/core/file.rs +++ b/src/core/file.rs @@ -1,7 +1,7 @@ use crate::File; impl File { - pub fn new(path_: String, content_: String) -> Self { + pub fn from(path_: String, content_: String) -> Self { File { path: path_, content: content_, diff --git a/src/core/keywords.rs b/src/core/keywords.rs index 06940a2..72428c9 100644 --- a/src/core/keywords.rs +++ b/src/core/keywords.rs @@ -1,6 +1,5 @@ -use crate::config::KEYWORDS_FORMAT; -use crate::Config; use crate::Keywords; +use crate::templates::KEYWORDS_FORMAT; use chrono::Datelike; use std::{collections::HashMap, env}; @@ -23,7 +22,7 @@ impl Keywords { keyword.replace("{{$", "").replace("}}", "") } - pub fn init(config: Config) -> HashMap { + pub fn init() -> HashMap { let mut keywords = HashMap::new(); keywords.insert( Self::new(String::from("HOME"), None), @@ -71,9 +70,6 @@ impl Keywords { chrono::Local::now().day().to_string(), ); - let other_keywords = config.get_keywords(); - - keywords.extend(other_keywords); keywords } diff --git a/src/core/lib.rs b/src/core/lib.rs index d8821ba..b6ce1ff 100644 --- a/src/core/lib.rs +++ b/src/core/lib.rs @@ -1,17 +1,11 @@ -pub mod config; mod file; pub mod funcs; pub mod keywords; +mod options; mod templates; mod utils; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone)] -pub struct Config { - pub path: String, - pub templates_path: String, -} - pub struct Keywords {} #[derive(Debug, Deserialize, Serialize)] @@ -22,6 +16,13 @@ pub struct Information { } #[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Options { + pub git: bool, + pub json_data: Option, + pub project_root: String, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] pub struct File { pub path: String, pub content: String, @@ -30,10 +31,11 @@ pub struct File { #[derive(Debug, Deserialize, Serialize)] pub struct Template { pub info: Option, - pub files: Vec, + pub options: Option, + pub files: Option>, } -#[derive(Clone, Copy)] +#[derive(Debug, Clone, Copy)] pub enum Fns { Read, Env, diff --git a/src/core/options.rs b/src/core/options.rs new file mode 100644 index 0000000..0f425d9 --- /dev/null +++ b/src/core/options.rs @@ -0,0 +1,92 @@ +use crate::Options; +use colored::*; +use std::process::Command; + +impl Default for Options { + fn default() -> Self { + Self { + json_data: Some(serde_json::Value::Null), + git: false, + project_root: String::new(), + } + } +} + +impl Options { + pub fn set_git(&mut self, git: bool) { + self.git = git; + } + + pub fn set_json(&mut self, json_data: serde_json::Value) { + self.json_data = Some(json_data); + } + + pub fn set_project_root(&mut self, project_root: &str) { + self.project_root = project_root.to_string(); + } + + pub fn check_git() -> Result<(), String> { + if Command::new("git").arg("--version").spawn().is_err() { + return Err("Git is not installed. Please install git and try again.".to_string()); + } + Ok(()) + } + + pub fn git_init(self) { + if let Err(e) = Self::check_git() { + println!("{}: {}", "error".red().bold(), e.red().bold()); + return; + } + + if self.project_root.is_empty() { + println!( + "{}: {}", + "error".red().bold(), + "Please specify a project root.".red().bold() + ); + return; + } + + if let Err(e) = std::env::set_current_dir(&self.project_root) { + println!( + "{}: {}", + "error".red().bold(), + format!("Failed to change directory: {}", e).red().bold() + ); + return; + } + + let cmd = Command::new("git") + .arg("init") //TODO: maybe add git arguments? that can be a bit risky.. + .stderr(std::process::Stdio::null()) // hide hints and errors + .status(); + + if let Ok(status) = cmd { + if status.success() { + println!("{}", "\n✅ Git initialized successfully.".green().bold()); + } else { + println!( + "{}: {}", + "error".red().bold(), + "Git initialization failed.".red().bold() + ); + } + } else { + println!( + "{}: {}", + "error".red().bold(), + "Failed to run git command.".red().bold() + ); + } + } + + pub fn handle_options(self) { + if self.git { + println!( + "\nInitializing git repository for {}\n", + self.project_root.blue() + ); + self.git_init(); + } + } +} diff --git a/src/core/templates.rs b/src/core/templates.rs index b57e5af..0473f7a 100644 --- a/src/core/templates.rs +++ b/src/core/templates.rs @@ -1,27 +1,50 @@ -use crate::config::*; use crate::utils::*; use crate::*; use colored::Colorize; -use liquid; use promptly::prompt; use regex::Regex; use std::{collections::HashMap, fs, path::Path}; -impl Template { - fn new(info_: Information, files_: Vec) -> Self { +pub const KEYWORDS_FORMAT: &str = "{{$%s:f}}"; +pub const KEYWORDS_REGEX: &str = r"\{\{\$.*?\}\}"; + +impl Default for Template { + fn default() -> Self { Self { - info: Some(info_), - files: files_, + options: Some(Options::default()), + info: None, + files: None, } } +} + +impl Template { + pub fn set_info(&mut self, info: Information) { + self.info = Some(info); + } + + pub fn set_files(&mut self, files: Vec) { + self.files = Some(files); + } + + pub fn set_options(&mut self, options: Options) { + self.options = Some(options); + } + + pub fn dump_options(&mut self) -> Option { + if self.options.is_none() { + return None; + } + Some(self.options.clone().unwrap()) + } pub fn generate(dest: &str) { - let mut files: Vec = Vec::new(); // Create a new Vector of File + let mut files: Vec = Vec::new(); list_files(Path::new("./")).iter().for_each(|file| { //TODO: Add more to ignore list maybe adding a --ignore flag will be good if !file.contains(".git") { - let file = File::new(file.to_string().replace("./", ""), { + let file = File::from(file.to_string().replace("./", ""), { match fs::read_to_string(file) { Ok(content) => content, Err(e) => panic!("{}:{}", file.red().bold(), e), @@ -31,14 +54,11 @@ impl Template { } }); - let template = Self::new( - Information { - name: Some(String::from("")), - author: Some(String::from("")), - description: Some(String::from("")), - }, - files, - ); + let template = Template { + info: None, + files: Some(files), + options: None, + }; let toml_string = toml::to_string_pretty(&template).expect("Failed to create toml string"); fs::write(dest, toml_string).unwrap(); @@ -47,46 +67,43 @@ impl Template { pub fn liquify(string: &str) -> String { let parser = liquid::ParserBuilder::with_stdlib().build().unwrap(); let empty_globals = liquid::Object::new(); - let new_template = parser - .parse(&string) + + parser + .parse(string) .unwrap() .render(&empty_globals) - .unwrap(); - - new_template + .unwrap() } - pub fn extract( - template: String, - is_file: bool, - keywords: &mut HashMap, - json_data: serde_json::Value, - ) { - let re = Regex::new(KEYWORDS_REGEX).unwrap(); - let sample = Self::parse(&template, is_file); - let files = sample.files; + pub fn extract(mut self, keywords: &mut HashMap) { let mut project = String::from(""); let mut output = String::from(""); + let re = Regex::new(KEYWORDS_REGEX).unwrap(); + let files = self.files.clone().expect("No files table"); + let mut options = self.dump_options().expect("No options"); files.into_iter().for_each(|file| { *keywords = find_and_exec( file.content.clone(), keywords.clone(), re.clone(), - json_data.clone(), + options.json_data.clone().unwrap_or(serde_json::Value::Null), ); *keywords = find_and_exec( file.path.clone(), keywords.clone(), re.clone(), - json_data.clone(), + options.json_data.clone().unwrap_or(serde_json::Value::Null), ); if file.path.contains("{{$PROJECTNAME}}") || file.content.contains("{{$PROJECTNAME}}") { if project.is_empty() { project = prompt("Project name").unwrap(); keywords.insert("{{$PROJECTNAME}}".to_string(), project.to_owned()); + if options.project_root == "{{$PROJECTNAME}}" { + options.set_project_root(&project); + } } } @@ -106,39 +123,10 @@ impl Template { write_content(&shellexpand::tilde(&path), liquified) }); - } - - /// Parse a Template - pub fn parse(template: &str, is_file: bool) -> Self { - #[allow(unused_assignments)] - let mut content = String::from(""); - match is_file { - true => { - content = fs::read_to_string(template) - .unwrap_or_else(|_| panic!("Failed to Parse {}", template)); - } - false => content = template.to_string(), - } - - toml::from_str(&content).unwrap() - } - - /// This method validates template path, in other words it just checks if the template is in - /// the current working directory,if not it uses the default templates directory, also automatically adds .toml - pub fn validate(mut template: String, template_path: String) -> String { - if !template.contains(".toml") { - template += ".toml" - } - - if fs::read_to_string(&template).is_err() { - template = template_path + &template - } - template + Options::handle_options(options); } - /// This method shows information about current template, basically Reads them from Information - /// section in the template TOML file pub fn show_info(template: &Self) { match &template.info { Some(information) => println!( diff --git a/src/core/utils.rs b/src/core/utils.rs index 65ccd8c..c005ccd 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -1,6 +1,5 @@ use crate::Fns; use colored::*; -use jq_rs; use regex::Regex; use std::{collections::HashMap, fs, path::Path}; @@ -43,17 +42,15 @@ pub fn find_and_exec( for (keyword_name, (keyword, function)) in found { //HACK: Just a bit of optimization, if the json_data is null then it doesn't make sense to run jq // because doing so is every expensive and here we are dealing with dynamic queries - if !json_data.is_null() { - if keyword_name.contains(".") { - //TODO: This is not very performant but it works for now UwU - let output = jq_rs::run(&keyword_name, &json_data.to_string()); + if !json_data.is_null() && keyword_name.contains(".") { + //TODO: This is not very performant but it works for now UwU + let output = jq_rs::run(&keyword_name, &json_data.to_string()); - if let Ok(value) = &output { - //NOTE: This will also replace any quotes in the value - keywords.insert(keyword, value.replace("\"", "")); - } - continue; + if let Ok(value) = output { + //NOTE: This will also replace any quotes in the value + keywords.insert(keyword, value.replace("\"", "")); } + continue; } if let Ok(value) = Fns::exec(function, keyword_name) {