From 2920f585124dc055435c137d3e0fc0f2d27af5b2 Mon Sep 17 00:00:00 2001 From: Devin Pastoor Date: Fri, 23 Aug 2024 13:02:20 -0600 Subject: [PATCH 01/13] chore: adjust project_name to _project_name to give ability for users to define that key --- cli/src/fill.rs | 2 +- docs/configuration.md | 2 +- src/core/copy.rs | 4 ++-- src/core/hook.rs | 4 ++-- src/core/template.rs | 2 +- src/lib.rs | 2 +- tests/data/proj1/{{{project_name}} => {{_project_name}}} | 0 tests/data/templated/file.j2 | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) rename tests/data/proj1/{{{project_name}} => {{_project_name}}} (100%) diff --git a/cli/src/fill.rs b/cli/src/fill.rs index 2df542d..aeab392 100644 --- a/cli/src/fill.rs +++ b/cli/src/fill.rs @@ -90,7 +90,7 @@ pub fn run( let start_time = Instant::now(); let mut slot_data = slot_data.clone(); - slot_data.insert("project_name".to_string(), get_project_name(project_dir)); + slot_data.insert("_project_name".to_string(), get_project_name(project_dir)); // CR(devin): when looking at the below code, this likely should be pushed // into the spackle lib itself, there are too many implementation details diff --git a/docs/configuration.md b/docs/configuration.md index f787f8b..9acf7e0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -10,7 +10,7 @@ A spackle project is defined by a `spackle.toml` file at the root directory. Bel Universal slots are available in all slot environments (`.j2` file contents, file names, {s} fields). -- project_name `string` +- `_project_name` `string` - The name of the project, defined by the name of the output directory ## Project-level config diff --git a/src/core/copy.rs b/src/core/copy.rs index b86535b..d0002ed 100644 --- a/src/core/copy.rs +++ b/src/core/copy.rs @@ -205,7 +205,7 @@ mod tests { fs::write( src_dir.join(format!("{}.tmpl", "{{template_name}}")), // copy will not do any replacement so contents should remain as is - "{{project_name}}", + "{{_project_name}}", ) .unwrap(); assert!(src_dir.join("{{template_name}}.tmpl").exists()); @@ -216,7 +216,7 @@ mod tests { &vec![], &HashMap::from([ ("template_name".to_string(), "template".to_string()), - ("project_name".to_string(), "foo".to_string()), + ("_project_name".to_string(), "foo".to_string()), ]), ) .unwrap(); diff --git a/src/core/hook.rs b/src/core/hook.rs index eeeb457..f26373f 100644 --- a/src/core/hook.rs +++ b/src/core/hook.rs @@ -117,7 +117,7 @@ pub fn run_hooks_stream( run_as_user: Option, ) -> Result, Error> { let mut slot_data = slot_data.clone(); - slot_data.insert("project_name".to_string(), get_project_name(dir.as_ref())); + slot_data.insert("_project_name".to_string(), get_project_name(dir.as_ref())); let mut skipped_hooks = Vec::new(); let mut queued_hooks = Vec::new(); @@ -641,7 +641,7 @@ mod tests { }, Hook { key: "2".to_string(), - command: vec!["echo".to_string(), "{{ project_name }}".to_string()], + command: vec!["echo".to_string(), "{{ _project_name }}".to_string()], r#if: None, optional: None, name: None, diff --git a/src/core/template.rs b/src/core/template.rs index 493d3bf..4863b9a 100644 --- a/src/core/template.rs +++ b/src/core/template.rs @@ -149,7 +149,7 @@ pub fn validate(dir: &PathBuf, slots: &Vec) -> Result<(), ValidateError> { .collect::>(), ) .map_err(|e| ValidateError::TeraError(e))?; - context.insert("project_name".to_string(), &get_project_name(dir)); + context.insert("_project_name".to_string(), &get_project_name(dir)); let errors = tera .get_template_names() diff --git a/src/lib.rs b/src/lib.rs index 5d4d9b0..b94cf29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,7 +77,7 @@ pub fn generate( let config = config::load(project_dir).map_err(GenerateError::BadConfig)?; let mut slot_data = slot_data.clone(); - slot_data.insert("project_name".to_string(), get_project_name(project_dir)); + slot_data.insert("_project_name".to_string(), get_project_name(project_dir)); // Copy all non-template files to the output directory copy::copy(project_dir, &out_dir, &config.ignore, &slot_data) diff --git a/tests/data/proj1/{{project_name}} b/tests/data/proj1/{{_project_name}} similarity index 100% rename from tests/data/proj1/{{project_name}} rename to tests/data/proj1/{{_project_name}} diff --git a/tests/data/templated/file.j2 b/tests/data/templated/file.j2 index 28c7384..e18d6b2 100644 --- a/tests/data/templated/file.j2 +++ b/tests/data/templated/file.j2 @@ -1,2 +1,2 @@ {{ slot_1 }} -{{ project_name }} \ No newline at end of file +{{ _project_name }} \ No newline at end of file From a79e886d46a58047ded0b5c53b999e56d815b1ba Mon Sep 17 00:00:00 2001 From: Devin Pastoor Date: Fri, 23 Aug 2024 18:43:12 -0500 Subject: [PATCH 02/13] preliminary implementation of tty for collecting slot information --- Cargo.lock | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ cli/Cargo.toml | 2 ++ cli/src/fill.rs | 43 +++++++++++++++++++++++++---- src/core/slot.rs | 2 +- 4 files changed, 113 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 85ca70a..d7f3101 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,6 +230,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -424,6 +435,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "cookie" version = "0.18.1" @@ -533,6 +557,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -549,6 +586,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -808,6 +851,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1773,6 +1825,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -1849,8 +1907,10 @@ dependencies = [ name = "spackle-cli" version = "0.1.0" dependencies = [ + "atty", "clap", "colored", + "dialoguer", "rocket", "rust-embed", "spackle", @@ -2263,6 +2323,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + [[package]] name = "unicode-xid" version = "0.2.4" @@ -2581,3 +2647,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" dependencies = [ "is-terminal", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4f05bb2..515e87f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -14,3 +14,5 @@ colored = "2.1.0" rocket = { version = "0.5.1", features = ["json"] } rust-embed = "8.4.0" tera = "1.20.0" +dialoguer = "0.11.0" +atty = "0.2.14" diff --git a/cli/src/fill.rs b/cli/src/fill.rs index aeab392..e7fef60 100644 --- a/cli/src/fill.rs +++ b/cli/src/fill.rs @@ -1,16 +1,17 @@ -use std::{collections::HashMap, fs, path::PathBuf, process::exit, time::Instant}; - use colored::Colorize; +use dialoguer::{theme::ColorfulTheme, Input}; use rocket::{futures::StreamExt, tokio}; use spackle::{ core::{ config::Config, copy, hook::{self, HookError, HookResult, HookResultKind, HookStreamResult}, - slot, template, + slot::{self, Slot, SlotType}, + template, }, get_project_name, }; +use std::{collections::HashMap, fs, path::PathBuf, process::exit, time::Instant}; use tokio::pin; use crate::{check, Cli}; @@ -28,7 +29,7 @@ pub fn run( println!(""); - let slot_data = slot + let mut slot_data = slot .iter() .filter_map(|data| match data.split_once('=') { Some((key, value)) => Some((key.to_string(), value.to_string())), @@ -45,6 +46,39 @@ pub fn run( .map(|(key, value)| (key.to_string(), value.to_string())) .collect::>(); + // at this point we've collected all the flags, so we should identify + // if any additional slots are needed and if we're in a tty context prompt + // for more slot info before validating + if atty::is(atty::Stream::Stdout) { + let missing_slots: Vec = config + .slots + .clone() + .into_iter() + .filter(|slot| !slot_data.contains_key(&slot.key)) + .collect(); + + missing_slots.iter().for_each(|slot| { + match &slot.r#type { + SlotType::String => { + // Handle String type here + let input: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt(&slot.key) + .interact_text() + .unwrap(); + slot_data.insert(slot.key.clone(), input); + } + SlotType::Boolean => { + // Handle Boolean type here + println!("Missing slot of type Boolean with value: {}", slot.key); + } + SlotType::Number => { + // Handle Number type here + println!("Missing slot of type Number with value: {}", slot.key); + } + } + }); + } + match slot::validate_data(&slot_data, &config.slots) { Ok(()) => {} Err(e) => { @@ -89,7 +123,6 @@ pub fn run( let start_time = Instant::now(); - let mut slot_data = slot_data.clone(); slot_data.insert("_project_name".to_string(), get_project_name(project_dir)); // CR(devin): when looking at the below code, this likely should be pushed diff --git a/src/core/slot.rs b/src/core/slot.rs index a3b8353..53c9bb4 100644 --- a/src/core/slot.rs +++ b/src/core/slot.rs @@ -2,7 +2,7 @@ use colored::Colorize; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fmt::Display}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Slot { pub key: String, #[serde(default)] From 2911fcf7dd8f7e5a964ed717bd42c61d692562b9 Mon Sep 17 00:00:00 2001 From: Devin Pastoor Date: Sat, 24 Aug 2024 10:20:16 -0400 Subject: [PATCH 03/13] implement single file, somewhat hacky --- Cargo.lock | 101 +++++++++++++++++++++++++++++++++++++-------- cli/Cargo.toml | 2 + cli/src/fill.rs | 105 ++++++++++++++++++++++++++++++----------------- cli/src/main.rs | 62 ++++++++++++++++++++-------- src/core/slot.rs | 2 +- 5 files changed, 197 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7f3101..d42a8be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,7 +653,7 @@ dependencies = [ "atomic 0.6.0", "pear", "serde", - "toml", + "toml 0.8.19", "uncased", "version_check", ] @@ -664,6 +664,17 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fronma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da11047dc6b6b3f21012056a0f9177e435a0ce4f34e1c5e7990b01342c0d4e49" +dependencies = [ + "serde", + "serde_yaml", + "toml 0.5.11", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -832,13 +843,19 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -989,6 +1006,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -996,7 +1023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", "serde", ] @@ -1056,6 +1083,12 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1611,7 +1644,7 @@ dependencies = [ "either", "figment", "futures", - "indexmap", + "indexmap 2.2.6", "log", "memchr", "multer", @@ -1643,7 +1676,7 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", - "indexmap", + "indexmap 2.2.6", "proc-macro2", "quote", "rocket_http", @@ -1663,7 +1696,7 @@ dependencies = [ "futures", "http 0.2.12", "hyper", - "indexmap", + "indexmap 2.2.6", "log", "memchr", "pear", @@ -1798,13 +1831,25 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap 1.9.3", + "ryu", + "serde", + "yaml-rust", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1896,7 +1941,7 @@ dependencies = [ "tera", "tokio", "tokio-stream", - "toml", + "toml 0.8.19", "tracing", "tracing-subscriber", "users", @@ -1911,10 +1956,12 @@ dependencies = [ "clap", "colored", "dialoguer", + "fronma", "rocket", "rust-embed", "spackle", "tera", + "toml 0.8.19", ] [[package]] @@ -2131,9 +2178,18 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -2143,20 +2199,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.15" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -2632,13 +2688,22 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 515e87f..63c9226 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -16,3 +16,5 @@ rust-embed = "8.4.0" tera = "1.20.0" dialoguer = "0.11.0" atty = "0.2.14" +fronma = { version = "0.2.0", features = ["toml"] } +toml = "0.8.19" diff --git a/cli/src/fill.rs b/cli/src/fill.rs index e7fef60..aaa2822 100644 --- a/cli/src/fill.rs +++ b/cli/src/fill.rs @@ -1,3 +1,4 @@ +use crate::{check, Cli}; use colored::Colorize; use dialoguer::{theme::ColorfulTheme, Input}; use rocket::{futures::StreamExt, tokio}; @@ -12,23 +13,36 @@ use spackle::{ get_project_name, }; use std::{collections::HashMap, fs, path::PathBuf, process::exit, time::Instant}; +use tera::{Context, Tera}; use tokio::pin; -use crate::{check, Cli}; - -pub fn run( +pub fn run_file( + template: String, slot: &Vec, - hook: &Vec, - project_dir: &PathBuf, - out: &PathBuf, - config: &Config, - cli: &Cli, -) { - // First, run spackle check - check::run(project_dir, config); + slots: Vec +) -> Result { + let mut slot_data = collect_slot_data(slot, slots.clone()); - println!(""); + // TODO: refactor all this is literally copy-pasted from run + match slot::validate_data(&slot_data, &slots) { + Ok(()) => {} + Err(e) => { + eprintln!( + "{}\n{}", + "❌ Error with supplied data".bright_red(), + e.to_string().red() + ); + + exit(1); + } + } + // TODO: end copy-paste + let context = Context::from_serialize(slot_data)?; + Tera::one_off(template.as_str(), &context, false) +} + +fn collect_slot_data(slot: &Vec, slots: Vec) -> HashMap { let mut slot_data = slot .iter() .filter_map(|data| match data.split_once('=') { @@ -50,34 +64,49 @@ pub fn run( // if any additional slots are needed and if we're in a tty context prompt // for more slot info before validating if atty::is(atty::Stream::Stdout) { - let missing_slots: Vec = config - .slots - .clone() - .into_iter() - .filter(|slot| !slot_data.contains_key(&slot.key)) - .collect(); - - missing_slots.iter().for_each(|slot| { - match &slot.r#type { - SlotType::String => { - // Handle String type here - let input: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt(&slot.key) - .interact_text() - .unwrap(); - slot_data.insert(slot.key.clone(), input); - } - SlotType::Boolean => { - // Handle Boolean type here - println!("Missing slot of type Boolean with value: {}", slot.key); - } - SlotType::Number => { - // Handle Number type here - println!("Missing slot of type Number with value: {}", slot.key); + let missing_slots: Vec = slots + .into_iter() + .filter(|slot| !slot_data.contains_key(&slot.key)) + .collect(); + + missing_slots.iter().for_each(|slot| { + match &slot.r#type { + SlotType::String => { + // Handle String type here + let input: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt(&slot.key) + .interact_text() + .unwrap(); + slot_data.insert(slot.key.clone(), input); + } + SlotType::Boolean => { + // Handle Boolean type here + println!("Missing slot of type Boolean with value: {}", slot.key); + } + SlotType::Number => { + // Handle Number type here + println!("Missing slot of type Number with value: {}", slot.key); + } } - } - }); + }); } + slot_data +} + +pub fn run( + slot: &Vec, + hook: &Vec, + project_dir: &PathBuf, + out: &PathBuf, + config: &Config, + cli: &Cli, +) { + // First, run spackle check + check::run(project_dir, config); + + println!(""); + + let mut slot_data = collect_slot_data(slot, config.slots.clone()); match slot::validate_data(&slot_data, &config.slots) { Ok(()) => {} diff --git a/cli/src/main.rs b/cli/src/main.rs index 89f112f..1e4883d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,8 +1,10 @@ use clap::{command, Parser, Subcommand}; use colored::Colorize; +use fronma::engines::Toml; +use fronma::parser::parse_with_engine; use spackle::core::config::{self, Config}; +use std::fs; use std::{path::PathBuf, process::exit}; - mod check; mod fill; mod info; @@ -13,6 +15,9 @@ struct Cli { #[command(subcommand)] command: Commands, + #[arg(short = 'f', long, default_value = None, global = true)] + file: Option, + /// The directory of the spackle project. Defaults to the current directory. #[arg(short = 'D', long, default_value = ".", global = true)] dir: PathBuf, @@ -51,7 +56,7 @@ fn main() { let cli = Cli::parse(); // Ensure the output directory is not the same as the project directory - if cli.out == cli.dir { + if cli.file.is_none() && cli.out == cli.dir { eprintln!( "{}\n{}", "❌ Output directory cannot be the same as project directory".bright_red(), @@ -61,9 +66,28 @@ fn main() { } let project_dir = cli.dir.clone(); - + let config = if cli.file.is_none() { + match config::load(&project_dir) { + Ok(config) => config, + Err(e) => { + eprintln!( + "❌ {}\n{}", + "Error loading project config".bright_red(), + e.to_string().red() + ); + exit(1); + } + } + } else { + let file_contents = fs::read_to_string(cli.file.clone().unwrap()).unwrap(); + // TODO: don't duplicate this lol + parse_with_engine::(&file_contents) + .unwrap() + .headers + // parse config off file frontmatter + }; // Check if the project directory is a spackle project - if !project_dir.join("spackle.toml").exists() { + if cli.file.is_none() && !project_dir.join("spackle.toml").exists() { eprintln!( "{}\n{}", "❌ Provided directory is not a spackle project".bright_red(), @@ -73,25 +97,27 @@ fn main() { } // Load the config - let config = match config::load(&project_dir) { - Ok(config) => config, - Err(e) => { - eprintln!( - "❌ {}\n{}", - "Error loading project config".bright_red(), - e.to_string().red() - ); - exit(1); - } - }; - - print_project_info(&project_dir, &config); + if cli.file.is_none() { + print_project_info(&project_dir, &config); + } match &cli.command { Commands::Check => check::run(&project_dir, &config), Commands::Info {} => info::run(&config), Commands::Fill { slot, hook } => { - fill::run(slot, hook, &project_dir, &cli.out, &config, &cli) + if cli.file.is_none() { + fill::run(slot, hook, &project_dir, &cli.out, &config, &cli) + } else { + // TODO: refactor - at the moment this is also duplicated in the config parsing logic + // where its read in prior. just doing this quick and dirty now where not dealing with + // the scoping rules/conditions of file vs dir + let file_contents = fs::read_to_string(cli.file.clone().unwrap()).unwrap(); + let result = + parse_with_engine::(&file_contents) + .unwrap(); + let res = fill::run_file(result.body.to_string(), slot, config.slots); + println!("{}", res.unwrap()); + } } } } diff --git a/src/core/slot.rs b/src/core/slot.rs index 53c9bb4..29c7d7a 100644 --- a/src/core/slot.rs +++ b/src/core/slot.rs @@ -2,7 +2,7 @@ use colored::Colorize; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fmt::Display}; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Slot { pub key: String, #[serde(default)] From 1bae1b1a46d2875f9b99d22f66911e831adff098 Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Mon, 26 Aug 2024 13:41:27 -0400 Subject: [PATCH 04/13] Move some logic out of src/main, reuse common slot input parsing logic --- Cargo.lock | 1 + Cargo.toml | 1 + cli/Cargo.toml | 2 +- cli/src/fill.rs | 129 ++++++++++++++++++++++++++++--------- cli/src/main.rs | 86 +++++++++++-------------- src/core/config.rs | 19 +++++- src/lib.rs | 14 +++- tests/data/single_file.j2t | 14 ++++ 8 files changed, 183 insertions(+), 83 deletions(-) create mode 100644 tests/data/single_file.j2t diff --git a/Cargo.lock b/Cargo.lock index d42a8be..9f75dcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1933,6 +1933,7 @@ dependencies = [ "async-process", "async-stream", "colored", + "fronma", "getrandom", "polyjuice", "serde", diff --git a/Cargo.toml b/Cargo.toml index 2a394c4..9c5112f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ install-updater = true async-process = "2.2.3" async-stream = "0.3.5" colored = "2.1.0" +fronma = { version = "0.2.0", features = ["toml"] } getrandom = { version = "0.2.15", features = ["js"] } polyjuice = { git = "https://github.com/a2-ai/polyjuice" } serde = { version = "1.0.202", features = ["derive"] } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 63c9226..3ec4f38 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -16,5 +16,5 @@ rust-embed = "8.4.0" tera = "1.20.0" dialoguer = "0.11.0" atty = "0.2.14" -fronma = { version = "0.2.0", features = ["toml"] } toml = "0.8.19" +fronma = { version = "0.2.0", features = ["toml"] } diff --git a/cli/src/fill.rs b/cli/src/fill.rs index aaa2822..2305e9f 100644 --- a/cli/src/fill.rs +++ b/cli/src/fill.rs @@ -1,10 +1,11 @@ use crate::{check, Cli}; use colored::Colorize; use dialoguer::{theme::ColorfulTheme, Input}; +use fronma::parser::parse_with_engine; use rocket::{futures::StreamExt, tokio}; use spackle::{ core::{ - config::Config, + config::{self, Config}, copy, hook::{self, HookError, HookResult, HookResultKind, HookStreamResult}, slot::{self, Slot, SlotType}, @@ -12,36 +13,10 @@ use spackle::{ }, get_project_name, }; -use std::{collections::HashMap, fs, path::PathBuf, process::exit, time::Instant}; +use std::{collections::HashMap, fmt::Debug, fs, path::PathBuf, process::exit, time::Instant}; use tera::{Context, Tera}; use tokio::pin; -pub fn run_file( - template: String, - slot: &Vec, - slots: Vec -) -> Result { - let mut slot_data = collect_slot_data(slot, slots.clone()); - - // TODO: refactor all this is literally copy-pasted from run - match slot::validate_data(&slot_data, &slots) { - Ok(()) => {} - Err(e) => { - eprintln!( - "{}\n{}", - "❌ Error with supplied data".bright_red(), - e.to_string().red() - ); - - exit(1); - } - } - - // TODO: end copy-paste - let context = Context::from_serialize(slot_data)?; - Tera::one_off(template.as_str(), &context, false) -} - fn collect_slot_data(slot: &Vec, slots: Vec) -> HashMap { let mut slot_data = slot .iter() @@ -90,6 +65,9 @@ fn collect_slot_data(slot: &Vec, slots: Vec) -> HashMap, + hook: &Vec, + project_dir: &PathBuf, + out: &PathBuf, + config: &Config, + cli: &Cli, +) { let hook_data = hook .iter() .filter_map(|data| match data.split_once('=') { @@ -152,8 +147,6 @@ pub fn run( let start_time = Instant::now(); - slot_data.insert("_project_name".to_string(), get_project_name(project_dir)); - // CR(devin): when looking at the below code, this likely should be pushed // into the spackle lib itself, there are too many implementation details // in the CLi that would also need to be replicated in any api/other client @@ -357,3 +350,79 @@ pub fn run( } }); } + +pub fn run_single(slot_data: &HashMap, cli: &Cli) { + let start_time = Instant::now(); + + let file_contents = match fs::read_to_string(&cli.project_path) { + Ok(o) => o, + Err(e) => { + eprintln!( + "❌ {}\n{}", + "Error reading project file".bright_red(), + e.to_string().red() + ); + exit(1); + } + }; + + let body = match parse_with_engine::(&file_contents) { + Ok(result) => result, + Err(e) => { + eprintln!("❌ {}\n{:#?}", "Error parsing project file".bright_red(), e); + exit(1); + } + } + .body; + + let context = match Context::from_serialize(slot_data) { + Ok(context) => context, + Err(e) => { + eprintln!( + "❌ {}\n{}", + "Error parsing context".bright_red(), + e.to_string().red() + ); + exit(1); + } + }; + + let result = match Tera::one_off(body, &context, false) { + Ok(result) => result, + Err(e) => { + eprintln!( + "❌ {}\n{}", + "Error rendering template".bright_red(), + e.to_string().red() + ); + exit(1); + } + }; + + let output_path = cli + .out_dir + .join(get_project_name(&cli.project_path).as_str()); + + // TODO do we want to output to out_dir to make consistent with full project render? + // match fs::write(&output_path, result.clone()) { + // Ok(_) => {} + // Err(e) => { + // eprintln!( + // "❌ {}\n{}", + // "Error writing output file".bright_red(), + // e.to_string().red() + // ); + // exit(1); + // } + // } + + println!( + "⛽ Rendered file {}\n {}", + format!("in {:?}", start_time.elapsed()).dimmed(), + output_path.to_string_lossy().bold() + ); + + // if cli.verbose { + println!("\n{}\n{}", "contents".dimmed(), result); + // } +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 1e4883d..21b2e49 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,9 +1,6 @@ use clap::{command, Parser, Subcommand}; use colored::Colorize; -use fronma::engines::Toml; -use fronma::parser::parse_with_engine; use spackle::core::config::{self, Config}; -use std::fs; use std::{path::PathBuf, process::exit}; mod check; mod fill; @@ -15,16 +12,13 @@ struct Cli { #[command(subcommand)] command: Commands, - #[arg(short = 'f', long, default_value = None, global = true)] - file: Option, - - /// The directory of the spackle project. Defaults to the current directory. - #[arg(short = 'D', long, default_value = ".", global = true)] - dir: PathBuf, + /// The directory of the spackle project or the single file to render. Defaults to the current directory. + #[arg(short = 'p', long = "project", default_value = ".", global = true)] + project_path: PathBuf, /// The directory to render to. Defaults to 'render' within the current directory. Cannot be the same as the project directory. - #[arg(short = 'o', long, default_value = "render", global = true)] - out: PathBuf, + #[arg(short = 'o', long = "out", default_value = "render", global = true)] + out_dir: PathBuf, /// Whether to run in verbose mode. #[arg(short, long, global = true)] @@ -56,7 +50,7 @@ fn main() { let cli = Cli::parse(); // Ensure the output directory is not the same as the project directory - if cli.file.is_none() && cli.out == cli.dir { + if cli.project_path.is_dir() && cli.out_dir == cli.project_path { eprintln!( "{}\n{}", "❌ Output directory cannot be the same as project directory".bright_red(), @@ -65,9 +59,19 @@ fn main() { exit(2); } - let project_dir = cli.dir.clone(); - let config = if cli.file.is_none() { - match config::load(&project_dir) { + // Load the config + // this can either be a directory or a single file + let config = if cli.project_path.is_dir() { + if !cli.project_path.join("spackle.toml").exists() { + eprintln!( + "{}\n{}", + "❌ Provided directory is not a spackle project".bright_red(), + "Valid projects must have a spackle.toml file.".red() + ); + exit(1); + } + + match config::load_dir(&cli.project_path) { Ok(config) => config, Err(e) => { eprintln!( @@ -79,45 +83,33 @@ fn main() { } } } else { - let file_contents = fs::read_to_string(cli.file.clone().unwrap()).unwrap(); - // TODO: don't duplicate this lol - parse_with_engine::(&file_contents) - .unwrap() - .headers - // parse config off file frontmatter + match config::load_file(&cli.project_path) { + Ok(config) => config, + Err(e) => { + eprintln!( + "❌ {}\n{}", + "Error loading project file".bright_red(), + e.to_string().red() + ); + exit(1); + } + } }; - // Check if the project directory is a spackle project - if cli.file.is_none() && !project_dir.join("spackle.toml").exists() { - eprintln!( - "{}\n{}", - "❌ Provided directory is not a spackle project".bright_red(), - "Valid projects must have a spackle.toml file.".red() - ); - exit(1); - } - // Load the config - if cli.file.is_none() { - print_project_info(&project_dir, &config); + if cli.project_path.is_dir() { + print_project_info(&cli.project_path, &config); + } else { + println!( + "📄 Using project file {}\n", + cli.project_path.to_string_lossy().bold() + ); } match &cli.command { - Commands::Check => check::run(&project_dir, &config), + Commands::Check => check::run(&cli.project_path, &config), Commands::Info {} => info::run(&config), Commands::Fill { slot, hook } => { - if cli.file.is_none() { - fill::run(slot, hook, &project_dir, &cli.out, &config, &cli) - } else { - // TODO: refactor - at the moment this is also duplicated in the config parsing logic - // where its read in prior. just doing this quick and dirty now where not dealing with - // the scoping rules/conditions of file vs dir - let file_contents = fs::read_to_string(cli.file.clone().unwrap()).unwrap(); - let result = - parse_with_engine::(&file_contents) - .unwrap(); - let res = fill::run_file(result.body.to_string(), slot, config.slots); - println!("{}", res.unwrap()); - } + fill::run(slot, hook, &cli.project_path, &cli.out_dir, &config, &cli) } } } diff --git a/src/core/config.rs b/src/core/config.rs index 6902c81..f14e7db 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -1,5 +1,7 @@ use super::slot::Slot; use colored::Colorize; +use fronma::engines::Toml; +use fronma::parser::parse_with_engine; use serde::{Deserialize, Serialize}; use std::{fmt::Display, fs, io, path::PathBuf}; @@ -63,6 +65,7 @@ pub const CONFIG_FILE: &str = "spackle.toml"; pub enum Error { ReadError(io::Error), ParseError(toml::de::Error), + FronmaError(fronma::error::Error), } impl std::fmt::Display for Error { @@ -70,12 +73,13 @@ impl std::fmt::Display for Error { match self { Error::ReadError(e) => write!(f, "Error reading file\n{}", e), Error::ParseError(e) => write!(f, "Error parsing contents\n{}", e), + Error::FronmaError(e) => write!(f, "Error parsing single file\n{:?}", e), } } } // Loads the config for the given directory -pub fn load(dir: &PathBuf) -> Result { +pub fn load_dir(dir: &PathBuf) -> Result { let config_path = dir.join(CONFIG_FILE); let config_str = match fs::read_to_string(config_path) { @@ -91,6 +95,17 @@ pub fn load(dir: &PathBuf) -> Result { Ok(config) } +pub fn load_file(file: &PathBuf) -> Result { + let file_contents = match fs::read_to_string(file) { + Ok(o) => o, + Err(e) => return Err(Error::ReadError(e)), + }; + + parse_with_engine::(&file_contents) + .map(|parsed| parsed.headers) + .map_err(Error::FronmaError) +} + #[cfg(test)] mod tests { use tempdir::TempDir; @@ -103,7 +118,7 @@ mod tests { fs::write(&dir.join("spackle.toml"), "").unwrap(); - let result = load(&dir); + let result = load_dir(&dir); assert!(result.is_ok()); } diff --git a/src/lib.rs b/src/lib.rs index b94cf29..94cbb13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,14 @@ pub fn get_project_name(project_dir: &Path) -> String { Err(_) => return "".to_string(), }; + if path.is_file() { + return path + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + } + return path .file_name() .unwrap_or_default() @@ -74,7 +82,7 @@ pub fn generate( return Err(GenerateError::AlreadyExists(out_dir.clone())); } - let config = config::load(project_dir).map_err(GenerateError::BadConfig)?; + let config = config::load_dir(project_dir).map_err(GenerateError::BadConfig)?; let mut slot_data = slot_data.clone(); slot_data.insert("_project_name".to_string(), get_project_name(project_dir)); @@ -120,7 +128,7 @@ pub fn run_hooks_stream( hook_data: &HashMap, run_as_user: Option, ) -> Result, RunHooksError> { - let config = config::load(project_dir).map_err(RunHooksError::BadConfig)?; + let config = config::load_dir(project_dir).map_err(RunHooksError::BadConfig)?; let result = hook::run_hooks_stream( &config.hooks, @@ -144,7 +152,7 @@ pub fn run_hooks( hook_data: &HashMap, run_as_user: Option, ) -> Result, RunHooksError> { - let config = config::load(project_dir).map_err(RunHooksError::BadConfig)?; + let config = config::load_dir(project_dir).map_err(RunHooksError::BadConfig)?; let result = hook::run_hooks( &config.hooks, diff --git a/tests/data/single_file.j2t b/tests/data/single_file.j2t new file mode 100644 index 0000000..fa90ae7 --- /dev/null +++ b/tests/data/single_file.j2t @@ -0,0 +1,14 @@ +--- +[[slots]] +key = "cmd" +type = "String" +name = "command name" +description = "name of the command" + +[[slots]] +key = "description" +type = "String" +name = "command name" +description = "name of the command" +--- +{{ cmd }} {{ description }} \ No newline at end of file From 8e87cc184ae0cd06643f1b90f49b553cd6c64b19 Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Mon, 26 Aug 2024 13:55:08 -0400 Subject: [PATCH 05/13] Add dialoguer handling for other slot types --- cli/src/fill.rs | 37 +++++++++++++++++++++++++++-------- src/core/slot.rs | 8 +++++++- tests/data/types/spackle.toml | 17 ++++++++++++++++ 3 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 tests/data/types/spackle.toml diff --git a/cli/src/fill.rs b/cli/src/fill.rs index 2305e9f..1d02b16 100644 --- a/cli/src/fill.rs +++ b/cli/src/fill.rs @@ -13,7 +13,7 @@ use spackle::{ }, get_project_name, }; -use std::{collections::HashMap, fmt::Debug, fs, path::PathBuf, process::exit, time::Instant}; +use std::{collections::HashMap, fs, path::PathBuf, process::exit, time::Instant}; use tera::{Context, Tera}; use tokio::pin; @@ -47,20 +47,41 @@ fn collect_slot_data(slot: &Vec, slots: Vec) -> HashMap { - // Handle String type here - let input: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt(&slot.key) + let input = Input::with_theme(&ColorfulTheme::default()) + .with_prompt(&slot.get_name()) .interact_text() .unwrap(); + slot_data.insert(slot.key.clone(), input); } SlotType::Boolean => { - // Handle Boolean type here - println!("Missing slot of type Boolean with value: {}", slot.key); + let input = Input::with_theme(&ColorfulTheme::default()) + .with_prompt(&slot.get_name()) + .validate_with(|input: &String| -> Result<(), &str> { + // ensure input is a boolean + if input.parse::().is_err() { + return Err("Input must be a boolean".into()); + } + Ok(()) + }) + .interact() + .unwrap(); + + slot_data.insert(slot.key.clone(), input); } SlotType::Number => { - // Handle Number type here - println!("Missing slot of type Number with value: {}", slot.key); + let input = Input::with_theme(&ColorfulTheme::default()) + .with_prompt(&slot.get_name()) + .validate_with(|input: &String| -> Result<(), &str> { + if input.parse::().is_err() { + return Err("Input must be a number".into()); + } + Ok(()) + }) + .interact_text() + .unwrap(); + + slot_data.insert(slot.key.clone(), input); } } }); diff --git a/src/core/slot.rs b/src/core/slot.rs index 29c7d7a..127eed2 100644 --- a/src/core/slot.rs +++ b/src/core/slot.rs @@ -2,7 +2,7 @@ use colored::Colorize; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fmt::Display}; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Slot { pub key: String, #[serde(default)] @@ -57,6 +57,12 @@ impl Display for Error { } } +impl Slot { + pub fn get_name(&self) -> String { + self.name.clone().unwrap_or(self.key.clone()) + } +} + pub fn validate_data(data: &HashMap, slots: &Vec) -> Result<(), Error> { for entry in data.iter() { // Check if the data is assigned to a slot diff --git a/tests/data/types/spackle.toml b/tests/data/types/spackle.toml new file mode 100644 index 0000000..016f978 --- /dev/null +++ b/tests/data/types/spackle.toml @@ -0,0 +1,17 @@ +[[slots]] +key = "string_slot" +type = "String" +name = "String slot" +description = "This is a string slot description" + +[[slots]] +key = "number_slot" +type = "Number" +name = "Number slot" +description = "This is a number slot description" + +[[slots]] +key = "boolean_slot" +type = "Boolean" +name = "Boolean slot" +description = "This is a boolean slot description" From 94a8f746949109b188662733fe806a6b1cd05a49 Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Fri, 30 Aug 2024 13:45:30 -0400 Subject: [PATCH 06/13] Use internally tagged enum serialization for HookError --- src/core/hook.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/hook.rs b/src/core/hook.rs index eeeb457..206e389 100644 --- a/src/core/hook.rs +++ b/src/core/hook.rs @@ -39,6 +39,7 @@ impl Display for HookResultKind { } #[derive(Serialize, Debug)] +#[serde(tag = "type")] pub enum HookError { ConditionalFailed(ConditionalError), CommandLaunchFailed(#[serde(skip)] io::Error), From c9f47c04528d07234ec9e9e99c78a5c967f66066 Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Fri, 30 Aug 2024 13:51:23 -0400 Subject: [PATCH 07/13] Use internally tagged type for serialization for ConditionalError --- src/core/hook.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/hook.rs b/src/core/hook.rs index 206e389..420a73c 100644 --- a/src/core/hook.rs +++ b/src/core/hook.rs @@ -299,6 +299,7 @@ pub fn run_hooks( } #[derive(Serialize, Debug)] +#[serde(tag = "type")] pub enum ConditionalError { InvalidContext(#[serde(skip)] tera::Error), InvalidTemplate(#[serde(skip)] tera::Error), From b3cf5dec443361681063d794fe21fa2dafe7744b Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Fri, 30 Aug 2024 16:02:05 -0400 Subject: [PATCH 08/13] Preserve Vec stdout/err type --- cli/src/fill.rs | 24 ++++++++++++++++++++---- src/core/hook.rs | 17 ++++++++--------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/cli/src/fill.rs b/cli/src/fill.rs index 2df542d..b63f5c3 100644 --- a/cli/src/fill.rs +++ b/cli/src/fill.rs @@ -260,8 +260,16 @@ pub fn run( if cli.verbose { if let HookError::CommandExited { stdout, stderr, .. } = error { - eprintln!("\n {}\n{}", "stdout".bold().dimmed(), stdout); - eprintln!(" {}\n{}", "stderr".bold().dimmed(), stderr); + eprintln!( + "\n {}\n{}", + "stdout".bold().dimmed(), + String::from_utf8_lossy(&stdout) + ); + eprintln!( + " {}\n{}", + "stderr".bold().dimmed(), + String::from_utf8_lossy(&stderr) + ); } } @@ -278,8 +286,16 @@ pub fn run( ); if cli.verbose { - println!(" {}\n{}", "stdout".bold().dimmed(), stdout); - println!(" {}\n{}", "stderr".bold().dimmed(), stderr); + println!( + " {}\n{}", + "stdout".bold().dimmed(), + String::from_utf8_lossy(&stdout) + ); + println!( + " {}\n{}", + "stderr".bold().dimmed(), + String::from_utf8_lossy(&stderr) + ); } } HookResult { diff --git a/src/core/hook.rs b/src/core/hook.rs index 420a73c..56ab1c7 100644 --- a/src/core/hook.rs +++ b/src/core/hook.rs @@ -22,7 +22,7 @@ pub struct HookResult { #[derive(Serialize, Debug)] pub enum HookResultKind { Skipped(SkipReason), - Completed { stdout: String, stderr: String }, + Completed { stdout: Vec, stderr: Vec }, Failed(HookError), } @@ -45,8 +45,8 @@ pub enum HookError { CommandLaunchFailed(#[serde(skip)] io::Error), CommandExited { exit_code: i32, - stdout: String, - stderr: String, + stdout: Vec, + stderr: Vec, }, } @@ -245,8 +245,8 @@ pub fn run_hooks_stream( hook: hook.clone(), kind: HookResultKind::Failed(HookError::CommandExited { exit_code: output.status.code().unwrap_or(1), - stdout: String::from_utf8_lossy(&output.stdout).to_string(), - stderr: String::from_utf8_lossy(&output.stderr).to_string(), + stdout: output.stdout, + stderr: output.stderr, }), }); continue; @@ -257,8 +257,8 @@ pub fn run_hooks_stream( yield HookStreamResult::HookDone(HookResult { hook: hook.clone(), kind: HookResultKind::Completed { - stdout: String::from_utf8_lossy(&output.stdout).into(), - stderr: String::from_utf8_lossy(&output.stderr).into(), + stdout: output.stdout, + stderr: output.stderr, } }); } @@ -675,14 +675,13 @@ mod tests { results ); - // Assert that hook 2 outputs "." assert!( results.iter().any(|x| match x { HookResult { hook, kind: HookResultKind::Completed { stdout, .. }, .. - } if hook.key == "2" => stdout.trim() == "spackle", + } if hook.key == "2" => String::from_utf8_lossy(stdout) == "spackle\n", _ => false, }), "Hook '2' should output 'spackle', got {:?}", From 3a9f24edd9a6a9c8671a19d922263a1a1f121c3f Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Wed, 4 Sep 2024 13:49:21 -0400 Subject: [PATCH 09/13] Add project name test data --- tests/data/project_name/spackle.toml | 1 + tests/data/project_name/{{project_name}} | 1 + 2 files changed, 2 insertions(+) create mode 100644 tests/data/project_name/spackle.toml create mode 100644 tests/data/project_name/{{project_name}} diff --git a/tests/data/project_name/spackle.toml b/tests/data/project_name/spackle.toml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/data/project_name/spackle.toml @@ -0,0 +1 @@ + diff --git a/tests/data/project_name/{{project_name}} b/tests/data/project_name/{{project_name}} new file mode 100644 index 0000000..d3a1b63 --- /dev/null +++ b/tests/data/project_name/{{project_name}} @@ -0,0 +1 @@ +{{project_name}} From c6e7cb3556a541b4cb8f4605f94f042a5525c229 Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Wed, 4 Sep 2024 13:52:40 -0400 Subject: [PATCH 10/13] Update project name test + remove frontend ref in justfile --- justfile | 1 - my-new-project/project_name | 1 + tests/data/project_name/{{project_name}} | 1 - tests/data/project_name/{{project_name}}.j2 | 1 + 4 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 my-new-project/project_name delete mode 100644 tests/data/project_name/{{project_name}} create mode 100644 tests/data/project_name/{{project_name}}.j2 diff --git a/justfile b/justfile index 0b26234..9471ba7 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,5 @@ setup: lefthook install - cd frontend && bun install run *args="": cargo run -p spackle-cli {{args}} diff --git a/my-new-project/project_name b/my-new-project/project_name new file mode 100644 index 0000000..15bb968 --- /dev/null +++ b/my-new-project/project_name @@ -0,0 +1 @@ +Hi my name is project_name \ No newline at end of file diff --git a/tests/data/project_name/{{project_name}} b/tests/data/project_name/{{project_name}} deleted file mode 100644 index d3a1b63..0000000 --- a/tests/data/project_name/{{project_name}} +++ /dev/null @@ -1 +0,0 @@ -{{project_name}} diff --git a/tests/data/project_name/{{project_name}}.j2 b/tests/data/project_name/{{project_name}}.j2 new file mode 100644 index 0000000..2351a7f --- /dev/null +++ b/tests/data/project_name/{{project_name}}.j2 @@ -0,0 +1 @@ +Hi my name is {{project_name}} \ No newline at end of file From 34a6bf5f8bb36ffa96c5941a83fd9db8b853f653 Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Wed, 4 Sep 2024 13:55:06 -0400 Subject: [PATCH 11/13] Delete extra file --- my-new-project/project_name | 1 - 1 file changed, 1 deletion(-) delete mode 100644 my-new-project/project_name diff --git a/my-new-project/project_name b/my-new-project/project_name deleted file mode 100644 index 15bb968..0000000 --- a/my-new-project/project_name +++ /dev/null @@ -1 +0,0 @@ -Hi my name is project_name \ No newline at end of file From 1180b58e7e5c5fdb355eea18892cb5fd202fdd08 Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Thu, 5 Sep 2024 15:08:21 -0400 Subject: [PATCH 12/13] Prompt for output path if none specified --- cli/src/fill.rs | 43 +++++++++----------- cli/src/main.rs | 29 +++++++++---- tests/data/project_name/{{_project_name}}.j2 | 1 + tests/data/project_name/{{project_name}}.j2 | 1 - 4 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 tests/data/project_name/{{_project_name}}.j2 delete mode 100644 tests/data/project_name/{{project_name}}.j2 diff --git a/cli/src/fill.rs b/cli/src/fill.rs index 60a24bc..ddef8d3 100644 --- a/cli/src/fill.rs +++ b/cli/src/fill.rs @@ -96,7 +96,7 @@ pub fn run( slot: &Vec, hook: &Vec, project_dir: &PathBuf, - out: &PathBuf, + out_path: &PathBuf, config: &Config, cli: &Cli, ) { @@ -123,9 +123,9 @@ pub fn run( slot_data.insert("_project_name".to_string(), get_project_name(project_dir)); if cli.project_path.is_dir() { - run_multi(&slot_data, hook, project_dir, out, config, cli); + run_multi(&slot_data, hook, project_dir, out_path, config, cli); } else { - run_single(&slot_data, &cli) + run_single(&slot_data, out_path, &cli) } } @@ -388,7 +388,7 @@ pub fn run_multi( }); } -pub fn run_single(slot_data: &HashMap, cli: &Cli) { +pub fn run_single(slot_data: &HashMap, out_path: &PathBuf, cli: &Cli) { let start_time = Instant::now(); let file_contents = match fs::read_to_string(&cli.project_path) { @@ -436,30 +436,25 @@ pub fn run_single(slot_data: &HashMap, cli: &Cli) { } }; - let output_path = cli - .out_dir - .join(get_project_name(&cli.project_path).as_str()); - - // TODO do we want to output to out_dir to make consistent with full project render? - // match fs::write(&output_path, result.clone()) { - // Ok(_) => {} - // Err(e) => { - // eprintln!( - // "❌ {}\n{}", - // "Error writing output file".bright_red(), - // e.to_string().red() - // ); - // exit(1); - // } - // } + match fs::write(&out_path, result.clone()) { + Ok(_) => {} + Err(e) => { + eprintln!( + "❌ {}\n{}", + "Error writing output file".bright_red(), + e.to_string().red() + ); + exit(1); + } + } println!( "⛽ Rendered file {}\n {}", format!("in {:?}", start_time.elapsed()).dimmed(), - output_path.to_string_lossy().bold() + out_path.to_string_lossy().bold() ); - // if cli.verbose { - println!("\n{}\n{}", "contents".dimmed(), result); - // } + if cli.verbose { + println!("\n{}\n{}", "contents".dimmed(), result); + } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 21b2e49..3dafeef 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,5 +1,6 @@ use clap::{command, Parser, Subcommand}; use colored::Colorize; +use dialoguer::{theme::ColorfulTheme, Input}; use spackle::core::config::{self, Config}; use std::{path::PathBuf, process::exit}; mod check; @@ -16,9 +17,9 @@ struct Cli { #[arg(short = 'p', long = "project", default_value = ".", global = true)] project_path: PathBuf, - /// The directory to render to. Defaults to 'render' within the current directory. Cannot be the same as the project directory. - #[arg(short = 'o', long = "out", default_value = "render", global = true)] - out_dir: PathBuf, + /// The path to render to. If the project is a single file, this is the output file. If the project is a directory, this is the output directory. + #[arg(short = 'o', long = "out", global = true)] + out_dir: Option, /// Whether to run in verbose mode. #[arg(short, long, global = true)] @@ -49,12 +50,24 @@ fn main() { let cli = Cli::parse(); - // Ensure the output directory is not the same as the project directory - if cli.project_path.is_dir() && cli.out_dir == cli.project_path { + let out_path = match &cli.out_dir { + Some(path) => path, + None => &Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter the output path") + .interact_text() + .map(|p: String| PathBuf::from(p)) + .unwrap_or_else(|e| { + eprintln!("❌ {}", e.to_string().red()); + exit(1); + }), + }; + + // Ensure the output path doesn't exist + if out_path.exists() { eprintln!( "{}\n{}", - "❌ Output directory cannot be the same as project directory".bright_red(), - "Please choose a different output directory.".red() + "❌ Output path already exists".bright_red(), + "Please choose a different output path.".red() ); exit(2); } @@ -109,7 +122,7 @@ fn main() { Commands::Check => check::run(&cli.project_path, &config), Commands::Info {} => info::run(&config), Commands::Fill { slot, hook } => { - fill::run(slot, hook, &cli.project_path, &cli.out_dir, &config, &cli) + fill::run(slot, hook, &cli.project_path, &out_path, &config, &cli) } } } diff --git a/tests/data/project_name/{{_project_name}}.j2 b/tests/data/project_name/{{_project_name}}.j2 new file mode 100644 index 0000000..7c1d086 --- /dev/null +++ b/tests/data/project_name/{{_project_name}}.j2 @@ -0,0 +1 @@ +Hi my name is {{_project_name}} \ No newline at end of file diff --git a/tests/data/project_name/{{project_name}}.j2 b/tests/data/project_name/{{project_name}}.j2 deleted file mode 100644 index 2351a7f..0000000 --- a/tests/data/project_name/{{project_name}}.j2 +++ /dev/null @@ -1 +0,0 @@ -Hi my name is {{project_name}} \ No newline at end of file From 3756f7b5d4dabefed95cafcf61417cfe265fb77f Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Thu, 5 Sep 2024 15:19:40 -0400 Subject: [PATCH 13/13] Move out dir to fill command --- README.md | 9 ++++----- cli/src/fill.rs | 24 +++++++++++++++++++++++- cli/src/main.rs | 41 ++++++++++------------------------------- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index fff0bff..030a687 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,10 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -D, --dir The directory of the spackle project. Defaults to the current directory [default: .] - -o, --out The directory to render to. Defaults to 'render' within the current directory. Cannot be the same as the project directory [default: render] - -v, --verbose Whether to run in verbose mode - -h, --help Print help - -V, --version Print version + -p, --project The spackle project to use (either a directory or a single file). Defaults to the current directory [default: .] + -v, --verbose Whether to run in verbose mode + -h, --help Print help + -V, --version Print version ``` ## Project configuration diff --git a/cli/src/fill.rs b/cli/src/fill.rs index ddef8d3..1980acf 100644 --- a/cli/src/fill.rs +++ b/cli/src/fill.rs @@ -96,7 +96,7 @@ pub fn run( slot: &Vec, hook: &Vec, project_dir: &PathBuf, - out_path: &PathBuf, + out_path: &Option, config: &Config, cli: &Cli, ) { @@ -122,6 +122,28 @@ pub fn run( slot_data.insert("_project_name".to_string(), get_project_name(project_dir)); + let out_path = match &out_path { + Some(path) => path, + None => &Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter the output path") + .interact_text() + .map(|p: String| PathBuf::from(p)) + .unwrap_or_else(|e| { + eprintln!("❌ {}", e.to_string().red()); + exit(1); + }), + }; + + // Ensure the output path doesn't exist + if out_path.exists() { + eprintln!( + "{}\n{}", + "❌ Output path already exists".bright_red(), + "Please choose a different output path.".red() + ); + exit(2); + } + if cli.project_path.is_dir() { run_multi(&slot_data, hook, project_dir, out_path, config, cli); } else { diff --git a/cli/src/main.rs b/cli/src/main.rs index 3dafeef..20fb6e3 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,6 +1,5 @@ use clap::{command, Parser, Subcommand}; use colored::Colorize; -use dialoguer::{theme::ColorfulTheme, Input}; use spackle::core::config::{self, Config}; use std::{path::PathBuf, process::exit}; mod check; @@ -13,14 +12,10 @@ struct Cli { #[command(subcommand)] command: Commands, - /// The directory of the spackle project or the single file to render. Defaults to the current directory. + /// The spackle project to use (either a directory or a single file). Defaults to the current directory. #[arg(short = 'p', long = "project", default_value = ".", global = true)] project_path: PathBuf, - /// The path to render to. If the project is a single file, this is the output file. If the project is a directory, this is the output directory. - #[arg(short = 'o', long = "out", global = true)] - out_dir: Option, - /// Whether to run in verbose mode. #[arg(short, long, global = true)] verbose: bool, @@ -40,6 +35,10 @@ enum Commands { /// Toggle a given hook on or off #[arg(short = 'H', long)] hook: Vec, + + /// The location the output should be written to. If the project is a single file, this is the output file. If the project is a directory, this is the output directory. + #[arg(short = 'o', long = "out", global = true)] + out_path: Option, }, /// Checks the validity of a spackle project Check, @@ -50,28 +49,6 @@ fn main() { let cli = Cli::parse(); - let out_path = match &cli.out_dir { - Some(path) => path, - None => &Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter the output path") - .interact_text() - .map(|p: String| PathBuf::from(p)) - .unwrap_or_else(|e| { - eprintln!("❌ {}", e.to_string().red()); - exit(1); - }), - }; - - // Ensure the output path doesn't exist - if out_path.exists() { - eprintln!( - "{}\n{}", - "❌ Output path already exists".bright_red(), - "Please choose a different output path.".red() - ); - exit(2); - } - // Load the config // this can either be a directory or a single file let config = if cli.project_path.is_dir() { @@ -121,9 +98,11 @@ fn main() { match &cli.command { Commands::Check => check::run(&cli.project_path, &config), Commands::Info {} => info::run(&config), - Commands::Fill { slot, hook } => { - fill::run(slot, hook, &cli.project_path, &out_path, &config, &cli) - } + Commands::Fill { + slot, + hook, + out_path, + } => fill::run(slot, hook, &cli.project_path, out_path, &config, &cli), } }