nginx_discovery/
error_builder.rs1use crate::error::Error;
6
7#[derive(Debug, Default)]
9pub struct ErrorBuilder {
10 message: String,
11 line: usize,
12 col: usize,
13 snippet: Option<String>,
14 help: Option<String>,
15}
16
17impl ErrorBuilder {
18 #[must_use]
20 pub fn new() -> Self {
21 Self::default()
22 }
23
24 #[must_use]
26 pub fn message(mut self, message: impl Into<String>) -> Self {
27 self.message = message.into();
28 self
29 }
30
31 #[must_use]
33 pub fn location(mut self, line: usize, col: usize) -> Self {
34 self.line = line;
35 self.col = col;
36 self
37 }
38
39 #[must_use]
41 pub fn snippet(mut self, snippet: impl Into<String>) -> Self {
42 self.snippet = Some(snippet.into());
43 self
44 }
45
46 #[must_use]
48 pub fn help(mut self, help: impl Into<String>) -> Self {
49 self.help = Some(help.into());
50 self
51 }
52
53 #[must_use]
55 pub fn build(self) -> Error {
56 if let (Some(snippet), Some(help)) = (self.snippet, self.help) {
57 Error::parse_with_context(self.message, self.line, self.col, snippet, help)
58 } else {
59 Error::parse(self.message, self.line, self.col)
60 }
61 }
62}
63
64#[must_use]
74pub fn extract_snippet(source: &str, line: usize, context_lines: usize) -> String {
75 let lines: Vec<&str> = source.lines().collect();
76 let line_idx = line.saturating_sub(1);
77
78 let start = line_idx.saturating_sub(context_lines);
79 let end = (line_idx + context_lines + 1).min(lines.len());
80
81 lines[start..end].join("\n")
82}
83
84pub fn get_line(source: &str, line: usize) -> Option<String> {
86 source.lines().nth(line.saturating_sub(1)).map(String::from)
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn test_error_builder_basic() {
95 let error = ErrorBuilder::new()
96 .message("unexpected token")
97 .location(5, 10)
98 .build();
99
100 assert!(error.to_string().contains("line 5"));
101 assert!(error.to_string().contains("column 10"));
102 }
103
104 #[test]
105 fn test_error_builder_with_context() {
106 let error = ErrorBuilder::new()
107 .message("missing semicolon")
108 .location(10, 20)
109 .snippet("server { listen 80 }")
110 .help("Add a semicolon after '80'")
111 .build();
112
113 let detailed = error.detailed();
114 assert!(detailed.contains("missing semicolon"));
115 assert!(detailed.contains("server { listen 80 }"));
116 assert!(detailed.contains("Help: Add a semicolon"));
117 }
118
119 #[test]
120 fn test_extract_snippet() {
121 let source = "line 1\nline 2\nline 3\nline 4\nline 5";
122
123 let snippet = extract_snippet(source, 3, 1);
124 assert_eq!(snippet, "line 2\nline 3\nline 4");
125
126 let snippet = extract_snippet(source, 1, 1);
127 assert_eq!(snippet, "line 1\nline 2");
128
129 let snippet = extract_snippet(source, 5, 1);
130 assert_eq!(snippet, "line 4\nline 5");
131 }
132
133 #[test]
134 fn test_get_line() {
135 let source = "line 1\nline 2\nline 3";
136
137 assert_eq!(get_line(source, 1), Some("line 1".to_string()));
138 assert_eq!(get_line(source, 2), Some("line 2".to_string()));
139 assert_eq!(get_line(source, 3), Some("line 3".to_string()));
140 assert_eq!(get_line(source, 4), None);
141 }
142
143 #[test]
144 fn test_builder_fluent_api() {
145 let error = ErrorBuilder::new()
146 .message("test error")
147 .location(1, 1)
148 .snippet("test snippet")
149 .help("test help")
150 .build();
151
152 assert!(error.detailed().contains("test error"));
153 assert!(error.detailed().contains("test snippet"));
154 assert!(error.detailed().contains("test help"));
155 }
156}