Skip to main content

ready_set/builtins/
ready.rs

1//! `ready-set ready`: read-only capability diagnosis.
2
3use std::ffi::OsString;
4
5use ready_set_sdk::{
6    CapabilityRelevance, CapabilityReport, CapabilityState, CapabilityVerb, ExitCode, OutputMode,
7};
8
9use crate::capabilities::{
10    CapabilityRegistry, RegisteredCapability, render_human_matrix, render_json_matrix,
11};
12use crate::env::EnvContract;
13use crate::lifecycle::{ReadyInvocation, invoke_ready};
14
15/// Built-in entry point. The dispatcher routes here for `ready-set ready`.
16pub fn run(args: &[OsString], contract: &EnvContract) -> ExitCode {
17    let selected = match parse_selected(args) {
18        Ok(selected) => selected,
19        Err(code) => return code,
20    };
21    let cwd = match std::env::current_dir() {
22        Ok(cwd) => cwd,
23        Err(err) => {
24            eprintln!("ready-set ready: cannot read current directory: {err}");
25            return ExitCode::SystemError;
26        },
27    };
28
29    let registry = match CapabilityRegistry::discover(&cwd) {
30        Ok(registry) => registry,
31        Err(err) => {
32            eprintln!("ready-set ready: {err}");
33            return (&err).into();
34        },
35    };
36
37    let capabilities = match select_capabilities(&registry, selected.as_deref()) {
38        Ok(capabilities) => capabilities,
39        Err(code) => return code,
40    };
41
42    let mut reports = Vec::new();
43    for capability in capabilities {
44        match build_report(capability, contract) {
45            Ok(report) => reports.push(report),
46            Err(err) => {
47                eprintln!("ready-set ready: {err}");
48                return ExitCode::SystemError;
49            },
50        }
51    }
52
53    match contract.output {
54        OutputMode::Json => match render_json_matrix(&reports) {
55            Ok(json) => println!("{json}"),
56            Err(err) => {
57                eprintln!("ready-set ready: failed to serialize JSON report: {err}");
58                return ExitCode::SystemError;
59            },
60        },
61        OutputMode::Human => print!("{}", render_human_matrix(&reports)),
62    }
63
64    if reports.iter().any(required_report_failed) {
65        ExitCode::UserError
66    } else {
67        ExitCode::Ok
68    }
69}
70
71fn select_capabilities<'a>(
72    registry: &'a CapabilityRegistry,
73    selected: Option<&str>,
74) -> Result<Vec<&'a RegisteredCapability>, ExitCode> {
75    if let Some(id) = selected {
76        let Some(capability) = registry
77            .capabilities()
78            .iter()
79            .find(|capability| capability.id.as_str() == id)
80        else {
81            eprintln!("ready-set ready: unknown capability `{id}`");
82            return Err(ExitCode::UserError);
83        };
84        if !capability.verbs.contains(&CapabilityVerb::Ready) {
85            eprintln!("ready-set ready: capability `{id}` does not support ready");
86            return Err(ExitCode::UserError);
87        }
88        return Ok(vec![capability]);
89    }
90
91    Ok(registry.capabilities().iter().collect())
92}
93
94fn parse_selected(args: &[OsString]) -> Result<Option<String>, ExitCode> {
95    match args {
96        [] => Ok(None),
97        [capability] => Ok(Some(capability.to_string_lossy().into_owned())),
98        _ => {
99            eprintln!("ready-set ready: expected at most one capability");
100            Err(ExitCode::UserError)
101        },
102    }
103}
104
105fn build_report(
106    capability: &RegisteredCapability,
107    contract: &EnvContract,
108) -> std::io::Result<CapabilityReport> {
109    if capability.relevance == CapabilityRelevance::NotNeeded {
110        return Ok(placeholder_report(
111            capability,
112            CapabilityState::NotNeeded,
113            "capability marked not needed",
114        ));
115    }
116    if !capability.verbs.contains(&CapabilityVerb::Ready) {
117        return Ok(placeholder_report(
118            capability,
119            unavailable_state(capability.relevance),
120            "capability does not support ready",
121        ));
122    }
123
124    match invoke_ready(&capability.provider, capability.id.as_str(), contract)? {
125        ReadyInvocation::Report(report) => Ok(apply_registry_metadata(capability, report)),
126        ReadyInvocation::ProviderUnavailable { summary }
127        | ReadyInvocation::ProviderFailed { summary } => Ok(placeholder_report(
128            capability,
129            unavailable_state(capability.relevance),
130            summary,
131        )),
132    }
133}
134
135fn apply_registry_metadata(
136    capability: &RegisteredCapability,
137    mut report: CapabilityReport,
138) -> CapabilityReport {
139    report.id = capability.id.clone();
140    capability.title.clone_into(&mut report.title);
141    report.provider = capability.provider.clone();
142    report.relevance = capability.relevance;
143    if capability.relevance == CapabilityRelevance::Optional
144        && !matches!(
145            report.state,
146            CapabilityState::Ready | CapabilityState::NotNeeded
147        )
148    {
149        report.state = CapabilityState::Optional;
150    }
151    report
152}
153
154fn placeholder_report(
155    capability: &RegisteredCapability,
156    state: CapabilityState,
157    summary: impl Into<String>,
158) -> CapabilityReport {
159    CapabilityReport {
160        id: capability.id.clone(),
161        title: capability.title.clone(),
162        provider: capability.provider.clone(),
163        state,
164        relevance: capability.relevance,
165        summary: summary.into(),
166        next_action: None,
167    }
168}
169
170const fn unavailable_state(relevance: CapabilityRelevance) -> CapabilityState {
171    match relevance {
172        CapabilityRelevance::Required => CapabilityState::Blocked,
173        CapabilityRelevance::Optional => CapabilityState::Optional,
174        CapabilityRelevance::NotNeeded => CapabilityState::NotNeeded,
175    }
176}
177
178fn required_report_failed(report: &CapabilityReport) -> bool {
179    report.relevance == CapabilityRelevance::Required
180        && !matches!(
181            report.state,
182            CapabilityState::Ready | CapabilityState::Optional | CapabilityState::NotNeeded
183        )
184}
185
186#[cfg(test)]
187mod tests {
188    use ready_set_sdk::{CapabilityId, CapabilityVerb, ProviderId};
189
190    use super::*;
191
192    fn capability(relevance: CapabilityRelevance) -> RegisteredCapability {
193        RegisteredCapability {
194            id: CapabilityId::from("formatting"),
195            title: "Formatting".into(),
196            provider: ProviderId::from("missing-provider"),
197            verbs: vec![CapabilityVerb::Ready],
198            relevance,
199        }
200    }
201
202    fn manifest(
203        capabilities: Vec<ready_set_sdk::CapabilityDescriptor>,
204    ) -> ready_set_sdk::manifest::Manifest {
205        ready_set_sdk::manifest::Manifest {
206            description: "test".into(),
207            version: "0.1.0".parse().unwrap(),
208            stability: ready_set_sdk::describe::Stability::Stable,
209            min_dispatcher_version: "0.1.0".parse().unwrap(),
210            platforms: vec![ready_set_sdk::describe::Platform::current().unwrap()],
211            requires_cargo_workspace: false,
212            capabilities,
213        }
214    }
215
216    fn descriptor(id: &str, verbs: Vec<CapabilityVerb>) -> ready_set_sdk::CapabilityDescriptor {
217        ready_set_sdk::CapabilityDescriptor {
218            id: id.into(),
219            title: id.into(),
220            provider: "provider".into(),
221            verbs,
222            default_relevance: CapabilityRelevance::Required,
223        }
224    }
225
226    #[test]
227    fn required_unavailable_provider_fails_readiness() {
228        let report = placeholder_report(
229            &capability(CapabilityRelevance::Required),
230            unavailable_state(CapabilityRelevance::Required),
231            "provider missing",
232        );
233
234        assert_eq!(report.state, CapabilityState::Blocked);
235        assert!(required_report_failed(&report));
236    }
237
238    #[test]
239    fn optional_unavailable_provider_does_not_fail_readiness() {
240        let report = placeholder_report(
241            &capability(CapabilityRelevance::Optional),
242            unavailable_state(CapabilityRelevance::Optional),
243            "provider missing",
244        );
245
246        assert_eq!(report.state, CapabilityState::Optional);
247        assert!(!required_report_failed(&report));
248    }
249
250    #[test]
251    fn not_needed_short_circuits_provider_lookup() {
252        let report = placeholder_report(
253            &capability(CapabilityRelevance::NotNeeded),
254            CapabilityState::NotNeeded,
255            "capability marked not needed",
256        );
257
258        assert_eq!(report.state, CapabilityState::NotNeeded);
259        assert!(!required_report_failed(&report));
260    }
261
262    #[test]
263    fn explicit_capability_must_support_ready() {
264        let registry = CapabilityRegistry::from_parts(
265            None,
266            [manifest(vec![descriptor(
267                "setup",
268                vec![CapabilityVerb::Set],
269            )])],
270        );
271
272        assert!(select_capabilities(&registry, Some("setup")).is_err());
273    }
274
275    #[test]
276    fn readyless_capability_reports_blocked_without_invocation() {
277        let capability = RegisteredCapability {
278            id: CapabilityId::from("setup"),
279            title: "Setup".into(),
280            provider: ProviderId::from("missing-provider"),
281            verbs: vec![CapabilityVerb::Set],
282            relevance: CapabilityRelevance::Required,
283        };
284
285        let contract = EnvContract {
286            dispatcher_version: semver::Version::new(0, 1, 0),
287            project_root: None,
288            config_path: None,
289            output: OutputMode::Human,
290            log: ready_set_sdk::context::LogLevel::Normal,
291            color: ready_set_sdk::context::ColorMode::Auto,
292        };
293        let report = build_report(&capability, &contract).unwrap();
294
295        assert_eq!(report.state, CapabilityState::Blocked);
296        assert_eq!(report.summary, "capability does not support ready");
297    }
298}