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 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 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
101fn 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
128pub 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}