use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fmt;
use std::sync::{Arc, OnceLock, RwLock};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub type ErrorContext = HashMap<String, Value>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "name", content = "details", rename_all = "PascalCase")]
pub enum CodeGraphError {
CodeGraphError {
message: String,
code: String,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<ErrorContext>,
},
FileError(FileError),
ParseError(ParseError),
DatabaseError(DatabaseError),
SearchError(SearchError),
VectorError(VectorError),
ConfigError(ConfigError),
}
impl CodeGraphError {
pub fn new(
message: impl Into<String>,
code: impl Into<String>,
context: Option<ErrorContext>,
) -> Self {
Self::CodeGraphError {
message: message.into(),
code: code.into(),
context,
}
}
pub fn message(&self) -> &str {
match self {
Self::CodeGraphError { message, .. } => message,
Self::FileError(error) => &error.message,
Self::ParseError(error) => &error.message,
Self::DatabaseError(error) => &error.message,
Self::SearchError(error) => &error.message,
Self::VectorError(error) => &error.message,
Self::ConfigError(error) => &error.message,
}
}
pub fn code(&self) -> &str {
match self {
Self::CodeGraphError { code, .. } => code,
Self::FileError(_) => "FILE_ERROR",
Self::ParseError(_) => "PARSE_ERROR",
Self::DatabaseError(_) => "DATABASE_ERROR",
Self::SearchError(_) => "SEARCH_ERROR",
Self::VectorError(_) => "VECTOR_ERROR",
Self::ConfigError(_) => "CONFIG_ERROR",
}
}
}
impl fmt::Display for CodeGraphError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message())
}
}
impl Error for CodeGraphError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FileError {
pub message: String,
pub file_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cause: Option<String>,
}
impl FileError {
pub fn new(
message: impl Into<String>,
file_path: impl Into<String>,
cause: Option<String>,
) -> Self {
Self {
message: message.into(),
file_path: file_path.into(),
cause,
}
}
}
impl From<FileError> for CodeGraphError {
fn from(error: FileError) -> Self {
Self::FileError(error)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParseError {
pub message: String,
pub file_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cause: Option<String>,
}
impl ParseError {
pub fn new(
message: impl Into<String>,
file_path: impl Into<String>,
line: Option<u64>,
column: Option<u64>,
cause: Option<String>,
) -> Self {
Self {
message: message.into(),
file_path: file_path.into(),
line,
column,
cause,
}
}
}
impl From<ParseError> for CodeGraphError {
fn from(error: ParseError) -> Self {
Self::ParseError(error)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DatabaseError {
pub message: String,
pub operation: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cause: Option<String>,
}
impl DatabaseError {
pub fn new(
message: impl Into<String>,
operation: impl Into<String>,
cause: Option<String>,
) -> Self {
Self {
message: message.into(),
operation: operation.into(),
cause,
}
}
}
impl From<DatabaseError> for CodeGraphError {
fn from(error: DatabaseError) -> Self {
Self::DatabaseError(error)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchError {
pub message: String,
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cause: Option<String>,
}
impl SearchError {
pub fn new(
message: impl Into<String>,
query: impl Into<String>,
cause: Option<String>,
) -> Self {
Self {
message: message.into(),
query: query.into(),
cause,
}
}
}
impl From<SearchError> for CodeGraphError {
fn from(error: SearchError) -> Self {
Self::SearchError(error)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VectorError {
pub message: String,
pub operation: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cause: Option<String>,
}
impl VectorError {
pub fn new(
message: impl Into<String>,
operation: impl Into<String>,
cause: Option<String>,
) -> Self {
Self {
message: message.into(),
operation: operation.into(),
cause,
}
}
}
impl From<VectorError> for CodeGraphError {
fn from(error: VectorError) -> Self {
Self::VectorError(error)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigError {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<ErrorContext>,
}
impl ConfigError {
pub fn new(message: impl Into<String>, details: Option<ErrorContext>) -> Self {
Self {
message: message.into(),
details,
}
}
}
impl From<ConfigError> for CodeGraphError {
fn from(error: ConfigError) -> Self {
Self::ConfigError(error)
}
}
pub trait Logger: Send + Sync {
fn debug(&self, message: &str, context: Option<&ErrorContext>);
fn warn(&self, message: &str, context: Option<&ErrorContext>);
fn error(&self, message: &str, context: Option<&ErrorContext>);
}
#[derive(Debug, Clone, Copy)]
pub struct DefaultLogger;
pub const DEFAULT_LOGGER: DefaultLogger = DefaultLogger;
impl Logger for DefaultLogger {
fn debug(&self, message: &str, context: Option<&ErrorContext>) {
if env::var_os("RUSTCODEGRAPH_DEBUG").is_some() {
eprintln!("[RustCodeGraph] {message} {}", format_context(context));
}
}
fn warn(&self, message: &str, context: Option<&ErrorContext>) {
eprintln!("[RustCodeGraph] {message} {}", format_context(context));
}
fn error(&self, message: &str, context: Option<&ErrorContext>) {
eprintln!("[RustCodeGraph] {message} {}", format_context(context));
}
}
#[derive(Debug, Clone, Copy)]
pub struct SilentLogger;
pub const SILENT_LOGGER: SilentLogger = SilentLogger;
impl Logger for SilentLogger {
fn debug(&self, _message: &str, _context: Option<&ErrorContext>) {}
fn warn(&self, _message: &str, _context: Option<&ErrorContext>) {}
fn error(&self, _message: &str, _context: Option<&ErrorContext>) {}
}
static CURRENT_LOGGER: OnceLock<RwLock<Arc<dyn Logger>>> = OnceLock::new();
fn current_logger_cell() -> &'static RwLock<Arc<dyn Logger>> {
CURRENT_LOGGER.get_or_init(|| RwLock::new(Arc::new(DEFAULT_LOGGER)))
}
pub fn set_logger<L>(logger: L)
where
L: Logger + 'static,
{
let mut current = current_logger_cell()
.write()
.expect("current logger lock poisoned");
*current = Arc::new(logger);
}
pub fn get_logger() -> Arc<dyn Logger> {
current_logger_cell()
.read()
.expect("current logger lock poisoned")
.clone()
}
pub fn log_debug(message: &str, context: Option<&ErrorContext>) {
get_logger().debug(message, context);
}
pub fn log_warn(message: &str, context: Option<&ErrorContext>) {
get_logger().warn(message, context);
}
pub fn log_error(message: &str, context: Option<&ErrorContext>) {
get_logger().error(message, context);
}
fn format_context(context: Option<&ErrorContext>) -> String {
match context {
Some(context) if !context.is_empty() => format!("{context:?}"),
_ => String::new(),
}
}