Skip to main content

faucet_cli/commands/
list.rs

1//! `faucet list` — show every compiled-in source, sink, transform, and
2//! state-store backend so users can discover what their binary supports.
3//! `--available` instead lists every connector in the registry index (#208),
4//! marking which are compiled into this binary.
5
6use crate::cli::ListArgs;
7use crate::conformance::tier_for;
8use crate::error::CliResult;
9use crate::registry::{sink_descriptions, sink_exists, source_descriptions, source_exists};
10use crate::registry_index::RegistryIndex;
11use crate::state::available_state_kinds;
12#[cfg(feature = "quality")]
13use crate::transforms::quality_descriptions;
14use crate::transforms::transform_descriptions;
15
16/// Execute the `list` subcommand.
17pub async fn run(args: ListArgs) -> CliResult<()> {
18    if args.available {
19        return list_available(args);
20    }
21    if args.json {
22        return run_json();
23    }
24    println!("Sources:");
25    print_connectors(&source_descriptions(), true);
26    println!();
27    println!("Sinks:");
28    print_connectors(&sink_descriptions(), false);
29    println!();
30    println!("Transforms:");
31    print_two_column(&transform_descriptions());
32    println!();
33    #[cfg(feature = "quality")]
34    {
35        println!("Quality checks:");
36        print_two_column(&quality_descriptions());
37        println!();
38    }
39    println!("State stores: {}", available_state_kinds().join(", "));
40    #[cfg(feature = "schedule")]
41    println!("Scheduler:    compiled in (run `faucet schedule --help`, `faucet schema schedule`)");
42    Ok(())
43}
44
45/// `faucet list --json` — the compiled-in connectors/transforms/state stores as
46/// a single JSON object, so tooling and CI can consume the listing directly.
47fn run_json() -> CliResult<()> {
48    let out = build_list_json();
49    println!(
50        "{}",
51        serde_json::to_string_pretty(&out).unwrap_or_else(|_| out.to_string())
52    );
53    Ok(())
54}
55
56/// Pure builder for the `faucet list --json` document (no I/O), so its shape can
57/// be unit-tested.
58fn build_list_json() -> serde_json::Value {
59    let to_entries = |entries: &[(&'static str, &'static str)], is_source: Option<bool>| {
60        entries
61            .iter()
62            .map(|(name, desc)| {
63                let mut obj = serde_json::json!({ "name": name, "description": desc });
64                if let Some(is_source) = is_source {
65                    obj["tier"] = serde_json::json!(tier_for(name, is_source).label());
66                }
67                obj
68            })
69            .collect::<Vec<_>>()
70    };
71    #[allow(unused_mut)]
72    let mut out = serde_json::json!({
73        "sources": to_entries(&source_descriptions(), Some(true)),
74        "sinks": to_entries(&sink_descriptions(), Some(false)),
75        "transforms": to_entries(&transform_descriptions(), None),
76        "state_stores": available_state_kinds(),
77    });
78    #[cfg(feature = "quality")]
79    {
80        out["quality_checks"] = serde_json::json!(to_entries(&quality_descriptions(), None));
81    }
82    out
83}
84
85/// `faucet list --available` — every connector in the registry index, with a
86/// marker for those already compiled into this binary.
87fn list_available(args: ListArgs) -> CliResult<()> {
88    let idx = RegistryIndex::load(args.index.as_deref())?;
89    let mut connectors: Vec<_> = idx.connectors.iter().collect();
90    connectors.sort_by(|a, b| {
91        (a.kind.as_str(), a.name.as_str()).cmp(&(b.kind.as_str(), b.name.as_str()))
92    });
93    let compiled_for = |c: &crate::registry_index::ConnectorEntry| match c.kind.as_str() {
94        "source" => source_exists(&c.name),
95        "sink" => sink_exists(&c.name),
96        _ => false,
97    };
98    if args.json {
99        let rows: Vec<_> = connectors
100            .iter()
101            .map(|c| {
102                serde_json::json!({
103                    "kind": c.kind,
104                    "name": c.name,
105                    "description": c.description,
106                    "tier": c.tier,
107                    "verified": c.verified,
108                    "compiled": compiled_for(c),
109                })
110            })
111            .collect();
112        let out = serde_json::json!({ "connectors": rows });
113        println!(
114            "{}",
115            serde_json::to_string_pretty(&out).unwrap_or_else(|_| out.to_string())
116        );
117        return Ok(());
118    }
119    println!(
120        "Registry connectors ({} total). ● = compiled into this binary, ○ = available via `faucet install`:\n",
121        connectors.len()
122    );
123    for c in connectors {
124        let mark = if compiled_for(c) { '●' } else { '○' };
125        let badge = if c.verified { "verified" } else { "community" };
126        let tier = c.tier.as_deref().unwrap_or("-");
127        println!(
128            "  {mark} {kind:<6} {name:<14} {tier:<12} {desc}  [{badge}]",
129            kind = c.kind,
130            name = c.name,
131            desc = c.description
132        );
133    }
134    Ok(())
135}
136
137fn print_two_column(entries: &[(&'static str, &'static str)]) {
138    if entries.is_empty() {
139        println!("  (none — rebuild faucet-cli with the relevant features enabled)");
140        return;
141    }
142    let width = entries.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
143    for (name, desc) in entries {
144        println!("  {name:<width$}  {desc}", width = width);
145    }
146}
147
148/// Like [`print_two_column`] but prefixes each connector with its conformance
149/// maturity tier badge (`faucet conformance` for the full scorecards).
150fn print_connectors(entries: &[(&'static str, &'static str)], is_source: bool) {
151    if entries.is_empty() {
152        println!("  (none — rebuild faucet-cli with the relevant features enabled)");
153        return;
154    }
155    let width = entries.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
156    for (name, desc) in entries {
157        let tier = tier_for(name, is_source);
158        println!(
159            "  {badge} {name:<width$}  {tier:<12} {desc}",
160            badge = tier.badge(),
161            tier = tier.label(),
162            width = width,
163        );
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::build_list_json;
170
171    #[test]
172    fn list_json_has_expected_sections_and_builtins() {
173        let v = build_list_json();
174        // The four documented sections are present and are arrays.
175        for key in ["sources", "sinks", "transforms", "state_stores"] {
176            assert!(v[key].is_array(), "section `{key}` missing or not an array");
177        }
178        // Each connector entry carries name/description; sources/sinks add tier.
179        let names = |key: &str| -> Vec<String> {
180            v[key]
181                .as_array()
182                .unwrap()
183                .iter()
184                .map(|e| e["name"].as_str().unwrap_or_default().to_string())
185                .collect()
186        };
187        // Default CLI build compiles in the `rest` source and `jsonl` sink.
188        assert!(
189            names("sources").iter().any(|n| n == "rest"),
190            "sources: {:?}",
191            names("sources")
192        );
193        assert!(
194            names("sinks").iter().any(|n| n == "jsonl"),
195            "sinks: {:?}",
196            names("sinks")
197        );
198        assert!(
199            v["sources"][0].get("tier").is_some(),
200            "source entries should carry a tier"
201        );
202        // State stores always include the built-in memory + file backends.
203        let stores: Vec<String> = v["state_stores"]
204            .as_array()
205            .unwrap()
206            .iter()
207            .map(|s| s.as_str().unwrap_or_default().to_string())
208            .collect();
209        assert!(
210            stores.iter().any(|s| s == "memory"),
211            "state_stores: {stores:?}"
212        );
213    }
214}