1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use super::diagnostic::AgmError;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum ErrorOutputFormat {
12 Text,
13 Json,
14}
15
16impl fmt::Display for ErrorOutputFormat {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Text => write!(f, "text"),
20 Self::Json => write!(f, "json"),
21 }
22 }
23}
24
25impl std::str::FromStr for ErrorOutputFormat {
26 type Err = String;
27
28 fn from_str(s: &str) -> Result<Self, Self::Err> {
29 match s.to_lowercase().as_str() {
30 "text" => Ok(Self::Text),
31 "json" => Ok(Self::Json),
32 other => Err(format!(
33 "unknown error output format: {other} (expected `text` or `json`)"
34 )),
35 }
36 }
37}
38
39#[must_use]
40pub fn format_error_text(error: &AgmError) -> String {
41 let file = error.location.file.as_deref().unwrap_or("<unknown>");
42 let location = match error.location.line {
43 Some(line) => format!("{file}:{line}"),
44 None => file.to_string(),
45 };
46 format!(
47 "{location} [{code}] {severity}: {message}",
48 code = error.code,
49 severity = error.severity,
50 message = error.message,
51 )
52}
53
54#[must_use]
55pub fn format_error_json(error: &AgmError) -> serde_json::Value {
56 let mut map = serde_json::Map::new();
57 map.insert(
58 "code".to_string(),
59 serde_json::Value::String(error.code.to_string()),
60 );
61 map.insert(
62 "severity".to_string(),
63 serde_json::Value::String(error.severity.to_string()),
64 );
65 map.insert(
66 "message".to_string(),
67 serde_json::Value::String(error.message.clone()),
68 );
69 if let Some(ref file) = error.location.file {
70 map.insert("file".to_string(), serde_json::Value::String(file.clone()));
71 }
72 if let Some(line) = error.location.line {
73 map.insert(
74 "line".to_string(),
75 serde_json::Value::Number(serde_json::Number::from(line)),
76 );
77 }
78 if let Some(ref node) = error.location.node {
79 map.insert("node".to_string(), serde_json::Value::String(node.clone()));
80 }
81 serde_json::Value::Object(map)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::error::codes::ErrorCode;
88 use crate::error::diagnostic::{AgmError, ErrorLocation};
89
90 #[test]
91 fn test_format_error_text_full_location() {
92 let err = AgmError::new(
93 ErrorCode::V003,
94 "Duplicate node ID: `auth.login`",
95 ErrorLocation::full("file.agm", 42, "auth.login"),
96 );
97 let text = format_error_text(&err);
98 assert_eq!(
99 text,
100 "file.agm:42 [AGM-V003] error: Duplicate node ID: `auth.login`"
101 );
102 }
103
104 #[test]
105 fn test_format_error_text_warning() {
106 let err = AgmError::new(
107 ErrorCode::V010,
108 "Node type `workflow` typically includes field `steps` (missing)",
109 ErrorLocation::full("file.agm", 87, "deploy.step3"),
110 );
111 let text = format_error_text(&err);
112 assert_eq!(
113 text,
114 "file.agm:87 [AGM-V010] warning: Node type `workflow` typically includes field `steps` (missing)"
115 );
116 }
117
118 #[test]
119 fn test_format_error_text_no_file_uses_unknown() {
120 let err = AgmError::new(
121 ErrorCode::P008,
122 "Empty file (no nodes)",
123 ErrorLocation::default(),
124 );
125 let text = format_error_text(&err);
126 assert_eq!(text, "<unknown> [AGM-P008] error: Empty file (no nodes)");
127 }
128
129 #[test]
130 fn test_format_error_text_file_without_line() {
131 let err = AgmError::new(
132 ErrorCode::P008,
133 "Empty file (no nodes)",
134 ErrorLocation::new(Some("empty.agm".to_string()), None, None),
135 );
136 let text = format_error_text(&err);
137 assert_eq!(text, "empty.agm [AGM-P008] error: Empty file (no nodes)");
138 }
139
140 #[test]
141 fn test_format_error_json_full_location() {
142 let err = AgmError::new(
143 ErrorCode::V003,
144 "Duplicate node ID: `auth.login`",
145 ErrorLocation::full("file.agm", 42, "auth.login"),
146 );
147 let json = format_error_json(&err);
148 assert_eq!(json["code"], "AGM-V003");
149 assert_eq!(json["severity"], "error");
150 assert_eq!(json["message"], "Duplicate node ID: `auth.login`");
151 assert_eq!(json["file"], "file.agm");
152 assert_eq!(json["line"], 42);
153 assert_eq!(json["node"], "auth.login");
154 }
155
156 #[test]
157 fn test_format_error_json_minimal_location() {
158 let err = AgmError::new(
159 ErrorCode::P008,
160 "Empty file (no nodes)",
161 ErrorLocation::default(),
162 );
163 let json = format_error_json(&err);
164 assert_eq!(json["code"], "AGM-P008");
165 assert_eq!(json["severity"], "error");
166 assert!(json.get("file").is_none());
167 assert!(json.get("line").is_none());
168 assert!(json.get("node").is_none());
169 }
170
171 #[test]
172 fn test_format_error_json_is_valid_json() {
173 let err = AgmError::new(
174 ErrorCode::V003,
175 "test message",
176 ErrorLocation::full("file.agm", 42, "auth.login"),
177 );
178 let json = format_error_json(&err);
179 let json_string = serde_json::to_string(&json).unwrap();
180 let reparsed: serde_json::Value = serde_json::from_str(&json_string).unwrap();
181 assert_eq!(json, reparsed);
182 }
183
184 #[test]
185 fn test_format_error_json_warning_severity_value() {
186 let err = AgmError::new(ErrorCode::V010, "test", ErrorLocation::default());
187 let json = format_error_json(&err);
188 assert_eq!(json["severity"], "warning");
189 }
190
191 #[test]
192 fn test_format_error_json_info_severity_value() {
193 let err = AgmError::new(ErrorCode::P010, "test", ErrorLocation::default());
194 let json = format_error_json(&err);
195 assert_eq!(json["severity"], "info");
196 }
197
198 #[test]
199 fn test_error_output_format_from_str_text() {
200 assert_eq!(
201 "text".parse::<ErrorOutputFormat>().unwrap(),
202 ErrorOutputFormat::Text
203 );
204 assert_eq!(
205 "TEXT".parse::<ErrorOutputFormat>().unwrap(),
206 ErrorOutputFormat::Text
207 );
208 }
209
210 #[test]
211 fn test_error_output_format_from_str_json() {
212 assert_eq!(
213 "json".parse::<ErrorOutputFormat>().unwrap(),
214 ErrorOutputFormat::Json
215 );
216 assert_eq!(
217 "JSON".parse::<ErrorOutputFormat>().unwrap(),
218 ErrorOutputFormat::Json
219 );
220 }
221
222 #[test]
223 fn test_error_output_format_from_str_invalid() {
224 assert!("xml".parse::<ErrorOutputFormat>().is_err());
225 }
226
227 #[test]
228 fn test_error_output_format_display() {
229 assert_eq!(ErrorOutputFormat::Text.to_string(), "text");
230 assert_eq!(ErrorOutputFormat::Json.to_string(), "json");
231 }
232}