config: add convenient ConfigLayer wrapper that provides .save() method

I'm going to remove write/remove_config_value_to/from_file() functions, but I
don't want to copy layer.path.expect(..) to all callers.
This commit is contained in:
Yuya Nishihara 2024-12-12 23:07:46 +09:00
parent 215c82e975
commit d6ca0c9940
3 changed files with 87 additions and 32 deletions

View file

@ -22,6 +22,7 @@ use std::sync::Arc;
use itertools::Itertools as _; use itertools::Itertools as _;
use jj_lib::backend::BackendError; use jj_lib::backend::BackendError;
use jj_lib::config::ConfigFileSaveError;
use jj_lib::config::ConfigGetError; use jj_lib::config::ConfigGetError;
use jj_lib::config::ConfigLoadError; use jj_lib::config::ConfigLoadError;
use jj_lib::dsl_util::Diagnostics; use jj_lib::dsl_util::Diagnostics;
@ -246,6 +247,12 @@ impl From<ConfigEnvError> for CommandError {
} }
} }
impl From<ConfigFileSaveError> for CommandError {
fn from(err: ConfigFileSaveError) -> Self {
user_error(err)
}
}
impl From<ConfigGetError> for CommandError { impl From<ConfigGetError> for CommandError {
fn from(err: ConfigGetError) -> Self { fn from(err: ConfigGetError) -> Self {
let hint = match &err { let hint = match &err {

View file

@ -22,6 +22,7 @@ use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use itertools::Itertools; use itertools::Itertools;
use jj_lib::config::ConfigFile;
use jj_lib::config::ConfigLayer; use jj_lib::config::ConfigLayer;
use jj_lib::config::ConfigLoadError; use jj_lib::config::ConfigLoadError;
use jj_lib::config::ConfigNamePathBuf; use jj_lib::config::ConfigNamePathBuf;
@ -426,42 +427,17 @@ pub fn parse_config_args(toml_strs: &[ConfigArg]) -> Result<Vec<ConfigLayer>, Co
.try_collect() .try_collect()
} }
fn load_config_file_or_empty(
source: ConfigSource,
path: &Path,
) -> Result<ConfigLayer, ConfigLoadError> {
match ConfigLayer::load_from_file(source, path.into()) {
Ok(layer) => Ok(layer),
Err(ConfigLoadError::Read(err)) if err.error.kind() == std::io::ErrorKind::NotFound => {
// If config doesn't exist yet, read as empty and we'll write one.
let mut layer = ConfigLayer::empty(source);
layer.path = Some(path.into());
Ok(layer)
}
Err(err) => Err(err),
}
}
fn write_config(path: &Path, doc: &toml_edit::DocumentMut) -> Result<(), CommandError> {
std::fs::write(path, doc.to_string()).map_err(|err| {
user_error_with_message(
format!("Failed to write file {path}", path = path.display()),
err,
)
})
}
pub fn write_config_value_to_file( pub fn write_config_value_to_file(
key: &ConfigNamePathBuf, key: &ConfigNamePathBuf,
value: toml_edit::Value, value: toml_edit::Value,
path: &Path, path: &Path,
) -> Result<(), CommandError> { ) -> Result<(), CommandError> {
// TODO: Load config layer by caller. Here we use a dummy source for now. // TODO: Load config layer by caller. Here we use a dummy source for now.
let mut layer = load_config_file_or_empty(ConfigSource::User, path)?; let mut file = ConfigFile::load_or_empty(ConfigSource::User, path)?;
layer file.set_value(key, value)
.set_value(key, value)
.map_err(|err| user_error_with_message(format!("Failed to set {key}"), err))?; .map_err(|err| user_error_with_message(format!("Failed to set {key}"), err))?;
write_config(path, &layer.data) file.save()?;
Ok(())
} }
pub fn remove_config_value_from_file( pub fn remove_config_value_from_file(
@ -469,14 +445,15 @@ pub fn remove_config_value_from_file(
path: &Path, path: &Path,
) -> Result<(), CommandError> { ) -> Result<(), CommandError> {
// TODO: Load config layer by caller. Here we use a dummy source for now. // TODO: Load config layer by caller. Here we use a dummy source for now.
let mut layer = load_config_file_or_empty(ConfigSource::User, path)?; let mut file = ConfigFile::load_or_empty(ConfigSource::User, path)?;
let old_value = layer let old_value = file
.delete_value(key) .delete_value(key)
.map_err(|err| user_error_with_message(format!("Failed to unset {key}"), err))?; .map_err(|err| user_error_with_message(format!("Failed to unset {key}"), err))?;
if old_value.is_none() { if old_value.is_none() {
return Err(user_error(format!(r#""{key}" doesn't exist"#))); return Err(user_error(format!(r#""{key}" doesn't exist"#)));
} }
write_config(path, &layer.data) file.save()?;
Ok(())
} }
/// Command name and arguments specified by config. /// Command name and arguments specified by config.

View file

@ -18,6 +18,7 @@ use std::borrow::Borrow;
use std::convert::Infallible; use std::convert::Infallible;
use std::fmt; use std::fmt;
use std::fs; use std::fs;
use std::io;
use std::ops::Range; use std::ops::Range;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
@ -58,6 +59,11 @@ pub enum ConfigLoadError {
}, },
} }
/// Error that can occur when saving config variables to file.
#[derive(Debug, Error)]
#[error("Failed to write configuration file")]
pub struct ConfigFileSaveError(#[source] pub PathError);
/// Error that can occur when looking up config variable. /// Error that can occur when looking up config variable.
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ConfigGetError { pub enum ConfigGetError {
@ -467,6 +473,71 @@ fn ensure_parent_table<'a, 'b>(
Ok((parent_table, leaf_key)) Ok((parent_table, leaf_key))
} }
/// Wrapper for file-based [`ConfigLayer`], providing convenient methods for
/// modification.
#[derive(Debug)]
pub struct ConfigFile {
layer: ConfigLayer,
}
impl ConfigFile {
/// Loads TOML file from the specified `path` if exists. Returns an empty
/// object if the file doesn't exist.
pub fn load_or_empty(
source: ConfigSource,
path: impl Into<PathBuf>,
) -> Result<Self, ConfigLoadError> {
let layer = match ConfigLayer::load_from_file(source, path.into()) {
Ok(layer) => layer,
Err(ConfigLoadError::Read(PathError { path, error }))
if error.kind() == io::ErrorKind::NotFound =>
{
ConfigLayer {
source,
path: Some(path),
data: DocumentMut::new(),
}
}
Err(err) => return Err(err),
};
Ok(ConfigFile { layer })
}
/// Writes serialized data to the source file.
pub fn save(&self) -> Result<(), ConfigFileSaveError> {
fs::write(self.path(), self.layer.data.to_string())
.context(self.path())
.map_err(ConfigFileSaveError)
}
/// Source file path.
pub fn path(&self) -> &Path {
self.layer.path.as_ref().expect("path must be known")
}
/// Returns the underlying config layer.
pub fn layer(&self) -> &ConfigLayer {
&self.layer
}
/// See [`ConfigLayer::set_value()`].
pub fn set_value(
&mut self,
name: impl ToConfigNamePath,
new_value: impl Into<ConfigValue>,
) -> Result<Option<ConfigValue>, ConfigUpdateError> {
self.layer.set_value(name, new_value)
}
/// See [`ConfigLayer::delete_value()`].
pub fn delete_value(
&mut self,
name: impl ToConfigNamePath,
) -> Result<Option<ConfigValue>, ConfigUpdateError> {
self.layer.delete_value(name)
}
}
/// Stack of configuration layers which can be merged as needed. /// Stack of configuration layers which can be merged as needed.
/// ///
/// A [`StackedConfig`] is something like a read-only `overlayfs`. Tables and /// A [`StackedConfig`] is something like a read-only `overlayfs`. Tables and