Skip to main content

ready_set_rust/
readiness.rs

1//! Read-only readiness evaluation for Rust capabilities.
2
3use std::path::Path;
4
5use ready_set_sdk::{
6    CapabilityRelevance, CapabilityReport, CapabilityState, Error, NextAction, Result,
7};
8use toml_edit::{DocumentMut, Item};
9
10use crate::PROVIDER_ID;
11use crate::gitignore;
12use crate::manifest_edit;
13use crate::members;
14use crate::templates::{CLIPPY, RUST_TOOLCHAIN, RUSTFMT};
15use crate::workspace::Workspace;
16
17/// Evaluate one Rust capability.
18///
19/// # Errors
20///
21/// Forwards filesystem and TOML parsing errors.
22pub fn report(capability: &str, workspace: Option<&Workspace>) -> Result<CapabilityReport> {
23    let Some(workspace) = workspace else {
24        return base_report(
25            capability,
26            CapabilityState::Blocked,
27            "no Cargo manifest found",
28            None,
29        );
30    };
31
32    match capability {
33        "workspace" => workspace_report(workspace),
34        "toolchain" => Ok(template_file_report(
35            capability,
36            &workspace.root,
37            "rust-toolchain.toml",
38            RUST_TOOLCHAIN,
39        )?),
40        "formatting" => Ok(template_file_report(
41            capability,
42            &workspace.root,
43            "rustfmt.toml",
44            RUSTFMT,
45        )?),
46        "linting" => linting_report(workspace),
47        other => Err(Error::contract(format!(
48            "unknown Rust capability `{other}`"
49        ))),
50    }
51}
52
53fn workspace_report(workspace: &Workspace) -> Result<CapabilityReport> {
54    let raw = std::fs::read_to_string(&workspace.manifest_path)?;
55    let doc = parse_manifest(&raw, workspace)?;
56
57    let workspace_ready = workspace_table_ready(&doc, &members::discover(&workspace.root));
58    let gitignore_ready = std::fs::read_to_string(workspace.root.join(".gitignore"))
59        .ok()
60        .is_some_and(|current| gitignore::plan(Some(&current)).is_none());
61    let config_ready = workspace.root.join(".ready-set.toml").is_file();
62
63    if workspace_ready && gitignore_ready && config_ready {
64        base_report(
65            "workspace",
66            CapabilityState::Ready,
67            "workspace configuration is ready",
68            None,
69        )
70    } else {
71        base_report(
72            "workspace",
73            CapabilityState::Missing,
74            "workspace setup is missing",
75            Some(set_action(
76                "ready-set set workspace",
77                "Create the workspace configuration",
78            )),
79        )
80    }
81}
82
83fn template_file_report(
84    capability: &str,
85    root: &Path,
86    file_name: &'static str,
87    template: &str,
88) -> Result<CapabilityReport> {
89    let path = root.join(file_name);
90    let Ok(current) = std::fs::read_to_string(path) else {
91        return base_report(
92            capability,
93            CapabilityState::Missing,
94            format!("{file_name} is missing"),
95            Some(set_action(
96                format!("ready-set set {capability}"),
97                format!("Create the {capability} configuration"),
98            )),
99        );
100    };
101
102    if current == template {
103        base_report(
104            capability,
105            CapabilityState::Ready,
106            format!("{file_name} is ready"),
107            None,
108        )
109    } else {
110        base_report(
111            capability,
112            CapabilityState::Stale,
113            format!("{file_name} differs from template"),
114            Some(set_action(
115                format!("ready-set set --force {capability}"),
116                format!("Reconcile the {capability} configuration"),
117            )),
118        )
119    }
120}
121
122fn linting_report(workspace: &Workspace) -> Result<CapabilityReport> {
123    let clippy = std::fs::read_to_string(workspace.root.join("clippy.toml")).ok();
124    let raw = std::fs::read_to_string(&workspace.manifest_path)?;
125    let doc = parse_manifest(&raw, workspace)?;
126    let lints_match = manifest_edit::workspace_lints_match(&doc)?;
127
128    if clippy.is_none() || lints_match.is_none() {
129        return base_report(
130            "linting",
131            CapabilityState::Missing,
132            "linting configuration is missing",
133            Some(set_action(
134                "ready-set set linting",
135                "Create the linting configuration",
136            )),
137        );
138    }
139
140    if clippy.as_deref() == Some(CLIPPY) && lints_match == Some(true) {
141        base_report(
142            "linting",
143            CapabilityState::Ready,
144            "linting configuration is ready",
145            None,
146        )
147    } else {
148        base_report(
149            "linting",
150            CapabilityState::Stale,
151            "linting configuration differs from template",
152            Some(set_action(
153                "ready-set set --force linting",
154                "Reconcile the linting configuration",
155            )),
156        )
157    }
158}
159
160fn workspace_table_ready(doc: &DocumentMut, desired_members: &[String]) -> bool {
161    let Some(workspace) = doc.get("workspace").and_then(Item::as_table) else {
162        return false;
163    };
164    if workspace.get("resolver").and_then(Item::as_str) != Some("3") {
165        return false;
166    }
167    let Some(members) = workspace.get("members").and_then(Item::as_array) else {
168        return false;
169    };
170    desired_members.iter().all(|desired| {
171        members
172            .iter()
173            .any(|member| member.as_str() == Some(desired.as_str()))
174    })
175}
176
177fn base_report(
178    id: &str,
179    state: CapabilityState,
180    summary: impl Into<String>,
181    next_action: Option<NextAction>,
182) -> Result<CapabilityReport> {
183    Ok(CapabilityReport {
184        id: id.into(),
185        title: title_for(id)?.into(),
186        provider: PROVIDER_ID.into(),
187        state,
188        relevance: CapabilityRelevance::Required,
189        summary: summary.into(),
190        next_action,
191    })
192}
193
194fn title_for(id: &str) -> Result<&'static str> {
195    match id {
196        "workspace" => Ok("Workspace"),
197        "toolchain" => Ok("Toolchain"),
198        "formatting" => Ok("Formatting"),
199        "linting" => Ok("Linting"),
200        other => Err(Error::contract(format!(
201            "unknown Rust capability `{other}`"
202        ))),
203    }
204}
205
206fn set_action(command: impl Into<String>, description: impl Into<String>) -> NextAction {
207    NextAction {
208        command: command.into(),
209        description: description.into(),
210    }
211}
212
213fn parse_manifest(raw: &str, workspace: &Workspace) -> Result<DocumentMut> {
214    raw.parse().map_err(|err: toml_edit::TomlError| {
215        Error::TomlParse(format!("{}: {err}", workspace.manifest_path.display()))
216    })
217}