faucet_cli/commands/
conformance.rs1use crate::cli::ConformanceArgs;
5use crate::conformance::{Report, Tier, build_reports};
6use crate::error::{CliError, CliResult};
7
8pub async fn run(args: ConformanceArgs) -> CliResult<()> {
10 let kind_filter = match args.kind.as_deref() {
11 None => None,
12 Some("source") => Some("source"),
13 Some("sink") => Some("sink"),
14 Some(other) => {
15 return Err(CliError::Config(format!(
16 "--kind must be 'source' or 'sink' (got '{other}')"
17 )));
18 }
19 };
20
21 let mut reports: Vec<Report> = build_reports()
22 .into_iter()
23 .filter(|r| kind_filter.is_none_or(|k| r.kind == k))
24 .filter(|r| args.name.as_deref().is_none_or(|n| r.name == n))
25 .collect();
26 reports.sort_by(|a, b| b.score.cmp(&a.score).then_with(|| a.name.cmp(&b.name)));
27
28 if reports.is_empty() {
29 if let Some(name) = &args.name {
30 return Err(CliError::Config(format!(
31 "no connector named '{name}' is compiled into this binary (try `faucet list`)"
32 )));
33 }
34 return Err(CliError::Config(
35 "no connectors matched the filter".to_string(),
36 ));
37 }
38
39 if args.json {
40 let json = serde_json::to_string_pretty(&reports)
41 .map_err(|e| CliError::Config(format!("serialize conformance report: {e}")))?;
42 println!("{json}");
43 return Ok(());
44 }
45
46 if args.name.is_some() {
48 for r in &reports {
49 println!(
50 "{} {} ({}) — {} · {}/100",
51 r.tier.badge(),
52 r.name,
53 r.kind,
54 r.tier.label(),
55 r.score
56 );
57 for d in &r.dimensions {
58 println!(
59 " {} {:<24} (+{:>2}) {}",
60 if d.met { "✓" } else { "·" },
61 d.name,
62 d.points,
63 d.note
64 );
65 }
66 if !r.badges.is_empty() {
67 println!(" capabilities: {}", r.badges.join(", "));
68 }
69 }
70 return Ok(());
71 }
72
73 println!(
75 "faucet connector conformance ({} connectors)\n",
76 reports.len()
77 );
78 for r in &reports {
79 let caps = if r.badges.is_empty() {
80 String::new()
81 } else {
82 format!(" · {}", r.badges.join(", "))
83 };
84 println!(
85 "{} {:<15} {:<7} {:>3}/100 {}{}",
86 r.tier.badge(),
87 r.name,
88 r.kind,
89 r.score,
90 r.tier.label(),
91 caps
92 );
93 }
94 let stable = reports
95 .iter()
96 .filter(|r| matches!(r.tier, Tier::Stable))
97 .count();
98 println!("\n{}/{} connectors at Stable.", stable, reports.len());
99 Ok(())
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 fn args(name: Option<&str>, kind: Option<&str>, json: bool) -> ConformanceArgs {
107 ConformanceArgs {
108 name: name.map(str::to_string),
109 kind: kind.map(str::to_string),
110 json,
111 }
112 }
113
114 #[tokio::test]
115 async fn runs_over_all_connectors() {
116 assert!(run(args(None, None, false)).await.is_ok());
117 }
118
119 #[tokio::test]
120 async fn runs_json_with_source_filter() {
121 assert!(run(args(None, Some("source"), true)).await.is_ok());
122 }
123
124 #[tokio::test]
125 async fn single_connector_detailed_view() {
126 assert!(run(args(Some("postgres"), None, false)).await.is_ok());
128 }
129
130 #[tokio::test]
131 async fn unknown_connector_is_an_error() {
132 assert!(
133 run(args(Some("definitely-not-a-connector"), None, false))
134 .await
135 .is_err()
136 );
137 }
138
139 #[tokio::test]
140 async fn bad_kind_is_an_error() {
141 assert!(run(args(None, Some("neither"), false)).await.is_err());
142 }
143}