1use serde::Serialize;
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
12pub enum ErrorCode {
13 IndexMissing,
15 NoMatches,
17 InvalidQuery,
19 TooManyFiles,
21 InvalidPath,
23 UnsupportedLanguage,
25 Internal,
27}
28
29impl ErrorCode {
30 #[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 #[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 #[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#[derive(Debug, Clone, Serialize)]
94pub struct ErrorResponse {
95 pub code: String,
97 pub message: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub path: Option<String>,
102 pub suggestion: String,
104}
105
106impl ErrorResponse {
107 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 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 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
143pub 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}