1use crate::doctor::{Capability, CapabilityReport, Level, Report};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum Format {
13 #[default]
14 Human,
15 Short,
16 Json,
17}
18
19fn level_word(cap: &CapabilityReport) -> &'static str {
23 match cap.level {
24 Level::Ok => "ok",
25 Level::Warn => "warn",
26 Level::Fail if cap.optional => "note",
27 Level::Fail => "fail",
28 }
29}
30
31pub fn render(report: &Report, format: Format) -> String {
32 match format {
33 Format::Human => human(report),
34 Format::Short => short(report),
35 Format::Json => json(report),
36 }
37}
38
39fn short(report: &Report) -> String {
40 let mut out = String::new();
41 for cap in &report.capabilities {
42 let word = level_word(cap);
43 let remedy = cap
45 .rows
46 .iter()
47 .find(|r| r.level != Level::Ok)
48 .and_then(|r| r.remedy.as_deref());
49 match remedy {
50 Some(r) => out.push_str(&format!("{}: {word} ({r})\n", cap.capability.token())),
51 None => out.push_str(&format!("{}: {word}\n", cap.capability.token())),
52 }
53 }
54 out
55}
56
57fn human(report: &Report) -> String {
58 let mut out = String::new();
59 let header = if report.is_all_ok() {
60 "bynk doctor — your environment is ready".to_string()
61 } else {
62 "bynk doctor — environment report".to_string()
63 };
64 out.push_str(&header);
65 out.push('\n');
66 out.push_str(&format!("driver: bynk {}\n", report.driver_version));
67 match (report.compiler.origin, report.compiler.path.as_deref()) {
70 (Some(crate::compiler::Origin::Override), Some(path)) => {
71 out.push_str(&format!(
72 "compiler: bynkc at {} (override)\n",
73 path.display()
74 ));
75 }
76 _ => out.push_str("compiler: in-process\n"),
77 }
78 out.push('\n');
79
80 for cap in &report.capabilities {
81 let mark = match cap.level {
82 Level::Ok => "✓",
83 Level::Warn => "!",
84 Level::Fail if cap.optional => "·",
85 Level::Fail => "✗",
86 };
87 out.push_str(&format!(
88 "{mark} {} [{}]{}\n",
89 cap.capability.token(),
90 level_word(cap),
91 if cap.optional { " (optional)" } else { "" }
92 ));
93 for row in &cap.rows {
94 out.push_str(&format!(" {} — {}\n", row.label, row.detail));
95 if let Some(remedy) = &row.remedy {
96 out.push_str(&format!(" ↳ fix: {remedy}\n"));
97 }
98 }
99 }
100 out
101}
102
103#[derive(serde::Serialize)]
109struct JsonReport<'a> {
110 driver: &'a str,
111 compiler: JsonCompiler,
112 all_ok: bool,
113 capabilities: Vec<JsonCap<'a>>,
114}
115
116#[derive(serde::Serialize)]
117struct JsonCompiler {
118 resolved: bool,
119 path: Option<String>,
120 version: Option<String>,
121 origin: Option<&'static str>,
122 skew: Option<&'static str>,
123}
124
125#[derive(serde::Serialize)]
126struct JsonCap<'a> {
127 capability: &'static str,
128 optional: bool,
129 level: &'static str,
130 rows: Vec<JsonRow<'a>>,
131}
132
133#[derive(serde::Serialize)]
134struct JsonRow<'a> {
135 label: &'a str,
136 level: &'static str,
137 detail: &'a str,
138 remedy: Option<&'a str>,
139}
140
141fn json(report: &Report) -> String {
142 let compiler = &report.compiler;
143 let value = JsonReport {
144 driver: &report.driver_version,
145 compiler: JsonCompiler {
146 resolved: compiler.is_resolved(),
147 path: compiler.path.as_ref().map(|p| p.display().to_string()),
148 version: compiler.version.map(|v| v.to_string()),
149 origin: compiler.origin.map(|o| o.token()),
150 skew: compiler.skew.map(|s| s.token()),
151 },
152 all_ok: report.is_all_ok(),
153 capabilities: report
154 .capabilities
155 .iter()
156 .map(|cap| JsonCap {
157 capability: cap.capability.token(),
158 optional: cap.optional,
159 level: level_word(cap),
160 rows: cap
161 .rows
162 .iter()
163 .map(|r| JsonRow {
164 label: &r.label,
165 level: level_token(r.level),
166 detail: &r.detail,
167 remedy: r.remedy.as_deref(),
168 })
169 .collect(),
170 })
171 .collect(),
172 };
173 let mut s = serde_json::to_string_pretty(&value).expect("Report serialises");
175 s.push('\n');
176 s
177}
178
179fn level_token(level: Level) -> &'static str {
180 match level {
181 Level::Ok => "ok",
182 Level::Warn => "warn",
183 Level::Fail => "fail",
184 }
185}
186
187pub fn parse_capability(token: &str) -> Option<Capability> {
189 match token {
190 "compile" => Some(Capability::Compile),
191 "test" => Some(Capability::Test),
192 "deploy" => Some(Capability::Deploy),
193 "editor" => Some(Capability::Editor),
194 "build" => Some(Capability::BuildFromSource),
195 _ => None,
196 }
197}