Skip to main content

scrape_core/query/
error.rs

1//! Error types for query operations.
2
3use thiserror::Error;
4
5use crate::error::{SourcePosition, SourceSpan};
6
7/// Result type alias for query operations.
8pub type QueryResult<T> = std::result::Result<T, QueryError>;
9
10/// Error type for query operations.
11///
12/// This error type distinguishes between invalid selectors and other query failures,
13/// enabling `Result<Option<Tag>, QueryError>` to differentiate "not found" from
14/// "invalid query".
15#[derive(Debug, Error, Clone, PartialEq, Eq)]
16pub enum QueryError {
17    /// Invalid CSS selector syntax.
18    #[error("invalid selector{}: {message}", format_position(span.as_ref()))]
19    InvalidSelector {
20        /// Error message from selector parser.
21        message: String,
22        /// Source location, if available.
23        span: Option<SourceSpan>,
24    },
25}
26
27fn format_position(span: Option<&SourceSpan>) -> String {
28    span.map_or_else(String::new, |s| {
29        format!(" at line {}, column {}", s.start.line, s.start.column)
30    })
31}
32
33impl QueryError {
34    /// Creates a new invalid selector error.
35    #[must_use]
36    pub fn invalid_selector(message: impl Into<String>) -> Self {
37        Self::InvalidSelector { message: message.into(), span: None }
38    }
39
40    /// Creates a new invalid selector error with position.
41    #[must_use]
42    pub fn invalid_selector_at(message: impl Into<String>, line: usize, column: usize) -> Self {
43        Self::InvalidSelector {
44            message: message.into(),
45            span: Some(SourceSpan::new(
46                SourcePosition::new(line, column, 0),
47                SourcePosition::new(line, column + 1, 0),
48            )),
49        }
50    }
51
52    /// Returns the source span if available.
53    #[must_use]
54    pub fn span(&self) -> Option<&SourceSpan> {
55        match self {
56            Self::InvalidSelector { span, .. } => span.as_ref(),
57        }
58    }
59
60    /// Returns the line number (1-indexed) if available.
61    #[must_use]
62    pub fn line(&self) -> Option<usize> {
63        self.span().map(|s| s.start.line)
64    }
65
66    /// Returns the column number (1-indexed) if available.
67    #[must_use]
68    pub fn column(&self) -> Option<usize> {
69        self.span().map(|s| s.start.column)
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_query_error_display() {
79        let err = QueryError::invalid_selector("unexpected token at position 5");
80        assert_eq!(err.to_string(), "invalid selector: unexpected token at position 5");
81    }
82
83    #[test]
84    fn test_query_error_with_position() {
85        let err = QueryError::invalid_selector_at("unexpected token", 1, 5);
86        assert_eq!(err.to_string(), "invalid selector at line 1, column 5: unexpected token");
87        assert_eq!(err.line(), Some(1));
88        assert_eq!(err.column(), Some(5));
89    }
90
91    #[test]
92    fn test_query_error_equality() {
93        let err1 = QueryError::invalid_selector("foo");
94        let err2 = QueryError::invalid_selector("foo");
95        let err3 = QueryError::invalid_selector("bar");
96        assert_eq!(err1, err2);
97        assert_ne!(err1, err3);
98    }
99
100    #[test]
101    fn test_query_error_span() {
102        let err_with_span = QueryError::invalid_selector_at("test", 2, 7);
103        assert!(err_with_span.span().is_some());
104
105        let err_without_span = QueryError::invalid_selector("test");
106        assert!(err_without_span.span().is_none());
107        assert_eq!(err_without_span.line(), None);
108        assert_eq!(err_without_span.column(), None);
109    }
110
111    #[test]
112    fn test_query_result_type() {
113        let ok: QueryResult<i32> = Ok(42);
114        let err: QueryResult<i32> = Err(QueryError::invalid_selector("test"));
115
116        assert!(ok.is_ok());
117        assert!(err.is_err());
118    }
119}