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