use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Default, JsonSchema, PartialEq)]
#[serde(default, deny_unknown_fields)]
pub struct CompletionsConfig {
pub generate: Option<String>,
pub from_build_out: Option<String>,
pub copy: Option<String>,
pub shells: Option<Vec<String>>,
pub dst: Option<String>,
}
#[derive(Debug, Clone, Serialize, Default, JsonSchema, PartialEq)]
#[serde(default, deny_unknown_fields)]
pub struct ManpagesConfig {
pub generate: Option<String>,
pub from_build_out: Option<String>,
pub copy: Option<String>,
pub dst: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum GenMode<'a> {
Generate(&'a str),
FromBuildOut(&'a str),
Copy(&'a str),
None,
}
fn enforce_single_mode<E: serde::de::Error>(
block: &str,
generate: bool,
from_build_out: bool,
copy: bool,
) -> Result<(), E> {
let set: Vec<&str> = [
("generate", generate),
("from_build_out", from_build_out),
("copy", copy),
]
.into_iter()
.filter_map(|(name, on)| on.then_some(name))
.collect();
if set.len() > 1 {
return Err(E::custom(format!(
"{block}: only one of `generate`, `from_build_out`, `copy` may be set \
(got {}); these are mutually-exclusive generation modes",
set.join(", ")
)));
}
Ok(())
}
impl CompletionsConfig {
pub const DEFAULT_DST: &'static str = "completions/";
pub const DEFAULT_SHELLS: &'static [&'static str] = &["bash", "zsh", "fish", "powershell"];
pub fn mode(&self) -> GenMode<'_> {
if let Some(g) = self.generate.as_deref() {
GenMode::Generate(g)
} else if let Some(b) = self.from_build_out.as_deref() {
GenMode::FromBuildOut(b)
} else if let Some(c) = self.copy.as_deref() {
GenMode::Copy(c)
} else {
GenMode::None
}
}
pub fn resolved_dst(&self) -> &str {
self.dst.as_deref().unwrap_or(Self::DEFAULT_DST)
}
pub fn resolved_shells(&self) -> Vec<String> {
match &self.shells {
Some(s) if !s.is_empty() => s.clone(),
_ => Self::DEFAULT_SHELLS.iter().map(|s| s.to_string()).collect(),
}
}
}
impl ManpagesConfig {
pub const DEFAULT_DST: &'static str = "man/man1/";
pub fn mode(&self) -> GenMode<'_> {
if let Some(g) = self.generate.as_deref() {
GenMode::Generate(g)
} else if let Some(b) = self.from_build_out.as_deref() {
GenMode::FromBuildOut(b)
} else if let Some(c) = self.copy.as_deref() {
GenMode::Copy(c)
} else {
GenMode::None
}
}
pub fn resolved_dst(&self) -> &str {
self.dst.as_deref().unwrap_or(Self::DEFAULT_DST)
}
}
impl<'de> Deserialize<'de> for CompletionsConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
struct Raw {
generate: Option<String>,
from_build_out: Option<String>,
copy: Option<String>,
shells: Option<Vec<String>>,
dst: Option<String>,
}
let raw = Raw::deserialize(deserializer)?;
enforce_single_mode(
"completions",
raw.generate.is_some(),
raw.from_build_out.is_some(),
raw.copy.is_some(),
)?;
Ok(CompletionsConfig {
generate: raw.generate,
from_build_out: raw.from_build_out,
copy: raw.copy,
shells: raw.shells,
dst: raw.dst,
})
}
}
impl<'de> Deserialize<'de> for ManpagesConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
struct Raw {
generate: Option<String>,
from_build_out: Option<String>,
copy: Option<String>,
dst: Option<String>,
}
let raw = Raw::deserialize(deserializer)?;
enforce_single_mode(
"manpages",
raw.generate.is_some(),
raw.from_build_out.is_some(),
raw.copy.is_some(),
)?;
Ok(ManpagesConfig {
generate: raw.generate,
from_build_out: raw.from_build_out,
copy: raw.copy,
dst: raw.dst,
})
}
}
pub fn completion_filename(binary: &str, shell: &str) -> String {
match shell.to_ascii_lowercase().as_str() {
"bash" => binary.to_string(),
"zsh" => format!("_{binary}"),
"fish" => format!("{binary}.fish"),
"powershell" | "pwsh" => format!("_{binary}.ps1"),
"elvish" => format!("{binary}.elv"),
"nushell" | "nu" => format!("{binary}.nu"),
"fig" => format!("{binary}.ts"),
other => format!("{binary}.{other}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_completions(yaml: &str) -> Result<CompletionsConfig, serde_yaml_ng::Error> {
serde_yaml_ng::from_str(yaml)
}
#[test]
fn mode_a_generate_parses() {
let c = parse_completions(
"generate: \"{{ .ArtifactPath }} completions {{ .Shell }}\"\nshells: [bash, zsh, nushell, elvish]\ndst: \"completions/\"",
)
.unwrap();
assert_eq!(
c.mode(),
GenMode::Generate("{{ .ArtifactPath }} completions {{ .Shell }}")
);
assert_eq!(
c.resolved_shells(),
vec!["bash", "zsh", "nushell", "elvish"]
);
assert_eq!(c.resolved_dst(), "completions/");
}
#[test]
fn mode_b_from_build_out_parses() {
let c = parse_completions("from_build_out: \"**/out/{{ .Binary }}.{bash,fish}\"").unwrap();
assert_eq!(
c.mode(),
GenMode::FromBuildOut("**/out/{{ .Binary }}.{bash,fish}")
);
}
#[test]
fn mode_c_copy_parses() {
let c = parse_completions("copy: \"contrib/completion/*\"").unwrap();
assert_eq!(c.mode(), GenMode::Copy("contrib/completion/*"));
}
#[test]
fn two_modes_at_once_is_error() {
let err = parse_completions("generate: \"x\"\ncopy: \"y\"").unwrap_err();
assert!(
err.to_string().contains("only one of"),
"expected mutual-exclusivity error, got: {err}"
);
}
#[test]
fn no_mode_is_noop() {
let c = parse_completions("shells: [bash]").unwrap();
assert_eq!(c.mode(), GenMode::None);
}
#[test]
fn default_shells_when_omitted() {
let c = parse_completions("generate: \"x\"").unwrap();
assert_eq!(
c.resolved_shells(),
vec!["bash", "zsh", "fish", "powershell"]
);
}
#[test]
fn manpages_two_modes_error() {
let err: Result<ManpagesConfig, _> =
serde_yaml_ng::from_str("generate: \"x\"\nfrom_build_out: \"y\"");
assert!(err.unwrap_err().to_string().contains("only one of"));
}
#[test]
fn manpages_default_dst() {
let m: ManpagesConfig =
serde_yaml_ng::from_str("generate: \"{{ .ArtifactPath }} --man\"").unwrap();
assert_eq!(m.resolved_dst(), "man/man1/");
}
#[test]
fn completion_filenames_follow_clap_convention() {
assert_eq!(completion_filename("rg", "bash"), "rg");
assert_eq!(completion_filename("rg", "zsh"), "_rg");
assert_eq!(completion_filename("rg", "fish"), "rg.fish");
assert_eq!(completion_filename("rg", "powershell"), "_rg.ps1");
assert_eq!(completion_filename("rg", "elvish"), "rg.elv");
assert_eq!(completion_filename("rg", "nushell"), "rg.nu");
assert_eq!(completion_filename("rg", "fig"), "rg.ts");
assert_eq!(completion_filename("rg", "weirdshell"), "rg.weirdshell");
}
}