refactor(config)!: transition from anyhow to thiserror

This commit is contained in:
WillLillis 2025-10-04 01:22:02 -04:00 committed by Christian Clason
parent db2d221ae9
commit 7eb23d9f3c
3 changed files with 66 additions and 17 deletions

2
Cargo.lock generated
View file

@ -2035,11 +2035,11 @@ dependencies = [
name = "tree-sitter-config"
version = "0.26.0"
dependencies = [
"anyhow",
"etcetera",
"log",
"serde",
"serde_json",
"thiserror 2.0.16",
]
[[package]]

View file

@ -20,8 +20,8 @@ path = "src/tree_sitter_config.rs"
workspace = true
[dependencies]
anyhow.workspace = true
etcetera.workspace = true
log.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true

View file

@ -1,12 +1,54 @@
#![cfg_attr(not(any(test, doctest)), doc = include_str!("../README.md"))]
use std::{env, fs, path::PathBuf};
use std::{
env, fs,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use etcetera::BaseStrategy as _;
use log::warn;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
pub type ConfigResult<T> = Result<T, ConfigError>;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Bad JSON config {0} -- {1}")]
ConfigRead(String, serde_json::Error),
#[error(transparent)]
HomeDir(#[from] etcetera::HomeDirError),
#[error(transparent)]
IO(IoError),
#[error(transparent)]
Serialization(#[from] serde_json::Error),
}
#[derive(Debug, Error)]
pub struct IoError {
pub error: std::io::Error,
pub path: Option<String>,
}
impl IoError {
fn new(error: std::io::Error, path: Option<&Path>) -> Self {
Self {
error,
path: path.map(|p| p.to_string_lossy().to_string()),
}
}
}
impl std::fmt::Display for IoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.error)?;
if let Some(ref path) = self.path {
write!(f, " ({path})")?;
}
Ok(())
}
}
/// Holds the contents of tree-sitter's configuration file.
///
@ -23,7 +65,7 @@ pub struct Config {
}
impl Config {
pub fn find_config_file() -> Result<Option<PathBuf>> {
pub fn find_config_file() -> ConfigResult<Option<PathBuf>> {
if let Ok(path) = env::var("TREE_SITTER_DIR") {
let mut path = PathBuf::from(path);
path.push("config.json");
@ -46,8 +88,12 @@ impl Config {
.join("tree-sitter")
.join("config.json");
if legacy_apple_path.is_file() {
fs::create_dir_all(xdg_path.parent().unwrap())?;
fs::rename(&legacy_apple_path, &xdg_path)?;
let xdg_dir = xdg_path.parent().unwrap();
fs::create_dir_all(xdg_dir)
.map_err(|e| ConfigError::IO(IoError::new(e, Some(xdg_dir))))?;
fs::rename(&legacy_apple_path, &xdg_path).map_err(|e| {
ConfigError::IO(IoError::new(e, Some(legacy_apple_path.as_path())))
})?;
warn!(
"Your config.json file has been automatically migrated from \"{}\" to \"{}\"",
legacy_apple_path.display(),
@ -67,7 +113,7 @@ impl Config {
Ok(None)
}
fn xdg_config_file() -> Result<PathBuf> {
fn xdg_config_file() -> ConfigResult<PathBuf> {
let xdg_path = etcetera::choose_base_strategy()?
.config_dir()
.join("tree-sitter")
@ -84,7 +130,7 @@ impl Config {
/// [`etcetera::choose_base_strategy`](https://docs.rs/etcetera/*/etcetera/#basestrategy)
/// - `$HOME/.tree-sitter/config.json` as a fallback from where tree-sitter _used_ to store
/// its configuration
pub fn load(path: Option<PathBuf>) -> Result<Self> {
pub fn load(path: Option<PathBuf>) -> ConfigResult<Self> {
let location = if let Some(path) = path {
path
} else if let Some(path) = Self::find_config_file()? {
@ -94,9 +140,9 @@ impl Config {
};
let content = fs::read_to_string(&location)
.with_context(|| format!("Failed to read {}", location.to_string_lossy()))?;
.map_err(|e| ConfigError::IO(IoError::new(e, Some(location.as_path()))))?;
let config = serde_json::from_str(&content)
.with_context(|| format!("Bad JSON config {}", location.to_string_lossy()))?;
.map_err(|e| ConfigError::ConfigRead(location.to_string_lossy().to_string(), e))?;
Ok(Self { location, config })
}
@ -106,7 +152,7 @@ impl Config {
/// disk.
///
/// (Note that this is typically only done by the `tree-sitter init-config` command.)
pub fn initial() -> Result<Self> {
pub fn initial() -> ConfigResult<Self> {
let location = if let Ok(path) = env::var("TREE_SITTER_DIR") {
let mut path = PathBuf::from(path);
path.push("config.json");
@ -119,17 +165,20 @@ impl Config {
}
/// Saves this configuration to the file that it was originally loaded from.
pub fn save(&self) -> Result<()> {
pub fn save(&self) -> ConfigResult<()> {
let json = serde_json::to_string_pretty(&self.config)?;
fs::create_dir_all(self.location.parent().unwrap())?;
fs::write(&self.location, json)?;
let config_dir = self.location.parent().unwrap();
fs::create_dir_all(config_dir)
.map_err(|e| ConfigError::IO(IoError::new(e, Some(config_dir))))?;
fs::write(&self.location, json)
.map_err(|e| ConfigError::IO(IoError::new(e, Some(self.location.as_path()))))?;
Ok(())
}
/// Parses a component-specific configuration from the configuration file. The type `C` must
/// be [deserializable](https://docs.rs/serde/*/serde/trait.Deserialize.html) from a JSON
/// object, and must only include the fields relevant to that component.
pub fn get<C>(&self) -> Result<C>
pub fn get<C>(&self) -> ConfigResult<C>
where
C: for<'de> Deserialize<'de>,
{
@ -140,7 +189,7 @@ impl Config {
/// Adds a component-specific configuration to the configuration file. The type `C` must be
/// [serializable](https://docs.rs/serde/*/serde/trait.Serialize.html) into a JSON object, and
/// must only include the fields relevant to that component.
pub fn add<C>(&mut self, config: C) -> Result<()>
pub fn add<C>(&mut self, config: C) -> ConfigResult<()>
where
C: Serialize,
{