From 954768bffd79b71529ecae01e3b45b5dd8726448 Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Wed, 16 Oct 2024 10:58:12 -0400 Subject: [PATCH 1/2] Add default slot values --- Cargo.lock | 182 ++++++++++++++++------- Cargo.toml | 1 + cli/Cargo.toml | 6 +- cli/src/fill.rs | 120 +++++++++------ cli/src/main.rs | 1 + cli/src/util/file_path_completer.rs | 107 +++++++++++++ cli/src/util/mod.rs | 1 + docs/configuration.md | 11 ++ src/core/hook.rs | 1 + src/core/slot.rs | 42 +++--- src/core/template.rs | 10 +- src/lib.rs | 1 + src/util/mod.rs | 0 tests/data/default_slot_val/spackle.toml | 28 ++++ tests/data/default_slot_val/test.j2 | 3 + 15 files changed, 387 insertions(+), 127 deletions(-) create mode 100644 cli/src/util/file_path_completer.rs create mode 100644 cli/src/util/mod.rs create mode 100644 src/util/mod.rs create mode 100644 tests/data/default_slot_val/spackle.toml create mode 100644 tests/data/default_slot_val/test.j2 diff --git a/Cargo.lock b/Cargo.lock index 0e91be8..d891c67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + [[package]] name = "async-channel" version = "2.3.1" @@ -268,6 +274,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -318,6 +330,12 @@ version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.6.0" @@ -435,19 +453,6 @@ 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" @@ -499,6 +504,31 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -550,26 +580,13 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" dependencies = [ - "bitflags", + "bitflags 2.6.0", "proc-macro2", "proc-macro2-diagnostics", "quote", "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" @@ -581,16 +598,16 @@ dependencies = [ ] [[package]] -name = "either" -version = "1.13.0" +name = "dyn-clone" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] -name = "encode_unicode" -version = "0.3.6" +name = "either" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encoding_rs" @@ -759,6 +776,24 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generator" version = "0.7.5" @@ -826,7 +861,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags", + "bitflags 2.6.0", "ignore", "walkdir", ] @@ -1033,6 +1068,23 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.6.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "is-terminal" version = "0.4.12" @@ -1163,6 +1215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -1186,6 +1239,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1554,7 +1616,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1758,7 +1820,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -1871,10 +1933,25 @@ dependencies = [ ] [[package]] -name = "shell-words" -version = "1.1.0" +name = "signal-hook" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] [[package]] name = "signal-hook-registry" @@ -1940,6 +2017,7 @@ dependencies = [ "strum_macros", "tempdir", "tera", + "thiserror", "tokio", "tokio-stream", "toml 0.8.19", @@ -1951,13 +2029,15 @@ dependencies = [ [[package]] name = "spackle-cli" -version = "0.2.0" +version = "0.3.0" dependencies = [ + "anyhow", "atty", "clap", "colored", - "dialoguer", "fronma", + "fuzzy-matcher", + "inquire", "rocket", "rust-embed", "spackle", @@ -2065,18 +2145,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -2380,6 +2460,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.13" @@ -2713,9 +2799,3 @@ 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/Cargo.toml b/Cargo.toml index b8b4368..af93536 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ serde = { version = "1.0.202", features = ["derive"] } strum_macros = "0.26.2" tempdir = "0.3.7" tera = "1.19.1" +thiserror = "1.0.64" tokio = { version = "1.38.0", features = ["macros", "rt", "rt-multi-thread"] } tokio-stream = "0.1.15" toml = "0.8.13" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3ecc2e3..f6ebc15 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spackle-cli" -version = "0.2.0" +version = "0.3.0" edition = "2021" repository = "https://github.com/a2-ai/spackle" @@ -15,7 +15,9 @@ 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" toml = "0.8.19" fronma = { version = "0.2.0", features = ["toml"] } +inquire = "0.7.5" +anyhow = "1.0.89" +fuzzy-matcher = "0.3.7" diff --git a/cli/src/fill.rs b/cli/src/fill.rs index 1f3b426..a3e2702 100644 --- a/cli/src/fill.rs +++ b/cli/src/fill.rs @@ -1,7 +1,8 @@ -use crate::{check, Cli}; +use crate::{check, util::file_path_completer::FilePathCompleter, Cli}; +use anyhow::{Context, Result}; use colored::Colorize; -use dialoguer::{theme::ColorfulTheme, Input}; use fronma::parser::parse_with_engine; +use inquire::{Confirm, CustomType, Text}; use rocket::{futures::StreamExt, tokio}; use spackle::{ core::{ @@ -14,10 +15,10 @@ use spackle::{ get_project_name, }; use std::{collections::HashMap, fs, path::PathBuf, process::exit, time::Instant}; -use tera::{Context, Tera}; +use tera::Tera; use tokio::pin; -fn collect_slot_data(slot: &Vec, slots: Vec) -> HashMap { +fn collect_slot_data(slot: &Vec, slots: Vec) -> Result> { let mut slot_data = slot .iter() .filter_map(|data| match data.split_once('=') { @@ -40,56 +41,70 @@ fn collect_slot_data(slot: &Vec, slots: Vec) -> HashMap = slots + .clone() .into_iter() .filter(|slot| !slot_data.contains_key(&slot.key)) .collect(); - missing_slots.iter().for_each(|slot| { + for slot in missing_slots { match &slot.r#type { SlotType::String => { - let input = Input::with_theme(&ColorfulTheme::default()) - .with_prompt(&slot.get_name()) - .interact_text() - .unwrap(); + let slot_name = slot.get_name(); + let default_str = slot.default.clone().unwrap_or_default(); + let mut input = Text::new(&slot_name) + .with_help_message(slot.description.as_deref().unwrap_or_default()); + + if let Some(_default) = slot.default { + // We can unwrap here because we've done prior validation + input = input.with_default(default_str.as_str()); + } + + let value = input + .prompt() + .with_context(|| format!("Error getting input for slot: {}", slot.key))?; - slot_data.insert(slot.key.clone(), input); + slot_data.insert(slot.key.clone(), value.to_string()); } SlotType::Boolean => { - 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(); + let slot_name = slot.get_name(); + let mut input = Confirm::new(&slot_name) + .with_help_message(slot.description.as_deref().unwrap_or_default()); + + if let Some(default) = slot.default { + // We can unwrap here because we've done prior validation + input = input.with_default(default.parse::().unwrap()); + } + + let value = input + .prompt() + .with_context(|| format!("Error getting input for slot: {}", slot.key))?; - slot_data.insert(slot.key.clone(), input); + slot_data.insert(slot.key.clone(), value.to_string()); } SlotType::Number => { - 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(); + let slot_name = slot.get_name(); + let mut input = CustomType::::new(&slot_name) + .with_error_message("Please type a valid number") + .with_help_message(slot.description.as_deref().unwrap_or_default()); + + if let Some(default) = slot.default { + // We can unwrap here because we've done prior validation + input = input.with_default(default.parse::().unwrap()); + } + + let value = input + .prompt() + .with_context(|| format!("Error getting input for slot: {}", slot.key))?; - slot_data.insert(slot.key.clone(), input); + slot_data.insert(slot.key.clone(), value.to_string()); } } - }); + } } println!(); - slot_data + Ok(slot_data) } pub fn run( @@ -105,7 +120,13 @@ pub fn run( println!(""); - let mut slot_data = collect_slot_data(slot, config.slots.clone()); + let mut slot_data = match collect_slot_data(slot, config.slots.clone()) { + Ok(slot_data) => slot_data, + Err(e) => { + eprintln!("❌ {}", format!("{:?}", e).red()); + exit(1); + } + }; match slot::validate_data(&slot_data, &config.slots) { Ok(()) => {} @@ -122,14 +143,23 @@ pub fn run( 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); - }), + // Cannot use CustomType here because PathBuf does not implement ToString + None => { + let path = &Text::new("Enter the output path") + .with_help_message("The path to output the filled project") + .with_autocomplete(FilePathCompleter::default()) + .prompt(); + + println!(); + + match path { + Ok(p) => &PathBuf::from(p), + Err(e) => { + eprintln!("❌ {}", e.to_string().red()); + exit(1); + } + } + } }; slot_data.insert("_project_name".to_string(), get_project_name(&out_path)); @@ -434,7 +464,7 @@ pub fn run_single(slot_data: &HashMap, out_path: &PathBuf, cli: } .body; - let context = match Context::from_serialize(slot_data) { + let context = match tera::Context::from_serialize(slot_data) { Ok(context) => context, Err(e) => { eprintln!( diff --git a/cli/src/main.rs b/cli/src/main.rs index 20fb6e3..63a815d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -5,6 +5,7 @@ use std::{path::PathBuf, process::exit}; mod check; mod fill; mod info; +mod util; #[derive(Parser)] #[command(version, about, long_about = None)] diff --git a/cli/src/util/file_path_completer.rs b/cli/src/util/file_path_completer.rs new file mode 100644 index 0000000..fc4e269 --- /dev/null +++ b/cli/src/util/file_path_completer.rs @@ -0,0 +1,107 @@ +use std::io::ErrorKind; + +use fuzzy_matcher::skim::SkimMatcherV2; +use fuzzy_matcher::FuzzyMatcher; +use inquire::{ + autocompletion::{Autocomplete, Replacement}, + CustomUserError, +}; + +#[derive(Clone, Default)] +pub struct FilePathCompleter { + input: String, + paths: Vec, +} + +impl FilePathCompleter { + fn update_input(&mut self, input: &str) -> Result<(), CustomUserError> { + if input == self.input && !self.paths.is_empty() { + return Ok(()); + } + + self.input = input.to_owned(); + self.paths.clear(); + + let input_path = std::path::PathBuf::from(input); + + let fallback_parent = input_path + .parent() + .map(|p| { + if p.to_string_lossy() == "" { + std::path::PathBuf::from(".") + } else { + p.to_owned() + } + }) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + + let scan_dir = if input.ends_with('/') { + input_path + } else { + fallback_parent.clone() + }; + + let entries = match std::fs::read_dir(scan_dir) { + Ok(read_dir) => Ok(read_dir), + Err(err) if err.kind() == ErrorKind::NotFound => std::fs::read_dir(fallback_parent), + Err(err) => Err(err), + }? + .collect::, _>>()?; + + for entry in entries { + let path = entry.path(); + let path_str = if path.is_dir() { + format!("{}/", path.to_string_lossy()) + } else { + path.to_string_lossy().to_string() + }; + + self.paths.push(path_str); + } + + Ok(()) + } + + fn fuzzy_sort(&self, input: &str) -> Vec<(String, i64)> { + let mut matches: Vec<(String, i64)> = self + .paths + .iter() + .filter_map(|path| { + SkimMatcherV2::default() + .smart_case() + .fuzzy_match(path, input) + .map(|score| (path.clone(), score)) + }) + .collect(); + + matches.sort_by(|a, b| b.1.cmp(&a.1)); + matches + } +} + +impl Autocomplete for FilePathCompleter { + fn get_suggestions(&mut self, input: &str) -> Result, CustomUserError> { + self.update_input(input)?; + + let matches = self.fuzzy_sort(input); + Ok(matches.into_iter().take(15).map(|(path, _)| path).collect()) + } + + fn get_completion( + &mut self, + input: &str, + highlighted_suggestion: Option, + ) -> Result { + self.update_input(input)?; + + Ok(if let Some(suggestion) = highlighted_suggestion { + Replacement::Some(suggestion) + } else { + let matches = self.fuzzy_sort(input); + matches + .first() + .map(|(path, _)| Replacement::Some(path.clone())) + .unwrap_or(Replacement::None) + }) + } +} diff --git a/cli/src/util/mod.rs b/cli/src/util/mod.rs new file mode 100644 index 0000000..0373f2b --- /dev/null +++ b/cli/src/util/mod.rs @@ -0,0 +1 @@ +pub mod file_path_completer; diff --git a/docs/configuration.md b/docs/configuration.md index 90d1125..e311c2d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -35,6 +35,7 @@ key = "slot_name" type = "string" name = "Slot name" description = "A description of the slot" +default = "default value" ``` ### key `string` @@ -73,6 +74,16 @@ The human-friendly description of the slot. description = "A description of the slot" ``` +### default `string` + +The default value of the slot. The CLI will use the default value if one is not provided by the user (e.g. they press enter without typing anything). + +For library consumers, is up to you to decide whether to use the default value or not. The generate function will not use the default value if the slot is not provided, and will instead error if a slot is not provided properly. + +```toml +default = "default value" +``` + ## hooks `table` Hooks are defined by one or more `[[hooks]]` table entries in the `spackle.toml` file. Hooks are ran after the project is rendered and ran in the generated directory, and can be used to modify the project or enable specific functionality. diff --git a/src/core/hook.rs b/src/core/hook.rs index 945044b..dc2532c 100644 --- a/src/core/hook.rs +++ b/src/core/hook.rs @@ -162,6 +162,7 @@ pub fn run_hooks_stream( let mut commands = Vec::new(); for hook in templated_hooks { let cmd = match run_as_user { + // TODO spackle shouldn't need to depend on polyjuice, it should instead be able to receive an arbitrary Command from a consumer, who may choose to wrap it in polyjuice or not Some(ref user) => match polyjuice::cmd_as_user(&hook.command[0], user.clone()) { Ok(cmd) => cmd, Err(e) => { diff --git a/src/core/slot.rs b/src/core/slot.rs index 127eed2..70ae6bd 100644 --- a/src/core/slot.rs +++ b/src/core/slot.rs @@ -9,6 +9,7 @@ pub struct Slot { pub r#type: SlotType, pub name: Option, pub description: Option, + pub default: Option, } #[derive(Serialize, Deserialize, Debug, strum_macros::Display, Default, Clone)] @@ -38,6 +39,18 @@ impl Display for Slot { } } +impl Default for Slot { + fn default() -> Self { + Slot { + key: "".to_string(), + r#type: SlotType::String, + name: None, + description: None, + default: None, + } + } +} + #[derive(Debug)] pub enum Error { UnknownSlot(String), @@ -114,15 +127,11 @@ mod tests { let slots = vec![ Slot { key: "key".to_string(), - r#type: SlotType::String, - name: None, - description: None, + ..Default::default() }, Slot { key: "key2".to_string(), - r#type: SlotType::String, - name: None, - description: None, + ..Default::default() }, ]; @@ -139,15 +148,11 @@ mod tests { let slots = vec![ Slot { key: "key".to_string(), - r#type: SlotType::String, - name: None, - description: None, + ..Default::default() }, Slot { key: "key2".to_string(), - r#type: SlotType::String, - name: None, - description: None, + ..Default::default() }, ]; @@ -163,9 +168,7 @@ mod tests { fn extra_data() { let slots = vec![Slot { key: "key".to_string(), - r#type: SlotType::String, - name: None, - description: None, + ..Default::default() }]; let data = HashMap::from([("key", "value"), ("key2", "value2")]) @@ -182,14 +185,12 @@ mod tests { Slot { key: "key".to_string(), r#type: SlotType::Number, - name: None, - description: None, + ..Default::default() }, Slot { key: "key2".to_string(), r#type: SlotType::Boolean, - name: None, - description: None, + ..Default::default() }, ]; @@ -206,8 +207,7 @@ mod tests { let slots = vec![Slot { key: "key".to_string(), r#type: SlotType::Number, - name: None, - description: None, + ..Default::default() }]; let data = HashMap::from([("key", "value")]) diff --git a/src/core/template.rs b/src/core/template.rs index 6651dd2..2852667 100644 --- a/src/core/template.rs +++ b/src/core/template.rs @@ -168,8 +168,6 @@ pub fn validate(dir: &PathBuf, slots: &Vec) -> Result<(), ValidateError> { mod tests { use tempdir::TempDir; - use crate::core::slot::SlotType; - use super::*; #[test] @@ -197,9 +195,7 @@ mod tests { &PathBuf::from("tests/data/proj1"), &vec![Slot { key: "defined_field".to_string(), - r#type: SlotType::String, - name: None, - description: None, + ..Default::default() }], ); @@ -212,9 +208,7 @@ mod tests { &PathBuf::from("tests/data/proj2"), &vec![Slot { key: "defined_field".to_string(), - r#type: SlotType::String, - name: None, - description: None, + ..Default::default() }], ); diff --git a/src/lib.rs b/src/lib.rs index 9712539..ea91b18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ use tokio_stream::Stream; use users::User; pub mod core; +mod util; #[derive(Debug)] pub enum GenerateError { diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/default_slot_val/spackle.toml b/tests/data/default_slot_val/spackle.toml new file mode 100644 index 0000000..25be32b --- /dev/null +++ b/tests/data/default_slot_val/spackle.toml @@ -0,0 +1,28 @@ + +[[slots]] +key = "default_string" +type = "String" +name = "Default string" +description = "Default string" +default = "default value" + +[[slots]] +key = "default_number" +type = "Number" +name = "Default number" +description = "Default number" +default = "123" + +[[slots]] +key = "default_boolean" +type = "Boolean" +name = "Default boolean" +description = "Default boolean" +default = "true" + + +[[slots]] +key = "nondefault_value" +type = "String" +name = "Non-default value" +description = "Non-default value" diff --git a/tests/data/default_slot_val/test.j2 b/tests/data/default_slot_val/test.j2 new file mode 100644 index 0000000..5a4168e --- /dev/null +++ b/tests/data/default_slot_val/test.j2 @@ -0,0 +1,3 @@ +{{ default_string }} +{{ default_number }} +{{ default_boolean }} \ No newline at end of file From 8aebca5611cd7e604fee2c69758ab577dabb5116 Mon Sep 17 00:00:00 2001 From: Andriy Massimilla Date: Wed, 16 Oct 2024 11:37:41 -0400 Subject: [PATCH 2/2] Validate default values for slots --- cli/src/check.rs | 19 ++++++++++++++-- src/core/slot.rs | 24 ++++++++++++++++++++ tests/data/bad_default_slot_val/spackle.toml | 21 +++++++++++++++++ tests/data/bad_default_slot_val/test.j2 | 3 +++ 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 tests/data/bad_default_slot_val/spackle.toml create mode 100644 tests/data/bad_default_slot_val/test.j2 diff --git a/cli/src/check.rs b/cli/src/check.rs index 80f5033..36d3a5d 100644 --- a/cli/src/check.rs +++ b/cli/src/check.rs @@ -3,6 +3,7 @@ use std::{error::Error, path::PathBuf, process::exit, time::Instant}; use colored::Colorize; use spackle::core::{ config::Config, + slot, template::{self, ValidateError}, }; @@ -19,7 +20,7 @@ pub fn run(project_dir: &PathBuf, config: &Config) { match e { ValidateError::TeraError(e) => { eprintln!( - " {}\n {}\n", + "{}\n{}\n", "❌ Error validating template files".bright_red(), e.to_string().red() ); @@ -27,7 +28,7 @@ pub fn run(project_dir: &PathBuf, config: &Config) { ValidateError::RenderError(e) => { for (templ, e) in e { eprintln!( - " {}\n {}\n", + "{}\n{}\n", format!("❌ Template {} has errors", templ.bright_red().bold()) .bright_red(), e.source().map(|e| e.to_string()).unwrap_or_default().red() @@ -41,6 +42,20 @@ pub fn run(project_dir: &PathBuf, config: &Config) { } } + match slot::validate(&config.slots) { + Ok(()) => { + println!(" 👌 {}\n", "Slot data is valid".bright_green()); + } + Err(e) => { + eprintln!( + "{}\n{}\n", + "❌ Error validating slot configuration".bright_red(), + e.to_string().red() + ); + exit(1); + } + } + print_elapsed_time(start_time); } diff --git a/src/core/slot.rs b/src/core/slot.rs index 70ae6bd..d20c067 100644 --- a/src/core/slot.rs +++ b/src/core/slot.rs @@ -76,6 +76,30 @@ impl Slot { } } +pub fn validate(slots: &Vec) -> Result<(), Error> { + for slot in slots { + if let Some(default_value) = &slot.default { + match slot.r#type { + SlotType::String => { + // String always valid, no need to check + } + SlotType::Number => { + if default_value.parse::().is_err() { + return Err(Error::TypeMismatch(slot.key.clone(), "number".to_string())); + } + } + SlotType::Boolean => { + if default_value.parse::().is_err() { + return Err(Error::TypeMismatch(slot.key.clone(), "boolean".to_string())); + } + } + } + } + } + + Ok(()) +} + 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/bad_default_slot_val/spackle.toml b/tests/data/bad_default_slot_val/spackle.toml new file mode 100644 index 0000000..258ff10 --- /dev/null +++ b/tests/data/bad_default_slot_val/spackle.toml @@ -0,0 +1,21 @@ + +[[slots]] +key = "default_string" +type = "String" +name = "Default string" +description = "Default string" +default = "default" + +[[slots]] +key = "default_number" +type = "Number" +name = "Default number" +description = "Default number" +default = "notanumber" + +[[slots]] +key = "default_boolean" +type = "Boolean" +name = "Default boolean" +description = "Default boolean" +default = "invalid" diff --git a/tests/data/bad_default_slot_val/test.j2 b/tests/data/bad_default_slot_val/test.j2 new file mode 100644 index 0000000..5a4168e --- /dev/null +++ b/tests/data/bad_default_slot_val/test.j2 @@ -0,0 +1,3 @@ +{{ default_string }} +{{ default_number }} +{{ default_boolean }} \ No newline at end of file