use crate::CliResult;
use crate::cli::Cli;
use clap::CommandFactory;
use clap_complete::aot::{Shell, generate};
use clap_complete::engine::CompletionCandidate;
use std::io;
pub fn run(shell: Shell) -> CliResult<()> {
write_completions(shell, &mut io::stdout());
Ok(())
}
fn write_completions(shell: Shell, out: &mut impl io::Write) {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "faucet", out);
}
pub(crate) fn source_kind_candidates() -> Vec<CompletionCandidate> {
crate::registry::source_kinds()
.into_iter()
.map(CompletionCandidate::new)
.collect()
}
pub(crate) fn sink_kind_candidates() -> Vec<CompletionCandidate> {
crate::registry::sink_kinds()
.into_iter()
.map(CompletionCandidate::new)
.collect()
}
pub(crate) fn transform_candidates() -> Vec<CompletionCandidate> {
crate::transforms::transform_descriptions()
.into_iter()
.map(|(kind, desc)| CompletionCandidate::new(kind).help(Some(desc.into())))
.collect()
}
pub(crate) fn status_candidates() -> Vec<CompletionCandidate> {
["mandatory", "active", "available", "draft", "archived"]
.into_iter()
.map(CompletionCandidate::new)
.collect()
}
pub(crate) fn matrix_id_candidates() -> Vec<CompletionCandidate> {
let dir = match std::env::current_dir() {
Ok(d) => d,
Err(_) => return Vec::new(),
};
expanded_ids_from_dir(&dir)
.into_iter()
.map(CompletionCandidate::new)
.collect()
}
pub(crate) fn tag_candidates() -> Vec<CompletionCandidate> {
let dir = match std::env::current_dir() {
Ok(d) => d,
Err(_) => return Vec::new(),
};
expanded_tags_from_dir(&dir)
.into_iter()
.map(CompletionCandidate::new)
.collect()
}
fn expanded_ids_from_dir(dir: &std::path::Path) -> Vec<String> {
load_expanded_from_dir(dir)
.map(|nodes| nodes.into_iter().map(|n| n.id).collect())
.unwrap_or_default()
}
fn expanded_tags_from_dir(dir: &std::path::Path) -> Vec<String> {
let mut tags: Vec<String> = load_expanded_from_dir(dir)
.map(|nodes| nodes.into_iter().flat_map(|n| n.tags).collect())
.unwrap_or_default();
tags.sort();
tags.dedup();
tags
}
fn load_expanded_from_dir(dir: &std::path::Path) -> Option<Vec<crate::expand::ExpandedNode>> {
let path = crate::env_loader::discover_config_path(dir)?;
let text = std::fs::read_to_string(&path).ok()?;
let cfg = crate::config::PipelineConfig::from_text(&text, &path).ok()?;
crate::expand::expand(&cfg).ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn labels(cands: &[CompletionCandidate]) -> Vec<String> {
cands
.iter()
.map(|c| c.get_value().to_string_lossy().into_owned())
.collect()
}
#[test]
fn source_and_sink_kinds_match_registry() {
let src = labels(&source_kind_candidates());
assert_eq!(src, crate::registry::source_kinds());
let sink = labels(&sink_kind_candidates());
assert_eq!(sink, crate::registry::sink_kinds());
assert!(src.contains(&"rest".to_string()));
assert!(sink.contains(&"jsonl".to_string()));
}
#[test]
fn transform_candidates_cover_registry() {
let got = labels(&transform_candidates());
let expected: Vec<String> = crate::transforms::transform_descriptions()
.into_iter()
.map(|(k, _)| k.to_string())
.collect();
assert_eq!(got, expected);
}
#[test]
fn status_candidates_are_the_readiness_ladder() {
assert_eq!(
labels(&status_candidates()),
vec!["mandatory", "active", "available", "draft", "archived"]
);
}
#[test]
fn config_providers_are_empty_without_a_config() {
let dir = tempfile::tempdir().expect("tempdir");
assert!(expanded_ids_from_dir(dir.path()).is_empty());
assert!(expanded_tags_from_dir(dir.path()).is_empty());
}
#[test]
fn matrix_ids_and_tags_from_a_config_fixture() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = r#"
version: 1
name: demo
pipeline:
source: { type: rest, config: { url: "https://example.com" } }
sink: { type: jsonl, config: { path: out.jsonl } }
matrix:
- id: alpha
tags: [daily, us]
- id: beta
tags: [daily]
"#;
std::fs::write(dir.path().join("faucet.yaml"), cfg).expect("write cfg");
let ids = expanded_ids_from_dir(dir.path());
let tags = expanded_tags_from_dir(dir.path());
assert!(ids.contains(&"alpha".to_string()), "ids: {ids:?}");
assert!(ids.contains(&"beta".to_string()), "ids: {ids:?}");
assert_eq!(tags, vec!["daily", "us"]);
}
#[test]
fn static_generation_produces_a_nonempty_script() {
for shell in [
Shell::Bash,
Shell::Zsh,
Shell::Fish,
Shell::PowerShell,
Shell::Elvish,
] {
let mut buf: Vec<u8> = Vec::new();
write_completions(shell, &mut buf);
let script = String::from_utf8(buf).expect("utf8 script");
assert!(
script.contains("faucet"),
"{shell} script should mention the binary name"
);
assert!(!script.is_empty());
}
}
#[test]
fn run_emits_a_script_and_succeeds() {
run(Shell::Bash).expect("completions run should succeed");
}
#[test]
fn cwd_providers_are_best_effort_and_never_panic() {
let _ids = matrix_id_candidates();
let _tags = tag_candidates();
}
}