laburnum 1.17.0

An LSP framework for building language servers and compilers, powered by an incremental query tree with content-addressed storage, task-based dataflow, and parallel queries.
Documentation
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

use {
  crate::Uri,
  error_stack::{
    Context,
    Report,
  },
  std::{
    io,
    path::PathBuf,
  },
};

/// The central error type for the laburnum-fs crate.
#[derive(thiserror::Error, Debug, Clone)]
pub enum FsError {
  #[error("I/O error: {0}")]
  Io(String),

  #[error("Uri error: {0}")]
  Uri(String),

  #[error("Path conversion error: {0}")]
  PathConversion(String),

  #[error("Invalid glob pattern: {0}")]
  InvalidGlob(String),

  #[error("File system error: {0}")]
  FileSystem(String),

  #[error("Invalid operation: {0}")]
  InvalidOperation(String),

  #[error("Not implemented: {0}")]
  NotImplemented(String),

  #[error("No filesystem found")]
  NoFileSystem,
}

// Define a shorthand for Result
pub type Result<T> = std::result::Result<T, Report<FsError>>;

// Helper functions for creating errors with appropriate context
impl FsError {
  pub fn io_error(message: impl Into<String>) -> Self {
    FsError::Io(message.into())
  }

  pub fn uri_error(message: impl Into<String>) -> Self {
    FsError::Uri(message.into())
  }

  pub fn path_conversion_error(message: impl Into<String>) -> Self {
    FsError::PathConversion(message.into())
  }

  pub fn invalid_glob(message: impl Into<String>) -> Self {
    FsError::InvalidGlob(message.into())
  }

  pub fn filesystem_error(message: impl Into<String>) -> Self {
    FsError::FileSystem(message.into())
  }

  pub fn invalid_operation(message: impl Into<String>) -> Self {
    FsError::InvalidOperation(message.into())
  }

  pub fn not_implemented(message: impl Into<String>) -> Self {
    FsError::NotImplemented(message.into())
  }
}

// Create Report from std::io::Error
pub fn io_err(error: io::Error) -> Report<FsError> {
  Report::new(FsError::Io(error.to_string()))
    .attach_printable(format!("I/O error: {error}"))
}

// Create Report from fluent_uri::error::ParseError
pub fn uri_err(error: fluent_uri::error::ParseError) -> Report<FsError> {
  Report::new(FsError::Uri(error.to_string()))
    .attach_printable(format!("Uri parse error: {error}"))
}

/// File path context for attaching to errors
#[derive(Debug, Clone)]
pub struct FilePath(pub PathBuf);

/// Uri context for attaching to errors
#[derive(Debug, Clone)]
pub struct UriPath(pub Uri);

/// Extension traits for std::io::Error to convert to our error type
pub trait IoErrorExt {
  fn to_fs_error(self) -> Report<FsError>;
  fn to_fs_error_with_path(self, path: impl Into<PathBuf>) -> Report<FsError>;
  fn to_fs_error_with_uri(self, uri: Uri) -> Report<FsError>;
}

impl IoErrorExt for io::Error {
  fn to_fs_error(self) -> Report<FsError> {
    Report::new(FsError::Io(self.to_string()))
      .attach_printable(format!("I/O error: {self}"))
  }

  fn to_fs_error_with_path(self, path: impl Into<PathBuf>) -> Report<FsError> {
    let path = path.into();
    Report::new(FsError::Io(self.to_string()))
      .attach_printable(format!("I/O error: {self}"))
      .attach(FilePath(path.clone()))
      .attach_printable(format!("Path: {}", path.display()))
  }

  fn to_fs_error_with_uri(self, uri: Uri) -> Report<FsError> {
    Report::new(FsError::Io(self.to_string()))
      .attach_printable(format!("I/O error: {self}"))
      .attach(UriPath(uri.clone()))
      .attach_printable(format!("uri: {uri}"))
  }
}

/// Useful macro to create a file system error with context
#[macro_export]
macro_rules! fs_error {
  ($msg:expr) => {
    error_stack::Report::new($crate::fs::errors::FsError::fs_error($msg))
  };
  ($fmt:expr, $($arg:tt)*) => {
    error_stack::Report::new($crate::fs::errors::FsError::fs_error(format!($fmt, $($arg)*)))
  };
}

/// Extension methods for the Result type to make error handling more ergonomic
pub trait FsResultExt<T, E: Context> {
  fn with_path(
    self,
    path: impl Into<PathBuf>,
  ) -> std::result::Result<T, Report<FsError>>;
  fn with_uri(self, uri: Uri) -> std::result::Result<T, Report<FsError>>;
}

impl<T, E: Context> FsResultExt<T, E> for std::result::Result<T, E> {
  fn with_path(
    self,
    path: impl Into<PathBuf>,
  ) -> std::result::Result<T, Report<FsError>> {
    let path = path.into();
    self.map_err(|e| {
      Report::new(e)
        .change_context(FsError::filesystem_error("Operation failed"))
        .attach(FilePath(path.clone()))
        .attach_printable(format!("Path: {}", path.display()))
    })
  }

  fn with_uri(self, uri: Uri) -> std::result::Result<T, Report<FsError>> {
    self.map_err(|e| {
      Report::new(e)
        .change_context(FsError::filesystem_error("Operation failed"))
        .attach(UriPath(uri.clone()))
        .attach_printable(format!("uri: {uri}"))
    })
  }
}

/// Extension methods for Report<E> to add file system specific context
pub trait ReportExt<E: Context> {
  fn with_path(self, path: impl Into<PathBuf>) -> Report<FsError>;
  fn with_uri(self, uri: Uri) -> Report<FsError>;
}

impl<E: Context> ReportExt<E> for Report<E> {
  fn with_path(self, path: impl Into<PathBuf>) -> Report<FsError> {
    let path = path.into();
    self
      .change_context(FsError::filesystem_error("Operation failed"))
      .attach(FilePath(path.clone()))
      .attach_printable(format!("Path: {}", path.display()))
  }

  fn with_uri(self, uri: Uri) -> Report<FsError> {
    self
      .change_context(FsError::filesystem_error("Operation failed"))
      .attach(UriPath(uri.clone()))
      .attach_printable(format!("uri: {uri}"))
  }
}