1use 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
22pub 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 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
103fn 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
130pub 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}