faucet_cli/commands/
list.rs1use 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
16pub 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
45fn 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
56fn 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
85fn 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
148fn 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 for key in ["sources", "sinks", "transforms", "state_stores"] {
176 assert!(v[key].is_array(), "section `{key}` missing or not an array");
177 }
178 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 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 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}