Skip to main content

ready_set_rust/
workflow.rs

1//! Workflow execution for Rust capabilities.
2
3use std::process::{Command, ExitStatus, Stdio};
4
5use ready_set_sdk::{
6    CapabilityAction, CapabilityActionKind, CapabilityRunReport, CapabilityVerb, Context, Error,
7    ExitCode, Output, OutputMode, Result, RunStatus,
8};
9
10use crate::workspace::Workspace;
11
12/// Execute `go` for one Rust capability.
13///
14/// # Errors
15///
16/// Returns SDK errors when `cargo` cannot be spawned or JSON output cannot be
17/// written.
18pub fn go_capability(capability: &str, workspace: &Workspace, ctx: &Context) -> Result<ExitCode> {
19    let Some(args) = cargo_args(capability) else {
20        return Ok(ExitCode::UserError);
21    };
22    let capture_json = matches!(ctx.output_mode(), OutputMode::Json);
23
24    if capture_json {
25        let output = Command::new("cargo")
26            .args(args)
27            .current_dir(&workspace.root)
28            .stdin(Stdio::null())
29            .stdout(Stdio::piped())
30            .stderr(Stdio::piped())
31            .output()
32            .map_err(map_cargo_spawn_error)?;
33        let report = report_for_status(capability, args, output.status);
34        let exit_code = workflow_exit_code(output.status);
35        let mut out = Output::for_context(ctx, std::io::stdout());
36        out.json(&report)?;
37        Ok(exit_code)
38    } else {
39        let status = Command::new("cargo")
40            .args(args)
41            .current_dir(&workspace.root)
42            .status()
43            .map_err(map_cargo_spawn_error)?;
44        Ok(workflow_exit_code(status))
45    }
46}
47
48fn cargo_args(capability: &str) -> Option<&'static [&'static str]> {
49    match capability {
50        "formatting" => Some(&["fmt", "--check"]),
51        "linting" => Some(&["clippy", "--workspace", "--all-targets"]),
52        _ => None,
53    }
54}
55
56fn report_for_status(capability: &str, args: &[&str], status: ExitStatus) -> CapabilityRunReport {
57    let command = command_label(args);
58    let (run_status, action) = if status.success() {
59        (
60            RunStatus::Ok,
61            CapabilityAction {
62                kind: CapabilityActionKind::Run,
63                summary: format!("{command} completed"),
64                path: None,
65            },
66        )
67    } else {
68        (
69            RunStatus::Failed,
70            CapabilityAction {
71                kind: CapabilityActionKind::Error,
72                summary: format!("{command} {}", status_summary(status)),
73                path: None,
74            },
75        )
76    };
77
78    CapabilityRunReport {
79        id: capability.into(),
80        verb: CapabilityVerb::Go,
81        status: run_status,
82        actions: vec![action],
83    }
84}
85
86fn command_label(args: &[&str]) -> String {
87    format!("cargo {}", args.join(" "))
88}
89
90fn status_summary(status: ExitStatus) -> String {
91    status.code().map_or_else(
92        || "terminated before reporting an exit code".into(),
93        |code| format!("exited with code {code}"),
94    )
95}
96
97fn workflow_exit_code(status: ExitStatus) -> ExitCode {
98    if status.success() {
99        ExitCode::Ok
100    } else if status.code().is_some() {
101        ExitCode::UserError
102    } else {
103        ExitCode::SystemError
104    }
105}
106
107fn map_cargo_spawn_error(err: std::io::Error) -> Error {
108    if err.kind() == std::io::ErrorKind::NotFound {
109        Error::MissingDependency {
110            name: "cargo".into(),
111            hint: Some("install Rust and Cargo, or ensure cargo is on PATH".into()),
112        }
113    } else {
114        Error::Io(err)
115    }
116}