Skip to main content

sva_core/
output.rs

1// Concern: builds the JSON envelope every answer is written in, success or failure | Non-concern: the data objects it carries (sva-cli's output.rs) | IO: (data or a code, diagnostics) -> JSON
2
3use crate::json::{NONE, escape, list, meta};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum Severity {
7    Error,
8    Warning,
9    Advice,
10}
11
12impl Severity {
13    fn as_str(self) -> &'static str {
14        match self {
15            Severity::Error => "error",
16            Severity::Warning => "warning",
17            Severity::Advice => "advice",
18        }
19    }
20}
21
22/// One located finding. No `fix` field: an edit needs a span, and no written literal carries one.
23pub struct Diagnostic {
24    pub code: String,
25    pub severity: Severity,
26    pub message: String,
27    pub file: Option<String>,
28    pub line: Option<usize>,
29    pub span: Option<(usize, usize)>,
30    pub help: Option<String>,
31}
32
33impl Diagnostic {
34    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Diagnostic {
35        Diagnostic {
36            code: code.into(),
37            severity: Severity::Error,
38            message: message.into(),
39            file: None,
40            line: None,
41            span: None,
42            help: None,
43        }
44    }
45
46    pub fn at(mut self, file: Option<String>, span: Option<(usize, usize)>) -> Diagnostic {
47        self.file = file;
48        self.span = span;
49        self
50    }
51
52    pub fn helped(mut self, help: impl Into<String>) -> Diagnostic {
53        self.help = Some(help.into());
54        self
55    }
56
57    pub fn on_line(mut self, line: Option<usize>) -> Diagnostic {
58        self.line = line;
59        self
60    }
61
62    pub fn with_severity(mut self, severity: Severity) -> Diagnostic {
63        self.severity = severity;
64        self
65    }
66}
67
68pub fn diagnostics_json(diagnostics: &[Diagnostic]) -> String {
69    list(diagnostics, diagnostic_json)
70}
71
72fn diagnostic_json(d: &Diagnostic) -> String {
73    let span = d.span.map_or_else(
74        || NONE.to_string(),
75        |(o, l)| format!("{{ \"offset\": {o}, \"length\": {l} }}"),
76    );
77    // A check knowing only the line points at its first column, no guess.
78    let start = d.line.map_or_else(
79        || NONE.to_string(),
80        |l| format!("{{ \"line\": {l}, \"column\": 1 }}"),
81    );
82    let location = d.file.as_ref().map_or_else(
83        || NONE.to_string(),
84        |f| {
85            format!(
86                "{{ \"file\": \"{}\", \"span\": {span}, \"start\": {start}, \"end\": {NONE} }}",
87                escape(f)
88            )
89        },
90    );
91    let help = d
92        .help
93        .as_ref()
94        .map_or_else(|| NONE.to_string(), |h| format!("\"{}\"", escape(h)));
95    format!(
96        "{{ \"code\": \"{}\", \"severity\": \"{}\", \"message\": \"{}\", \"location\": {location}, \"help\": {help} }}",
97        escape(&d.code),
98        d.severity.as_str(),
99        escape(&d.message)
100    )
101}
102
103/// `details` is free-form in the standard: the count and codes an agent branches on, so the
104/// located array is read once, at `data.diagnostics`.
105fn details_json(diagnostics: &[Diagnostic]) -> String {
106    let mut codes: Vec<&str> = Vec::new();
107    for d in diagnostics {
108        if !codes.contains(&d.code.as_str()) {
109            codes.push(&d.code);
110        }
111    }
112    format!(
113        "{{ \"count\": {}, \"codes\": {} }}",
114        diagnostics.len(),
115        crate::json::strings(&codes)
116    )
117}
118
119pub fn success_envelope(data: &str, diagnostics: &[Diagnostic]) -> String {
120    let fields = data
121        .strip_suffix('}')
122        .expect("every data builder answers one JSON object");
123    format!(
124        "{{\n  \"status\": \"success\",\n  \"data\": {fields},\n  \"diagnostics\": {}\n  }},\n  \"meta\": {}\n}}",
125        diagnostics_json(diagnostics),
126        meta()
127    )
128}
129
130/// `data.diagnostics` is always present, empty where the failure has no subject to locate.
131pub fn error_envelope(code: &str, message: &str, diagnostics: &[Diagnostic]) -> String {
132    format!(
133        "{{\n  \"status\": \"error\",\n  \"error\": {{ \"code\": \"{}\", \"message\": \"{}\", \"details\": {} }},\n  \"data\": {{ \"diagnostics\": {} }},\n  \"meta\": {}\n}}",
134        escape(code),
135        escape(message),
136        details_json(diagnostics),
137        diagnostics_json(diagnostics),
138        meta()
139    )
140}