diff --git a/Cargo.lock b/Cargo.lock index f1dd968..2579f08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,14 +11,20 @@ dependencies = [ "memchr", ] +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "cargo-mommy" version = "0.3.1" dependencies = [ + "bumpalo", "fastrand", "regex", "serde", - "serde-tuple-vec-map", "serde_json", ] @@ -120,15 +126,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-tuple-vec-map" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a04d0ebe0de77d7d445bb729a895dcb0a288854b267ca85f030ce51cdc578c82" -dependencies = [ - "serde", -] - [[package]] name = "serde_derive" version = "1.0.192" diff --git a/Cargo.toml b/Cargo.toml index f34880d..1f0a6bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,13 +15,20 @@ default = ["thirsty", "yikes"] thirsty = [] yikes = [] +runtime-responses = [] + [dependencies] +bumpalo = "3.14.0" fastrand = "1.8.0" +serde = { version = "1.0.151", features = ["derive"] } +serde_json = "1.0.91" +regex = "1.10.2" [build-dependencies] +bumpalo = "3.14.0" +fastrand = "1.8.0" serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.91" -serde-tuple-vec-map = "1.0.1" regex = "1.10.2" [workspace.metadata.release] diff --git a/build.rs b/build.rs index 2f32a6b..e6477b0 100644 --- a/build.rs +++ b/build.rs @@ -10,168 +10,47 @@ //! If your new mood or variable include... "spicy" terms, make sure to set an //! explicit `spiciness`. -use std::collections::BTreeMap; +#![allow(dead_code)] + use std::env; -use std::fmt::Write; use std::fs; -use std::ops::Range; use std::path::Path; -use regex::Regex; +// Mommy needs to use some code that's part of the final binary in her build +// script, too~ +#[path = "src/json.rs"] +mod json; +#[path = "src/template.rs"] +mod template; const RESPONSES: &str = include_str!("./responses.json"); -#[derive(PartialEq, Eq, PartialOrd, Ord, serde::Deserialize)] -#[serde(rename_all = "snake_case")] -enum Spiciness { - Chill, - Thirsty, - Yikes, -} - -impl Spiciness { - const CONFIGURED: Spiciness = if cfg!(feature = "yikes") { - Self::Yikes - } else if cfg!(feature = "thirsty") { - Self::Thirsty - } else { - Self::Chill - }; -} - -impl Default for Spiciness { - fn default() -> Self { - Self::Chill - } -} - -#[derive(serde::Deserialize)] -struct Mood { - positive: Vec, - negative: Vec, - overflow: Vec, - - #[serde(default)] - spiciness: Spiciness, -} - -#[derive(serde::Deserialize)] -struct Var { - defaults: Vec, - #[serde(default)] - env_key: Option, - - #[serde(default)] - spiciness: Spiciness, - - // Mommy needs a way to reference variables by index when doing template - // substitution. This type is the value of an ordered map, so we can just - // stick an index in after parsing~ - #[serde(skip)] - index: usize, -} - -#[derive(serde::Deserialize)] -struct Config { - vars: BTreeMap, - moods: BTreeMap, -} +// This needs to exist so Var::load() compiles, but don't worry, mommy will never +// call that from build.rs~ +const CONFIG: template::Config<'static> = template::Config { + vars: &[], + moods: &[], + mood: !0, + emote: !0, + pronoun: !0, + role: !0, +}; fn main() { let out_dir = &env::var("OUT_DIR").unwrap(); let dest_path = Path::new(out_dir).join("responses.rs"); - let mut config: Config = serde_json::from_str(RESPONSES).unwrap(); - let mut i = 0; - let mut vars = String::new(); - for (name, var) in config.vars.iter_mut() { - if var.spiciness > Spiciness::CONFIGURED { - continue; - } - var.index = i; - i += 1; - - let env_key = var - .env_key - .clone() - .unwrap_or_else(|| format!("{}S", name.to_uppercase())); - let defaults = &var.defaults; - let _ = write!( - vars, - r#"Var {{ env_key: "{env_key}", defaults: &{defaults:?} }},"# - ); - } - - let pattern = Regex::new(r"\{\w+\}").unwrap(); - let mut responses = String::new(); - for (name, mood) in &config.moods { - if mood.spiciness > Spiciness::CONFIGURED { - continue; - } - - let parse_response = |text: &str, out: &mut String| { - let _ = write!(out, "&["); - - // Mommy has to the template on matches for `pattern`, and generate - // an array of alternating Chunk::Text and Chunk::Var values. - let mut prev = 0; - for var in pattern.find_iter(text) { - let var_name = &var.as_str()[1..var.len() - 1]; - let var_idx = match config.vars.get(var_name) { - Some(var) => { - assert!( - var.spiciness <= Spiciness::CONFIGURED, - "{{{var_name}}} is too spicy!" - ); - var.index - } - None => panic!("undeclared variable {{{var_name}}}"), - }; - - let Range { start, end } = var.range(); - let prev_chunk = &text[prev..start]; - prev = end; - - let _ = write!(out, "Chunk::Text({prev_chunk:?}), Chunk::Var({var_idx}), "); - } - - let _ = write!(out, "Chunk::Text({:?})],", &text[prev..],); - }; - - let _ = write!(responses, "Mood {{ name: {name:?}, positive: &["); - for response in &mood.positive { - parse_response(response, &mut responses) - } - let _ = write!(responses, "], negative: &["); - for response in &mood.negative { - parse_response(response, &mut responses) - } - let _ = write!(responses, "], overflow: &["); - for response in &mood.overflow { - parse_response(response, &mut responses) - } - let _ = write!(responses, "] }},"); - } - - // Mommy needs some hard-coded vars at a specific location~ - let mood_idx = config.vars["mood"].index; - let emote_idx = config.vars["emote"].index; - let pronoun_idx = config.vars["pronoun"].index; - let role_idx = config.vars["role"].index; + let bump = bumpalo::Bump::new(); + let config = json::Config::parse("mommy", RESPONSES) + .unwrap() + .build("mommy", &bump) + .unwrap(); fs::write( dest_path, format!( - r" - static CONFIG: Config<'static> = Config {{ - vars: &[{vars}], - moods: &[{responses}], - }}; - static MOOD: &Var<'static> = &CONFIG.vars[{mood_idx}]; - static EMOTE: &Var<'static> = &CONFIG.vars[{emote_idx}]; - static PRONOUN: &Var<'static> = &CONFIG.vars[{pronoun_idx}]; - static ROLE: &Var<'static> = &CONFIG.vars[{role_idx}]; - " + "const CONFIG: template::Config<'static> = {};", + config.const_string() ), ) .unwrap(); diff --git a/src/json.rs b/src/json.rs new file mode 100644 index 0000000..4a43622 --- /dev/null +++ b/src/json.rs @@ -0,0 +1,200 @@ +use std::collections::BTreeMap; +use std::ops::Range; + +use bumpalo::Bump; +use regex::Regex; + +#[derive(serde::Deserialize)] +pub struct Config { + moods: BTreeMap, + vars: BTreeMap, +} + +impl Config { + /// Parses a JSON config; see `responses.json`. + pub fn parse(true_role: &str, data: &str) -> Result { + let mut config: Self = serde_json::from_str(data) + .map_err(|e| format!("the JSON blob you gave {true_role} seems to be invalid~\n{e}"))?; + + let mut i = 0; + for var in config.vars.values_mut() { + if var.spiciness > Spiciness::CONFIGURED { + continue; + } + + var.index = i; + i += 1; + } + + Ok(config) + } + + fn var_index(&self, true_role: &str, name: &str) -> Result { + self.vars.get(name).map(|v| v.index).ok_or_else(|| { + format!("{true_role} really needs a variable named {name:?} in order to work properly~") + }) + } + + /// Builds a "real" config. This mostly consists of converting formatting + /// strings into something simpler to interpolate strings into. + pub fn build<'a>( + &self, + true_role: &str, + arena: &'a Bump, + ) -> Result, String> { + use crate::template::*; + + let mut vars = Vec::new(); + for (name, var) in &self.vars { + if var.spiciness > Spiciness::CONFIGURED { + continue; + } + + let env_key = var + .env_key + .clone() + .unwrap_or_else(|| format!("{}S", name.to_uppercase())); + let defaults = var + .defaults + .iter() + .map(|s| &*arena.alloc_str(s)) + .collect::>(); + + vars.push(Var { + env_key: arena.alloc_str(&env_key), + defaults: arena.alloc_slice_copy(&defaults), + }); + } + + let pattern = Regex::new(r"\{\w+\}").unwrap(); + let mut moods = Vec::new(); + for (name, mood) in &self.moods { + if mood.spiciness > Spiciness::CONFIGURED { + continue; + } + + let mut error = None; + let mut parse_response = |text: &str| -> &'a [Chunk<'a>] { + let mut out = Vec::new(); + + // Mommy has to the template on matches for `pattern`, and generate + // an array of alternating Chunk::Text and Chunk::Var values. + let mut prev = 0; + for var in pattern.find_iter(text) { + let var_name = &var.as_str()[1..var.len() - 1]; + let var_idx = match self.vars.get(var_name) { + Some(var) => { + if var.spiciness > Spiciness::CONFIGURED && error.is_none() { + error = Some(format!( + "{{{var_name}}} is a little too spicy for {true_role}~" + )); + } + var.index + } + None => panic!("undeclared variable {{{var_name}}}"), + }; + + let Range { start, end } = var.range(); + let prev_chunk = &text[prev..start]; + prev = end; + + out.push(Chunk::Text(arena.alloc_str(prev_chunk))); + out.push(Chunk::Var(var_idx)); + } + + out.push(Chunk::Text(arena.alloc_str(&text[prev..]))); + arena.alloc_slice_copy(&out) + }; + + moods.push(Mood { + name: arena.alloc_str(name), + positive: arena.alloc_slice_copy( + &mood + .positive + .iter() + .map(|s| parse_response(s)) + .collect::>(), + ), + negative: arena.alloc_slice_copy( + &mood + .negative + .iter() + .map(|s| parse_response(s)) + .collect::>(), + ), + overflow: arena.alloc_slice_copy( + &mood + .overflow + .iter() + .map(|s| parse_response(s)) + .collect::>(), + ), + }); + + if let Some(error) = error { + return Err(error); + } + } + + Ok(Config { + vars: arena.alloc_slice_copy(&vars), + moods: arena.alloc_slice_copy(&moods), + + // Mommy needs some hard-coded vars at a specific location~ + mood: self.var_index(true_role, "mood")?, + emote: self.var_index(true_role, "emote")?, + pronoun: self.var_index(true_role, "pronoun")?, + role: self.var_index(true_role, "role")?, + }) + } +} + +#[derive(serde::Deserialize)] +struct Mood { + positive: Vec, + negative: Vec, + overflow: Vec, + + #[serde(default)] + spiciness: Spiciness, +} + +#[derive(serde::Deserialize)] +struct Var { + defaults: Vec, + #[serde(default)] + env_key: Option, + + #[serde(default)] + spiciness: Spiciness, + + // Mommy needs a way to reference variables by index when doing template + // substitution. This type is the value of an ordered map, so we can just + // stick an index in after parsing~ + #[serde(skip)] + index: usize, +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +enum Spiciness { + Chill, + Thirsty, + Yikes, +} + +impl Spiciness { + const CONFIGURED: Spiciness = if cfg!(feature = "yikes") { + Self::Yikes + } else if cfg!(feature = "thirsty") { + Self::Thirsty + } else { + Self::Chill + }; +} + +impl Default for Spiciness { + fn default() -> Self { + Self::Chill + } +} diff --git a/src/main.rs b/src/main.rs index f20743e..37bcd60 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,12 @@ use fastrand::Rng; use std::env; use std::io::IsTerminal; +use template::Config; + +mod template; + +#[cfg(feature = "runtime-responses")] +mod json; enum ResponseType { Positive, @@ -18,11 +24,14 @@ const RECURSION_LIMIT: u8 = 100; /// much of a mess~ const RECURSION_LIMIT_VAR: &str = "CARGO_MOMMY_RECURSION_LIMIT"; +// Mommy generates CONFIG and other global constants in build.rs~ +include!(concat!(env!("OUT_DIR"), "/responses.rs")); + fn main() { // Ideally mommy would use ExitCode but that's pretty new and mommy wants // to support more little ones~ let code = real_main().unwrap_or_else(|e| { - eprintln!("Error: {e:?}"); + pretty_print(Err(e.to_string())); 1 }); std::process::exit(code) @@ -59,6 +68,22 @@ fn real_main() -> Result> { "mommy".to_owned() }; + // Mommy needs her config~ + #[cfg(feature = "runtime-responses")] + let arena = bumpalo::Bump::new(); + let config = match env::var(format!("CARGO_{}S_RESPONSES", true_role.to_uppercase())) { + #[cfg(feature = "runtime-responses")] + Ok(mut file_or_json) => { + if !file_or_json.starts_with('{') { + file_or_json = std::fs::read_to_string(&file_or_json)?; + } + + let config = json::Config::parse(&true_role, &file_or_json)?; + &*arena.alloc(config.build(&true_role, &arena)?) + } + _ => &CONFIG, + }; + let cargo = env::var(format!("CARGO_{}S_ACTUAL", true_role.to_uppercase())) .or_else(|_| env::var("CARGO")) .unwrap_or_else(|_| "cargo".to_owned()); @@ -69,7 +94,8 @@ fn real_main() -> Result> { if let Ok(limit) = env::var(RECURSION_LIMIT_VAR) { if let Ok(n) = limit.parse::() { if n > RECURSION_LIMIT { - let mut response = select_response(&true_role, &rng, ResponseType::Overflow); + let mut response = + select_response(config, &true_role, &rng, ResponseType::Overflow); match &mut response { Ok(s) | Err(s) => { *s += "\nyou didn't set CARGO to something naughty, did you?\n" @@ -137,8 +163,8 @@ fn real_main() -> Result> { if let Err(e) = std::fs::copy(bin_path, new_bin_path) { Err(format!( "{role} couldn't copy {pronoun}self...\n{e:?}", - role = ROLE.load(&true_role, &rng)?, - pronoun = PRONOUN.load(&true_role, &rng)?, + role = config.role().load(&true_role, &rng)?, + pronoun = config.pronoun().load(&true_role, &rng)?, ))? } else { // Just exit immediately on success, don't try to get too clever here~ @@ -148,8 +174,8 @@ fn real_main() -> Result> { } else { Err(format!( "{role} couldn't copy {pronoun}self...\n(couldn't find own parent dir)", - role = ROLE.load(&true_role, &rng)?, - pronoun = PRONOUN.load(&true_role, &rng)?, + role = config.role().load(&true_role, &rng)?, + pronoun = config.pronoun().load(&true_role, &rng)?, ))?; } } @@ -169,9 +195,9 @@ fn real_main() -> Result> { // Time for mommy to tell you how you did~ let response = if status.success() { - select_response(&true_role, &rng, ResponseType::Positive) + select_response(config, &true_role, &rng, ResponseType::Positive) } else { - select_response(&true_role, &rng, ResponseType::Negative) + select_response(config, &true_role, &rng, ResponseType::Negative) }; pretty_print(response); @@ -202,15 +228,16 @@ fn is_quiet_mode_enabled(args: std::process::CommandArgs) -> bool { } fn select_response( + config: &Config, true_role: &str, rng: &Rng, response_type: ResponseType, ) -> Result { // Choose what mood mommy is in~ - let mood = MOOD.load(true_role, rng)?; + let mood = config.mood().load(true_role, rng)?; - let Some(group) = &CONFIG.moods.iter().find(|group| group.name == mood) else { - let supported_moods_str = CONFIG + let Some(group) = &config.moods.iter().find(|group| group.name == mood) else { + let supported_moods_str = config .moods .iter() .map(|group| group.name) @@ -218,8 +245,8 @@ fn select_response( .join(", "); return Err(format!( "{role} doesn't know how to feel {mood}... {pronoun} moods are {supported_moods_str}", - role = ROLE.load(true_role, rng)?, - pronoun = PRONOUN.load(true_role, rng)?, + role = config.role().load(true_role, rng)?, + pronoun = config.pronoun().load(true_role, rng)?, )); }; @@ -232,12 +259,12 @@ fn select_response( let response = &responses[rng.usize(..responses.len())]; // Apply options to the message template~ - let mut response = CONFIG.apply_template(true_role, response, rng)?; + let mut response = config.apply_template(true_role, response, rng)?; // Let mommy show a little emote~ let should_emote = rng.bool(); if should_emote { - if let Ok(emote) = EMOTE.load(true_role, rng) { + if let Ok(emote) = config.emote().load(true_role, rng) { response.push(' '); response.push_str(&emote); } @@ -247,99 +274,6 @@ fn select_response( Ok(response) } -// Mommy generates CONFIG and other global constants in build.rs~ -include!(concat!(env!("OUT_DIR"), "/responses.rs")); - -struct Config<'a> { - vars: &'a [Var<'a>], - moods: &'a [Mood<'a>], -} - -impl Config<'_> { - /// Applies a template by resolving `Chunk::Var`s against `self.vars`. - fn apply_template( - &self, - true_role: &str, - chunks: &[Chunk], - rng: &Rng, - ) -> Result { - let mut out = String::new(); - for chunk in chunks { - match chunk { - Chunk::Text(text) => out.push_str(text), - Chunk::Var(i) => out.push_str(&self.vars[*i].load(true_role, rng)?), - } - } - Ok(out) - } -} - -struct Mood<'a> { - name: &'a str, - // Each of mommy's response templates is an alternating sequence of - // Text and Var chunks; Text is literal text that should be printed as-is; - // Var is an index into mommy's CONFIG.vars table~ - positive: &'a [&'a [Chunk<'a>]], - negative: &'a [&'a [Chunk<'a>]], - overflow: &'a [&'a [Chunk<'a>]], -} - -enum Chunk<'a> { - Text(&'a str), - Var(usize), -} - -struct Var<'a> { - env_key: &'a str, - defaults: &'a [&'a str], -} - -impl Var<'_> { - /// Loads this variable and selects one of the possible values for it; - /// produces an in-character error message on failure. - fn load(&self, true_role: &str, rng: &Rng) -> Result { - // try to load custom settings from env vars~ - let var = env::var(self.env(true_role)); - let split; - - // parse the custom settings or use the builtins~ - let choices = match var.as_deref() { - Ok("") => &[], - Ok(value) => { - split = value.split('/').collect::>(); - split.as_slice() - } - Err(_) => self.defaults, - }; - - if choices.is_empty() { - // If there's no ROLES set, default to mommy's true nature~ - if self.env_key == "ROLES" { - return Ok(true_role.to_owned()); - } - - // Otherwise, report an error~ - let role = ROLE.load(true_role, rng)?; - return Err(format!( - "{role} needs at least one value for {}~", - self.env_key - )); - } - - // now select a choice from the options~ - Ok(choices[rng.usize(..choices.len())].to_owned()) - } - - /// Gets the name of the env var to load~ - fn env(&self, true_role: &str) -> String { - // Normally we'd load from CARGO_MOMMYS_* - // but if cargo-mommy is cargo-daddy, we should load CARGO_DADDYS_* instead~ - // If we have multiple words in our role, we must also be careful with spaces~ - let screaming_role = true_role.to_ascii_uppercase().replace(' ', "_"); - format!("CARGO_{screaming_role}S_{}", self.env_key) - } -} - #[cfg(test)] #[test] fn test() { diff --git a/src/template.rs b/src/template.rs new file mode 100644 index 0000000..c360b85 --- /dev/null +++ b/src/template.rs @@ -0,0 +1,207 @@ +use std::env; +use std::fmt; +use std::fmt::Write; + +use fastrand::Rng; + +#[derive(Copy, Clone)] +pub struct Config<'a> { + pub vars: &'a [Var<'a>], + pub moods: &'a [Mood<'a>], + + pub mood: usize, + pub emote: usize, + pub pronoun: usize, + pub role: usize, +} + +impl Config<'_> { + /// Returns the special MOODS variable. + pub fn mood(&self) -> &Var { + &self.vars[self.mood] + } + + /// Returns the special EMOTES variable. + pub fn emote(&self) -> &Var { + &self.vars[self.emote] + } + + /// Returns the special PRONOUNS variable. + pub fn pronoun(&self) -> &Var { + &self.vars[self.pronoun] + } + + /// Returns the special ROLES variable. + pub fn role(&self) -> &Var { + &self.vars[self.role] + } + + /// Returns a valid Rust expression in a string representing this + /// config's data. + #[allow(unused)] + pub fn const_string(&self) -> String { + format!("{:#?}", DebugByToConst(self)) + } + + /// Applies a template by resolving `Chunk::Var`s against `self.vars`. + pub fn apply_template( + &self, + true_role: &str, + chunks: &[Chunk], + rng: &Rng, + ) -> Result { + let mut out = String::new(); + for chunk in chunks { + match chunk { + Chunk::Text(text) => out.push_str(text), + Chunk::Var(i) => out.push_str(&self.vars[*i].load(true_role, rng)?), + } + } + Ok(out) + } +} + +impl ToConst for Config<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("crate::template::Config") + .field("vars", &DebugByToConst(&self.vars)) + .field("moods", &DebugByToConst(&self.moods)) + .field("mood", &self.mood) + .field("emote", &self.emote) + .field("pronoun", &self.pronoun) + .field("role", &self.role) + .finish() + } +} + +#[derive(Copy, Clone)] +pub struct Mood<'a> { + pub name: &'a str, + // Each of mommy's response templates is an alternating sequence of + // Text and Var chunks; Text is literal text that should be printed as-is; + // Var is an index into mommy's CONFIG.vars table~ + pub positive: &'a [&'a [Chunk<'a>]], + pub negative: &'a [&'a [Chunk<'a>]], + pub overflow: &'a [&'a [Chunk<'a>]], +} + +impl ToConst for Mood<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("crate::template::Mood") + .field("name", &self.name) + .field("positive", &DebugByToConst(&self.positive)) + .field("negative", &DebugByToConst(&self.negative)) + .field("overflow", &DebugByToConst(&self.overflow)) + .finish() + } +} + +#[derive(Copy, Clone)] +pub enum Chunk<'a> { + Text(&'a str), + Var(usize), +} + +impl ToConst for Chunk<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Text(x) => write!(f, "crate::template::Chunk::Text({x:?})"), + Self::Var(x) => write!(f, "crate::template::Chunk::Var({x:?})"), + } + } +} + +#[derive(Copy, Clone)] +pub struct Var<'a> { + pub env_key: &'a str, + pub defaults: &'a [&'a str], +} + +impl ToConst for Var<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("crate::template::Var") + .field("env_key", &self.env_key) + .field("defaults", &DebugByToConst(&self.defaults)) + .finish() + } +} + +impl Var<'_> { + /// Loads this variable and selects one of the possible values for it; + /// produces an in-character error message on failure. + pub fn load(&self, true_role: &str, rng: &Rng) -> Result { + // try to load custom settings from env vars~ + let var = env::var(self.env(true_role)); + let split; + + // parse the custom settings or use the builtins~ + let choices = match var.as_deref() { + Ok("") => &[], + Ok(value) => { + split = value.split('/').collect::>(); + split.as_slice() + } + Err(_) => self.defaults, + }; + + if choices.is_empty() { + // If there's no ROLES set, default to mommy's true nature~ + if self.env_key == "ROLES" { + return Ok(true_role.to_owned()); + } + + // Otherwise, report an error~ + let role = crate::CONFIG.role().load(true_role, rng)?; + return Err(format!( + "{role} needs at least one value for {}~", + self.env_key + )); + } + + // now select a choice from the options~ + Ok(choices[rng.usize(..choices.len())].to_owned()) + } + + /// Gets the name of the env var to load~ + pub fn env(&self, true_role: &str) -> String { + // Normally we'd load from CARGO_MOMMYS_* + // but if cargo-mommy is cargo-daddy, we should load CARGO_DADDYS_* instead~ + // If we have multiple words in our role, we must also be careful with spaces~ + let screaming_role = true_role.to_ascii_uppercase().replace(' ', "_"); + format!("CARGO_{screaming_role}S_{}", self.env_key) + } +} + +/// This is some nonsense mommy needs to convert responses into something she +/// can bake into her binary for you~ +trait ToConst { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result; +} + +struct DebugByToConst<'a, T>(&'a T); +impl fmt::Debug for DebugByToConst<'_, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ToConst::fmt(self.0, f) + } +} + +impl ToConst for usize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +impl ToConst for &str { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +impl ToConst for &[T] { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_char('&')?; + f.debug_list() + .entries(self.iter().map(DebugByToConst)) + .finish() + } +}