1use 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
17pub 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(¤t)).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}