Skip to main content

sqry_core/
errors.rs

1//! Structured error types for programmatic tool integration
2//!
3//! This module provides standardized error codes and responses that enable
4//! programmatic consumers to handle failures gracefully and suggest actionable fixes.
5
6use serde::Serialize;
7use std::fmt;
8
9/// Standardized error codes for tool integration
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
12pub enum ErrorCode {
13    /// Index file not found - agent should run `sqry index`
14    IndexMissing,
15    /// Query returned zero results
16    NoMatches,
17    /// Query syntax is invalid
18    InvalidQuery,
19    /// Scope too broad (>100k files by default)
20    TooManyFiles,
21    /// Path doesn't exist or not accessible
22    InvalidPath,
23    /// Language filter references unsupported language
24    UnsupportedLanguage,
25    /// Generic internal error
26    Internal,
27}
28
29impl ErrorCode {
30    /// Get the error code as a string
31    #[must_use]
32    pub fn as_str(&self) -> &'static str {
33        match self {
34            ErrorCode::IndexMissing => "INDEX_MISSING",
35            ErrorCode::NoMatches => "NO_MATCHES",
36            ErrorCode::InvalidQuery => "INVALID_QUERY",
37            ErrorCode::TooManyFiles => "TOO_MANY_FILES",
38            ErrorCode::InvalidPath => "INVALID_PATH",
39            ErrorCode::UnsupportedLanguage => "UNSUPPORTED_LANGUAGE",
40            ErrorCode::Internal => "INTERNAL",
41        }
42    }
43
44    /// Get a default message for this error code
45    #[must_use]
46    pub fn default_message(&self) -> &'static str {
47        match self {
48            ErrorCode::IndexMissing => "No symbol index found",
49            ErrorCode::NoMatches => "No symbols match the query",
50            ErrorCode::InvalidQuery => "Query syntax is invalid",
51            ErrorCode::TooManyFiles => "Scope too broad - too many files to process",
52            ErrorCode::InvalidPath => "Path does not exist or is not accessible",
53            ErrorCode::UnsupportedLanguage => "Language is not supported",
54            ErrorCode::Internal => "Internal error occurred",
55        }
56    }
57
58    /// Get a suggested action for this error
59    #[must_use]
60    pub fn suggestion(&self, context: Option<&str>) -> String {
61        match self {
62            ErrorCode::IndexMissing => {
63                let path = context.unwrap_or(".");
64                format!("Run: sqry index {path}")
65            }
66            ErrorCode::NoMatches => {
67                "Try broadening your search or using fuzzy mode with --fuzzy".to_string()
68            }
69            ErrorCode::InvalidQuery => {
70                "Check query syntax. Example: kind:function AND name:test".to_string()
71            }
72            ErrorCode::TooManyFiles => {
73                "Narrow the scope with a more specific path or use filters".to_string()
74            }
75            ErrorCode::InvalidPath => {
76                "Verify the path exists and you have read permissions".to_string()
77            }
78            ErrorCode::UnsupportedLanguage => {
79                "Run 'sqry --list-languages' to see supported languages".to_string()
80            }
81            ErrorCode::Internal => "Please report this issue on GitHub".to_string(),
82        }
83    }
84}
85
86impl fmt::Display for ErrorCode {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        write!(f, "{}", self.as_str())
89    }
90}
91
92/// Structured error response for JSON output
93#[derive(Debug, Clone, Serialize)]
94pub struct ErrorResponse {
95    /// Error code (e.g., "`INDEX_MISSING`")
96    pub code: String,
97    /// Human-readable error message
98    pub message: String,
99    /// Optional context (e.g., path that failed)
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub path: Option<String>,
102    /// Suggested action to fix the error
103    pub suggestion: String,
104}
105
106impl ErrorResponse {
107    /// Create a new error response
108    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
109        Self {
110            code: code.as_str().to_string(),
111            message: message.into(),
112            path: None,
113            suggestion: code.suggestion(None),
114        }
115    }
116
117    /// Create error response with path context
118    pub fn with_path(code: ErrorCode, message: impl Into<String>, path: impl Into<String>) -> Self {
119        let path_str = path.into();
120        Self {
121            code: code.as_str().to_string(),
122            message: message.into(),
123            suggestion: code.suggestion(Some(&path_str)),
124            path: Some(path_str),
125        }
126    }
127
128    /// Create error response with custom suggestion
129    pub fn with_suggestion(
130        code: ErrorCode,
131        message: impl Into<String>,
132        suggestion: impl Into<String>,
133    ) -> Self {
134        Self {
135            code: code.as_str().to_string(),
136            message: message.into(),
137            path: None,
138            suggestion: suggestion.into(),
139        }
140    }
141}
142
143/// Result type using `ErrorResponse`
144pub type ToolResult<T> = Result<T, ErrorResponse>;
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_error_code_as_str() {
152        assert_eq!(ErrorCode::IndexMissing.as_str(), "INDEX_MISSING");
153        assert_eq!(ErrorCode::NoMatches.as_str(), "NO_MATCHES");
154        assert_eq!(ErrorCode::InvalidQuery.as_str(), "INVALID_QUERY");
155    }
156
157    #[test]
158    fn test_error_code_display() {
159        assert_eq!(format!("{}", ErrorCode::IndexMissing), "INDEX_MISSING");
160    }
161
162    #[test]
163    fn test_error_response_new() {
164        let err = ErrorResponse::new(ErrorCode::IndexMissing, "Index not found");
165        assert_eq!(err.code, "INDEX_MISSING");
166        assert_eq!(err.message, "Index not found");
167        assert!(err.path.is_none());
168        assert!(err.suggestion.starts_with("Run: sqry index"));
169    }
170
171    #[test]
172    fn test_error_response_with_path() {
173        let err = ErrorResponse::with_path(
174            ErrorCode::InvalidPath,
175            "Path does not exist",
176            "/invalid/path",
177        );
178        assert_eq!(err.code, "INVALID_PATH");
179        assert_eq!(err.path, Some("/invalid/path".to_string()));
180    }
181
182    #[test]
183    fn test_error_response_serialization() {
184        let err = ErrorResponse::new(ErrorCode::NoMatches, "No results");
185        let json = serde_json::to_string(&err).unwrap();
186        assert!(json.contains("\"code\":\"NO_MATCHES\""));
187        assert!(json.contains("\"message\":\"No results\""));
188    }
189
190    #[test]
191    fn test_suggestion_with_context() {
192        let suggestion = ErrorCode::IndexMissing.suggestion(Some("/home/project"));
193        assert_eq!(suggestion, "Run: sqry index /home/project");
194    }
195}