1use 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
20pub struct Diagnostic {
22 pub code: String,
23 pub severity: Severity,
24 pub message: String,
25 pub file: Option<String>,
26 pub start: Option<(usize, usize)>,
28 pub end: Option<(usize, 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 start: None,
41 end: None,
42 span: None,
43 help: None,
44 }
45 }
46
47 pub fn at(mut self, file: Option<String>, span: Option<(usize, usize)>) -> Diagnostic {
48 self.file = file;
49 self.span = span;
50 self
51 }
52
53 pub fn helped(mut self, help: impl Into<String>) -> Diagnostic {
54 self.help = Some(help.into());
55 self
56 }
57
58 pub fn on_line(mut self, line: Option<usize>) -> Diagnostic {
60 self.start = line.map(|l| (l, 1));
61 self
62 }
63
64 pub fn placed(mut self, at: Option<(usize, usize)>) -> Diagnostic {
65 self.start = at;
66 self.end = at;
67 self
68 }
69
70 pub fn with_severity(mut self, severity: Severity) -> Diagnostic {
71 self.severity = severity;
72 self
73 }
74}
75
76pub fn diagnostics_json(diagnostics: &[Diagnostic]) -> String {
77 list(diagnostics, diagnostic_json)
78}
79
80fn diagnostic_json(d: &Diagnostic) -> String {
81 let span = d.span.map_or_else(
82 || NONE.to_string(),
83 |(o, l)| format!("{{ \"offset\": {o}, \"length\": {l} }}"),
84 );
85 let place = |at: Option<(usize, usize)>| {
86 at.map_or_else(
87 || NONE.to_string(),
88 |(l, c)| format!("{{ \"line\": {l}, \"column\": {c} }}"),
89 )
90 };
91 let (start, end) = (place(d.start), place(d.end));
92 let location = d.file.as_ref().map_or_else(
93 || NONE.to_string(),
94 |f| {
95 format!(
96 "{{ \"file\": \"{}\", \"span\": {span}, \"start\": {start}, \"end\": {end} }}",
97 escape(f)
98 )
99 },
100 );
101 let help = d
102 .help
103 .as_ref()
104 .map_or_else(|| NONE.to_string(), |h| format!("\"{}\"", escape(h)));
105 format!(
106 "{{ \"code\": \"{}\", \"severity\": \"{}\", \"message\": \"{}\", \"location\": {location}, \"help\": {help} }}",
107 escape(&d.code),
108 d.severity.as_str(),
109 escape(&d.message)
110 )
111}
112
113fn details_json(diagnostics: &[Diagnostic]) -> String {
116 let mut codes: Vec<&str> = Vec::new();
117 for d in diagnostics {
118 if !codes.contains(&d.code.as_str()) {
119 codes.push(&d.code);
120 }
121 }
122 format!(
123 "{{ \"count\": {}, \"codes\": {} }}",
124 diagnostics.len(),
125 crate::json::strings(&codes)
126 )
127}
128
129pub fn success_envelope(data: &str, diagnostics: &[Diagnostic]) -> String {
130 let fields = data
131 .strip_suffix('}')
132 .expect("every data builder answers one JSON object");
133 format!(
134 "{{\n \"status\": \"success\",\n \"data\": {fields},\n \"diagnostics\": {}\n }},\n \"meta\": {}\n}}",
135 diagnostics_json(diagnostics),
136 meta()
137 )
138}
139
140pub fn error_envelope(code: &str, message: &str, diagnostics: &[Diagnostic]) -> String {
142 format!(
143 "{{\n \"status\": \"error\",\n \"error\": {{ \"code\": \"{}\", \"message\": \"{}\", \"details\": {} }},\n \"data\": {{ \"diagnostics\": {} }},\n \"meta\": {}\n}}",
144 escape(code),
145 escape(message),
146 details_json(diagnostics),
147 diagnostics_json(diagnostics),
148 meta()
149 )
150}