Skip to main content

faucet_cli/commands/
conformance.rs

1//! `faucet conformance` — score connectors against the faucet SDK contract and
2//! report a maturity tier + capabilities (#330).
3
4use crate::cli::ConformanceArgs;
5use crate::conformance::{Report, Tier, build_reports};
6use crate::error::{CliError, CliResult};
7
8/// Execute the `conformance` command.
9pub 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    // A single named connector → detailed scorecard.
47    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    // All connectors → one line each, highest score first.
74    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        // postgres is compiled into the default test build.
127        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}