1#![allow(dead_code)] use auths_verifier::AssuranceLevel;
11use console::{Style, Term};
12use serde::Serialize;
13use std::io::IsTerminal;
14use std::sync::atomic::{AtomicBool, Ordering};
15
16static JSON_MODE: AtomicBool = AtomicBool::new(false);
17
18#[derive(Debug, Clone, Serialize)]
22pub struct JsonResponse<T: Serialize> {
23 pub success: bool,
25 pub command: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub data: Option<T>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub error: Option<String>,
33}
34
35impl<T: Serialize> JsonResponse<T> {
36 pub fn success(command: impl Into<String>, data: T) -> Self {
38 Self {
39 success: true,
40 command: command.into(),
41 data: Some(data),
42 error: None,
43 }
44 }
45
46 pub fn error(command: impl Into<String>, error: impl Into<String>) -> JsonResponse<()> {
48 JsonResponse {
49 success: false,
50 command: command.into(),
51 data: None,
52 error: Some(error.into()),
53 }
54 }
55
56 pub fn print(&self) -> Result<(), serde_json::Error> {
58 println!("{}", serde_json::to_string_pretty(self)?);
59 Ok(())
60 }
61}
62
63pub fn is_json_mode() -> bool {
65 JSON_MODE.load(Ordering::Relaxed)
66}
67
68pub struct Output {
70 term: Term,
71 colors_enabled: bool,
72 success_style: Style,
74 error_style: Style,
75 warn_style: Style,
76 info_style: Style,
77 bold_style: Style,
78 dim_style: Style,
79}
80
81impl Default for Output {
82 fn default() -> Self {
83 Self::new()
84 }
85}
86
87impl Output {
88 pub fn new() -> Self {
90 let term = Term::stderr();
91 let colors_enabled = Self::should_use_colors(&term);
92
93 Self {
94 term,
95 colors_enabled,
96 success_style: Style::new().green(),
97 error_style: Style::new().red(),
98 warn_style: Style::new().yellow(),
99 info_style: Style::new().cyan(),
100 bold_style: Style::new().bold(),
101 dim_style: Style::new().dim(),
102 }
103 }
104
105 pub fn stdout() -> Self {
107 let term = Term::stdout();
108 let colors_enabled = Self::should_use_colors(&term);
109
110 Self {
111 term,
112 colors_enabled,
113 success_style: Style::new().green(),
114 error_style: Style::new().red(),
115 warn_style: Style::new().yellow(),
116 info_style: Style::new().cyan(),
117 bold_style: Style::new().bold(),
118 dim_style: Style::new().dim(),
119 }
120 }
121
122 fn should_use_colors(term: &Term) -> bool {
124 if JSON_MODE.load(Ordering::Relaxed) {
125 return false;
126 }
127
128 #[allow(clippy::disallowed_methods)] if std::env::var("NO_COLOR").is_ok() {
131 return false;
132 }
133
134 if !term.is_term() {
136 return false;
137 }
138
139 if !std::io::stderr().is_terminal() {
141 return false;
142 }
143
144 true
145 }
146
147 pub fn success(&self, text: &str) -> String {
149 if self.colors_enabled {
150 self.success_style.apply_to(text).to_string()
151 } else {
152 text.to_string()
153 }
154 }
155
156 pub fn error(&self, text: &str) -> String {
158 if self.colors_enabled {
159 self.error_style.apply_to(text).to_string()
160 } else {
161 text.to_string()
162 }
163 }
164
165 pub fn warn(&self, text: &str) -> String {
167 if self.colors_enabled {
168 self.warn_style.apply_to(text).to_string()
169 } else {
170 text.to_string()
171 }
172 }
173
174 pub fn info(&self, text: &str) -> String {
176 if self.colors_enabled {
177 self.info_style.apply_to(text).to_string()
178 } else {
179 text.to_string()
180 }
181 }
182
183 pub fn bold(&self, text: &str) -> String {
185 if self.colors_enabled {
186 self.bold_style.apply_to(text).to_string()
187 } else {
188 text.to_string()
189 }
190 }
191
192 pub fn dim(&self, text: &str) -> String {
194 if self.colors_enabled {
195 self.dim_style.apply_to(text).to_string()
196 } else {
197 text.to_string()
198 }
199 }
200
201 pub fn print_success(&self, message: &str) {
203 let icon = if self.colors_enabled {
204 self.success_style.apply_to("\u{2713}").to_string()
205 } else {
206 "[OK]".to_string()
207 };
208 eprintln!("{} {}", icon, message);
209 }
210
211 pub fn print_error(&self, message: &str) {
213 let icon = if self.colors_enabled {
214 self.error_style.apply_to("\u{2717}").to_string()
215 } else {
216 "[ERROR]".to_string()
217 };
218 eprintln!("{} {}", icon, message);
219 }
220
221 pub fn print_warn(&self, message: &str) {
223 let icon = if self.colors_enabled {
224 self.warn_style.apply_to("!").to_string()
225 } else {
226 "[WARN]".to_string()
227 };
228 eprintln!("{} {}", icon, message);
229 }
230
231 pub fn print_info(&self, message: &str) {
233 let icon = if self.colors_enabled {
234 self.info_style.apply_to("i").to_string()
235 } else {
236 "[INFO]".to_string()
237 };
238 eprintln!("{} {}", icon, message);
239 }
240
241 pub fn print_heading(&self, text: &str) {
243 let styled = if self.colors_enabled {
244 self.bold_style.apply_to(text).to_string()
245 } else {
246 text.to_string()
247 };
248 eprintln!("{}", styled);
249 }
250
251 pub fn println(&self, text: &str) {
253 eprintln!("{}", text);
254 }
255
256 pub fn newline(&self) {
258 eprintln!();
259 }
260
261 pub fn key_value(&self, key: &str, value: &str) -> String {
263 if self.colors_enabled {
264 format!(
265 "{}: {}",
266 self.dim_style.apply_to(key),
267 self.info_style.apply_to(value)
268 )
269 } else {
270 format!("{}: {}", key, value)
271 }
272 }
273
274 pub fn assurance_badge(&self, level: AssuranceLevel) -> String {
284 let score = level.score();
285 let label = level.label();
286
287 if !self.colors_enabled {
288 return format!("[{}/4 {}]", score, label);
289 }
290
291 let filled = "\u{2588}".repeat(score as usize);
292 let empty = "\u{2591}".repeat(4 - score as usize);
293 let bar = format!("{}{}", filled, empty);
294
295 match level {
296 AssuranceLevel::Sovereign => {
297 format!(
298 "{} {}",
299 self.success_style.apply_to(&bar),
300 self.success_style.apply_to(label)
301 )
302 }
303 AssuranceLevel::Authenticated => {
304 format!(
305 "{} {}",
306 self.info_style.apply_to(&bar),
307 self.info_style.apply_to(label)
308 )
309 }
310 AssuranceLevel::TokenVerified => {
311 format!(
312 "{} {}",
313 self.warn_style.apply_to(&bar),
314 self.warn_style.apply_to(label)
315 )
316 }
317 AssuranceLevel::SelfAsserted | _ => {
318 format!(
319 "{} {}",
320 self.dim_style.apply_to(&bar),
321 self.dim_style.apply_to(label)
322 )
323 }
324 }
325 }
326
327 pub fn status(&self, passed: bool) -> &'static str {
329 if passed {
330 if self.colors_enabled {
331 "\u{2713}"
332 } else {
333 "[PASS]"
334 }
335 } else if self.colors_enabled {
336 "\u{2717}"
337 } else {
338 "[FAIL]"
339 }
340 }
341}
342
343pub fn set_json_mode(enabled: bool) {
347 JSON_MODE.store(enabled, Ordering::Relaxed);
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 impl Output {
355 fn new_without_colors() -> Self {
357 let term = Term::stderr();
358 Self {
359 term,
360 colors_enabled: false,
361 success_style: Style::new().green(),
362 error_style: Style::new().red(),
363 warn_style: Style::new().yellow(),
364 info_style: Style::new().cyan(),
365 bold_style: Style::new().bold(),
366 dim_style: Style::new().dim(),
367 }
368 }
369 }
370
371 #[test]
372 fn test_output_no_colors_in_test() {
373 let output = Output::new();
375 let success = output.success("test");
377 assert!(success.contains("test"));
378 }
379
380 #[test]
381 fn test_json_mode() {
382 let output = Output::new_without_colors();
384 let styled = output.success("test");
386 assert_eq!(styled, "test");
387 }
388
389 #[test]
390 fn test_key_value_format() {
391 let output = Output::new_without_colors();
393 let kv = output.key_value("name", "value");
394 assert_eq!(kv, "name: value");
395 }
396
397 #[test]
398 fn test_assurance_badge_no_colors() {
399 let output = Output::new_without_colors();
400 assert_eq!(
401 output.assurance_badge(AssuranceLevel::Sovereign),
402 "[4/4 Sovereign]"
403 );
404 assert_eq!(
405 output.assurance_badge(AssuranceLevel::Authenticated),
406 "[3/4 Authenticated]"
407 );
408 assert_eq!(
409 output.assurance_badge(AssuranceLevel::TokenVerified),
410 "[2/4 Token-Verified]"
411 );
412 assert_eq!(
413 output.assurance_badge(AssuranceLevel::SelfAsserted),
414 "[1/4 Self-Asserted]"
415 );
416 }
417}