Skip to main content

nginx_discovery/
error_builder.rs

1//! Error builder for constructing detailed error messages
2//!
3//! Provides a fluent API for building errors with context.
4
5use crate::error::Error;
6
7/// Builder for constructing parse errors with context
8#[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    /// Create a new error builder
19    #[must_use]
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    /// Set the error message
25    #[must_use]
26    pub fn message(mut self, message: impl Into<String>) -> Self {
27        self.message = message.into();
28        self
29    }
30
31    /// Set the location (line and column)
32    #[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    /// Set the source code snippet
40    #[must_use]
41    pub fn snippet(mut self, snippet: impl Into<String>) -> Self {
42        self.snippet = Some(snippet.into());
43        self
44    }
45
46    /// Set helpful suggestion
47    #[must_use]
48    pub fn help(mut self, help: impl Into<String>) -> Self {
49        self.help = Some(help.into());
50        self
51    }
52
53    /// Build the error
54    #[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/// Extract a snippet from source text around a specific line
65///
66/// # Arguments
67/// * `source` - The full source text
68/// * `line` - The line number (1-indexed)
69/// * `context_lines` - Number of lines to show before and after
70///
71/// # Returns
72/// A string containing the relevant lines
73#[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
84/// Get the line at a specific index
85pub 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}