use serde::Serialize;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
IndexMissing,
NoMatches,
InvalidQuery,
TooManyFiles,
InvalidPath,
UnsupportedLanguage,
Internal,
}
impl ErrorCode {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
ErrorCode::IndexMissing => "INDEX_MISSING",
ErrorCode::NoMatches => "NO_MATCHES",
ErrorCode::InvalidQuery => "INVALID_QUERY",
ErrorCode::TooManyFiles => "TOO_MANY_FILES",
ErrorCode::InvalidPath => "INVALID_PATH",
ErrorCode::UnsupportedLanguage => "UNSUPPORTED_LANGUAGE",
ErrorCode::Internal => "INTERNAL",
}
}
#[must_use]
pub fn default_message(&self) -> &'static str {
match self {
ErrorCode::IndexMissing => "No symbol index found",
ErrorCode::NoMatches => "No symbols match the query",
ErrorCode::InvalidQuery => "Query syntax is invalid",
ErrorCode::TooManyFiles => "Scope too broad - too many files to process",
ErrorCode::InvalidPath => "Path does not exist or is not accessible",
ErrorCode::UnsupportedLanguage => "Language is not supported",
ErrorCode::Internal => "Internal error occurred",
}
}
#[must_use]
pub fn suggestion(&self, context: Option<&str>) -> String {
match self {
ErrorCode::IndexMissing => {
let path = context.unwrap_or(".");
format!("Run: sqry index {path}")
}
ErrorCode::NoMatches => {
"Try broadening your search or using fuzzy mode with --fuzzy".to_string()
}
ErrorCode::InvalidQuery => {
"Check query syntax. Example: kind:function AND name:test".to_string()
}
ErrorCode::TooManyFiles => {
"Narrow the scope with a more specific path or use filters".to_string()
}
ErrorCode::InvalidPath => {
"Verify the path exists and you have read permissions".to_string()
}
ErrorCode::UnsupportedLanguage => {
"Run 'sqry --list-languages' to see supported languages".to_string()
}
ErrorCode::Internal => "Please report this issue on GitHub".to_string(),
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ErrorResponse {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
pub suggestion: String,
}
impl ErrorResponse {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code: code.as_str().to_string(),
message: message.into(),
path: None,
suggestion: code.suggestion(None),
}
}
pub fn with_path(code: ErrorCode, message: impl Into<String>, path: impl Into<String>) -> Self {
let path_str = path.into();
Self {
code: code.as_str().to_string(),
message: message.into(),
suggestion: code.suggestion(Some(&path_str)),
path: Some(path_str),
}
}
pub fn with_suggestion(
code: ErrorCode,
message: impl Into<String>,
suggestion: impl Into<String>,
) -> Self {
Self {
code: code.as_str().to_string(),
message: message.into(),
path: None,
suggestion: suggestion.into(),
}
}
}
pub type ToolResult<T> = Result<T, ErrorResponse>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_code_as_str() {
assert_eq!(ErrorCode::IndexMissing.as_str(), "INDEX_MISSING");
assert_eq!(ErrorCode::NoMatches.as_str(), "NO_MATCHES");
assert_eq!(ErrorCode::InvalidQuery.as_str(), "INVALID_QUERY");
}
#[test]
fn test_error_code_display() {
assert_eq!(format!("{}", ErrorCode::IndexMissing), "INDEX_MISSING");
}
#[test]
fn test_error_response_new() {
let err = ErrorResponse::new(ErrorCode::IndexMissing, "Index not found");
assert_eq!(err.code, "INDEX_MISSING");
assert_eq!(err.message, "Index not found");
assert!(err.path.is_none());
assert!(err.suggestion.starts_with("Run: sqry index"));
}
#[test]
fn test_error_response_with_path() {
let err = ErrorResponse::with_path(
ErrorCode::InvalidPath,
"Path does not exist",
"/invalid/path",
);
assert_eq!(err.code, "INVALID_PATH");
assert_eq!(err.path, Some("/invalid/path".to_string()));
}
#[test]
fn test_error_response_serialization() {
let err = ErrorResponse::new(ErrorCode::NoMatches, "No results");
let json = serde_json::to_string(&err).unwrap();
assert!(json.contains("\"code\":\"NO_MATCHES\""));
assert!(json.contains("\"message\":\"No results\""));
}
#[test]
fn test_suggestion_with_context() {
let suggestion = ErrorCode::IndexMissing.suggestion(Some("/home/project"));
assert_eq!(suggestion, "Run: sqry index /home/project");
}
}