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 min_tier = match args.min_tier.as_deref() {
23 None => None,
24 Some(s) => Some(Tier::parse(s).ok_or_else(|| {
25 CliError::Config(format!(
26 "--min-tier must be one of stable/experimental/beta/draft (got '{s}')"
27 ))
28 })?),
29 };
30
31 let mut reports: Vec<Report> = build_reports()
32 .into_iter()
33 .filter(|r| kind_filter.is_none_or(|k| r.kind == k))
34 .filter(|r| args.name.as_deref().is_none_or(|n| r.name == n))
35 .collect();
36 reports.sort_by(|a, b| b.score.cmp(&a.score).then_with(|| a.name.cmp(&b.name)));
37
38 if reports.is_empty() {
39 if let Some(name) = &args.name {
40 return Err(CliError::Config(format!(
41 "no connector named '{name}' is compiled into this binary (try `faucet list`)"
42 )));
43 }
44 return Err(CliError::Config(
45 "no connectors matched the filter".to_string(),
46 ));
47 }
48
49 if args.json {
50 let json = serde_json::to_string_pretty(&reports)
51 .map_err(|e| CliError::Config(format!("serialize conformance report: {e}")))?;
52 println!("{json}");
53 } else if args.name.is_some() {
54 for r in &reports {
56 println!(
57 "{} {} ({}) — {} · {}/100",
58 r.tier.badge(),
59 r.name,
60 r.kind,
61 r.tier.label(),
62 r.score
63 );
64 for d in &r.dimensions {
65 println!(
66 " {} {:<24} (+{:>2}) {}",
67 if d.met { "✓" } else { "·" },
68 d.name,
69 d.points,
70 d.note
71 );
72 }
73 if !r.badges.is_empty() {
74 println!(" capabilities: {}", r.badges.join(", "));
75 }
76 println!(" badge: {}", r.tier.badge_url());
77 }
78 } else {
79 println!(
81 "faucet connector conformance ({} connectors)\n",
82 reports.len()
83 );
84 for r in &reports {
85 let caps = if r.badges.is_empty() {
86 String::new()
87 } else {
88 format!(" · {}", r.badges.join(", "))
89 };
90 println!(
91 "{} {:<15} {:<7} {:>3}/100 {}{}",
92 r.tier.badge(),
93 r.name,
94 r.kind,
95 r.score,
96 r.tier.label(),
97 caps
98 );
99 }
100 let stable = reports
101 .iter()
102 .filter(|r| matches!(r.tier, Tier::Stable))
103 .count();
104 println!("\n{}/{} connectors at Stable.", stable, reports.len());
105 }
106
107 if let Some(min) = min_tier {
109 let below: Vec<&Report> = reports
110 .iter()
111 .filter(|r| r.tier.rank() < min.rank())
112 .collect();
113 if !below.is_empty() {
114 let names: Vec<String> = below
115 .iter()
116 .map(|r| format!("{} {} ({})", r.name, r.kind, r.tier.label()))
117 .collect();
118 return Err(CliError::Config(format!(
119 "{} connector(s) below the required `{}` tier: {}",
120 below.len(),
121 min.label(),
122 names.join(", ")
123 )));
124 }
125 }
126
127 Ok(())
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn args(name: Option<&str>, kind: Option<&str>, json: bool) -> ConformanceArgs {
135 ConformanceArgs {
136 name: name.map(str::to_string),
137 kind: kind.map(str::to_string),
138 all: false,
139 json,
140 min_tier: None,
141 }
142 }
143
144 #[tokio::test]
145 async fn runs_over_all_connectors() {
146 assert!(run(args(None, None, false)).await.is_ok());
147 }
148
149 #[tokio::test]
150 async fn runs_json_with_source_filter() {
151 assert!(run(args(None, Some("source"), true)).await.is_ok());
152 }
153
154 #[tokio::test]
155 async fn single_connector_detailed_view() {
156 assert!(run(args(Some("postgres"), None, false)).await.is_ok());
158 }
159
160 #[tokio::test]
161 async fn unknown_connector_is_an_error() {
162 assert!(
163 run(args(Some("definitely-not-a-connector"), None, false))
164 .await
165 .is_err()
166 );
167 }
168
169 #[tokio::test]
170 async fn bad_kind_is_an_error() {
171 assert!(run(args(None, Some("neither"), false)).await.is_err());
172 }
173
174 #[tokio::test]
175 async fn min_tier_stable_passes_for_builtins() {
176 let mut a = args(None, None, false);
178 a.min_tier = Some("stable".into());
179 assert!(run(a).await.is_ok());
180 }
181
182 #[tokio::test]
183 async fn bad_min_tier_is_an_error() {
184 let mut a = args(None, None, true);
185 a.min_tier = Some("platinum".into());
186 assert!(run(a).await.is_err());
187 }
188}