// Copyright 2022 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. //! Configuration store helpers. use std::borrow::Borrow;use std::convert::Infallible;use std::fmt;use std::fmt::Display;use std::fs;use std::io;use std::ops::Range;use std::path::Path;use std::path::PathBuf;use std::slice;use std::str::FromStr;use std::sync::Arc;use std::sync::LazyLock; use itertools::Itertools as _;use serde::Deserialize;use serde::de::IntoDeserializer as _;use thiserror::Error;use toml_edit::Document;use toml_edit::DocumentMut; pub use crate::config_resolver::ConfigMigrateError;pub use crate::config_resolver::ConfigMigrateLayerError;pub use crate::config_resolver::ConfigMigrationRule;pub use crate::config_resolver::ConfigResolutionContext;pub use crate::config_resolver::migrate;pub use crate::config_resolver::resolve;use crate::file_util::IoResultExt as _;use crate::file_util::PathError; /// Config value or table node.pub type ConfigItem = toml_edit::Item;/// Non-inline table of config key and value pairs.pub type ConfigTable = toml_edit::Table;/// Non-inline or inline table of config key and value pairs.pub type ConfigTableLike<'a> = dyn toml_edit::TableLike + 'a;/// Generic config value.pub type ConfigValue = toml_edit::Value; /// Error that can occur when parsing or loading config variables.#[derive(Debug, Error)]pub enum ConfigLoadError { /// Config file or directory cannot be read. #[error("Failed to read configuration file")] Read(#[source] PathError), /// TOML file or text cannot be parsed. #[error("Configuration cannot be parsed as TOML document")] Parse { /// Source error. #[source] error: Box<toml_edit::TomlError>, /// Source file path. source_path: Option<PathBuf>, },} /// 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.#[derive(Debug, Error)]pub enum ConfigGetError { /// Config value is not set. #[error("Value not found for {name}")] NotFound { /// Dotted config name path. name: String, }, /// Config value cannot be converted to the expected type. #[error("Invalid type or value for {name}")] Type { /// Dotted config name path. name: String, /// Source error. #[source] error: Box<dyn std::error::Error + Send + Sync>, /// Source file path where the value is defined. source_path: Option<PathBuf>, },} /// Error that can occur when updating config variable.#[derive(Debug, Error)]pub enum ConfigUpdateError { /// Non-table value exists at parent path, which shouldn't be removed. #[error("Would overwrite non-table value with parent table {name}")] WouldOverwriteValue { /// Dotted config name path. name: String, }, /// Non-inline table exists at the path, which shouldn't be overwritten by a /// value. #[error("Would overwrite entire table {name}")] WouldOverwriteTable { /// Dotted config name path. name: String, }, /// Non-inline table exists at the path, which shouldn't be deleted. #[error("Would delete entire table {name}")] WouldDeleteTable { /// Dotted config name path. name: String, },} /// Extension methods for `Result<T, ConfigGetError>`.pub trait ConfigGetResultExt<T> { /// Converts `NotFound` error to `Ok(None)`, leaving other errors. fn optional(self) -> Result<Option<T>, ConfigGetError>;} impl<T> ConfigGetResultExt<T> for Result<T, ConfigGetError> { fn optional(self) -> Result<Option<T>, ConfigGetError> { match self { Ok(value) => Ok(Some(value)), Err(ConfigGetError::NotFound { .. }) => Ok(None), Err(err) => Err(err), } }} /// Dotted config name path.#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]pub struct ConfigNamePathBuf(Vec<toml_edit::Key>); impl ConfigNamePathBuf { /// Creates an empty path pointing to the root table. /// /// This isn't a valid TOML key expression, but provided for convenience. pub fn root() -> Self { Self(vec![]) } /// Returns true if the path is empty (i.e. pointing to the root table.) pub fn is_root(&self) -> bool { self.0.is_empty() } /// Returns true if the `base` is a prefix of this path. pub fn starts_with(&self, base: impl AsRef<[toml_edit::Key]>) -> bool { self.0.starts_with(base.as_ref()) } /// Returns iterator of path components (or keys.) pub fn components(&self) -> slice::Iter<'_, toml_edit::Key> { self.0.iter() } /// Appends the given `key` component. pub fn push(&mut self, key: impl Into<toml_edit::Key>) { self.0.push(key.into()); }} // Help obtain owned value from ToConfigNamePath::Output. If we add a slice// type (like &Path for PathBuf), this will be From<&ConfigNamePath>.impl From<&Self> for ConfigNamePathBuf { fn from(value: &Self) -> Self { value.clone() }} impl<K: Into<toml_edit::Key>> FromIterator<K> for ConfigNamePathBuf { fn from_iter<I: IntoIterator<Item = K>>(iter: I) -> Self { let keys = iter.into_iter().map(|k| k.into()).collect(); Self(keys) }} impl FromStr for ConfigNamePathBuf { type Err = toml_edit::TomlError; fn from_str(s: &str) -> Result<Self, Self::Err> { // TOML parser ensures that the returned vec is not empty. toml_edit::Key::parse(s).map(ConfigNamePathBuf) }} impl AsRef<[toml_edit::Key]> for ConfigNamePathBuf { fn as_ref(&self) -> &[toml_edit::Key] { &self.0 }} impl Display for ConfigNamePathBuf { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut components = self.0.iter().fuse(); if let Some(key) = components.next() { write!(f, "{key}")?; } components.try_for_each(|key| write!(f, ".{key}")) }} /// Value that can be converted to a dotted config name path.////// This is an abstraction to specify a config name path in either a string or a/// parsed form. It's similar to `Into<T>`, but the output type `T` is/// constrained by the source type.pub trait ToConfigNamePath: Sized { /// Path type to be converted from `Self`. type Output: Borrow<ConfigNamePathBuf> + Into<ConfigNamePathBuf>; /// Converts this object into a dotted config name path. fn into_name_path(self) -> Self::Output;} impl ToConfigNamePath for ConfigNamePathBuf { type Output = Self; fn into_name_path(self) -> Self::Output { self }} impl ToConfigNamePath for &ConfigNamePathBuf { type Output = Self; fn into_name_path(self) -> Self::Output { self }} impl ToConfigNamePath for &'static str { // This can be changed to ConfigNamePathStr(str) if allocation cost matters. type Output = ConfigNamePathBuf; /// Parses this string into a dotted config name path. /// /// The string must be a valid TOML dotted key. A static str is required to /// prevent API misuse. fn into_name_path(self) -> Self::Output { self.parse() .expect("valid TOML dotted key must be provided") }} impl<const N: usize> ToConfigNamePath for [&str; N] { type Output = ConfigNamePathBuf; fn into_name_path(self) -> Self::Output { self.into_iter().collect() }} impl<const N: usize> ToConfigNamePath for &[&str; N] { type Output = ConfigNamePathBuf; fn into_name_path(self) -> Self::Output { self.as_slice().into_name_path() }} impl ToConfigNamePath for &[&str] { type Output = ConfigNamePathBuf; fn into_name_path(self) -> Self::Output { self.iter().copied().collect() }} /// Source of configuration variables in order of precedence.#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]pub enum ConfigSource { /// Default values (which has the lowest precedence.) Default, /// System configuration files. System, /// Base environment variables. EnvBase, /// User configuration files. User, /// Repo configuration files. Repo, /// Workspace configuration files. Workspace, /// Override environment variables. EnvOverrides, /// Command-line arguments (which has the highest precedence.) CommandArg,} impl Display for ConfigSource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use ConfigSource::*; let c = match self { Default => "default", System => "system", User => "user", Repo => "repo", Workspace => "workspace", CommandArg => "cli", EnvBase | EnvOverrides => "env", }; write!(f, "{c}") }} /// Set of configuration variables with source information.#[derive(Clone, Debug)]pub struct ConfigLayer { /// Source type of this layer. pub source: ConfigSource, /// Source file path of this layer if any. pub path: Option<PathBuf>, /// Configuration variables. pub data: DocumentMut,} impl ConfigLayer { /// Creates new layer with empty data. pub fn empty(source: ConfigSource) -> Self { Self::with_data(source, DocumentMut::new()) } /// Creates new layer with the configuration variables `data`. pub fn with_data(source: ConfigSource, data: DocumentMut) -> Self { Self { source, path: None, data, } } /// Parses TOML document `text` into new layer. pub fn parse(source: ConfigSource, text: &str) -> Result<Self, ConfigLoadError> { let data = Document::parse(text).map_err(|error| ConfigLoadError::Parse { error: Box::new(error), source_path: None, })?; Ok(Self::with_data(source, data.into_mut())) } /// Loads TOML file from the specified `path`. pub fn load_from_file(source: ConfigSource, path: PathBuf) -> Result<Self, ConfigLoadError> { let text = fs::read_to_string(&path) .context(&path) .map_err(ConfigLoadError::Read)?; let data = Document::parse(text).map_err(|error| ConfigLoadError::Parse { error: Box::new(error), source_path: Some(path.clone()), })?; Ok(Self { source, path: Some(path), data: data.into_mut(), }) } fn load_from_dir(source: ConfigSource, path: &Path) -> Result<Vec<Self>, ConfigLoadError> { // TODO: Walk the directory recursively? let mut file_paths: Vec<_> = path .read_dir() .and_then(|dir_entries| { dir_entries .map(|entry| Ok(entry?.path())) .filter_ok(|path| path.is_file() && path.extension() == Some("toml".as_ref())) .try_collect() }) .context(path) .map_err(ConfigLoadError::Read)?; file_paths.sort_unstable(); file_paths .into_iter() .map(|path| Self::load_from_file(source, path)) .try_collect() } /// Returns true if the table has no configuration variables. pub fn is_empty(&self) -> bool { self.data.is_empty() } // Add .get_value(name) if needed. look_up_*() are low-level API. /// Looks up sub table by the `name` path. Returns `Some(table)` if a table /// was found at the path. Returns `Err(item)` if middle or leaf node wasn't /// a table. pub fn look_up_table( &self, name: impl ToConfigNamePath, ) -> Result<Option<&ConfigTableLike<'_>>, &ConfigItem> { match self.look_up_item(name) { Ok(Some(item)) => match item.as_table_like() { Some(table) => Ok(Some(table)), None => Err(item), }, Ok(None) => Ok(None), Err(item) => Err(item), } } /// Looks up item by the `name` path. Returns `Some(item)` if an item /// found at the path. Returns `Err(item)` if middle node wasn't a table. pub fn look_up_item( &self, name: impl ToConfigNamePath, ) -> Result<Option<&ConfigItem>, &ConfigItem> { look_up_item(self.data.as_item(), name.into_name_path().borrow()) } /// Sets `new_value` to the `name` path. Returns old value if any. /// /// This function errors out if attempted to overwrite a non-table middle /// node or a leaf non-inline table. An inline table can be overwritten /// because it's syntactically a value. pub fn set_value( &mut self, name: impl ToConfigNamePath, new_value: impl Into<ConfigValue>, ) -> Result<Option<ConfigValue>, ConfigUpdateError> { let would_overwrite_table = |name| ConfigUpdateError::WouldOverwriteValue { name }; let name = name.into_name_path(); let name = name.borrow(); let (leaf_key, table_keys) = name .0 .split_last() .ok_or_else(|| would_overwrite_table(name.to_string()))?; let parent_table = ensure_table(self.data.as_table_mut(), table_keys) .map_err(|keys| would_overwrite_table(keys.join(".")))?; match parent_table.entry_format(leaf_key) { toml_edit::Entry::Occupied(mut entry) => { if !entry.get().is_value() { return Err(ConfigUpdateError::WouldOverwriteTable { name: name.to_string(), }); } let old_item = entry.insert(toml_edit::value(new_value)); Ok(Some(old_item.into_value().unwrap())) } toml_edit::Entry::Vacant(entry) => { entry.insert(toml_edit::value(new_value)); // Reset whitespace formatting (i.e. insert space before '=') let mut new_key = parent_table.key_mut(leaf_key).unwrap(); new_key.leaf_decor_mut().clear(); Ok(None) } } } /// Deletes value specified by the `name` path. Returns old value if any. /// /// Returns `Ok(None)` if middle node wasn't a table or a value wasn't /// found. Returns `Err` if attempted to delete a non-inline table. An /// inline table can be deleted because it's syntactically a value. pub fn delete_value( &mut self, name: impl ToConfigNamePath, ) -> Result<Option<ConfigValue>, ConfigUpdateError> { let would_delete_table = |name| ConfigUpdateError::WouldDeleteTable { name }; let name = name.into_name_path(); let name = name.borrow(); let mut keys = name.components(); let leaf_key = keys .next_back() .ok_or_else(|| would_delete_table(name.to_string()))?; let Some(parent_table) = keys.try_fold( self.data.as_table_mut() as &mut ConfigTableLike, |table, key| table.get_mut(key)?.as_table_like_mut(), ) else { return Ok(None); }; match parent_table.entry(leaf_key) { toml_edit::Entry::Occupied(entry) => { if !entry.get().is_value() { return Err(would_delete_table(name.to_string())); } let old_item = entry.remove(); Ok(Some(old_item.into_value().unwrap())) } toml_edit::Entry::Vacant(_) => Ok(None), } } /// Inserts tables down to the `name` path. Returns mutable reference to the /// leaf table. /// /// This function errors out if attempted to overwrite a non-table node. In /// file-system analogy, this is equivalent to `std::fs::create_dir_all()`. pub fn ensure_table( &mut self, name: impl ToConfigNamePath,Lines 1–500 of 1381. Load more
// Copyright 2022 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Configuration store helpers. use std::borrow::Borrow; use std::convert::Infallible; use std::fmt; use std::fmt::Display; use std::fs; use std::io; use std::ops::Range; use std::path::Path; use std::path::PathBuf; use std::slice; use std::str::FromStr; use std::sync::Arc; use std::sync::LazyLock; use itertools::Itertools as _; use serde::Deserialize; use serde::de::IntoDeserializer as _; use thiserror::Error; use toml_edit::Document; use toml_edit::DocumentMut; pub use crate::config_resolver::ConfigMigrateError; pub use crate::config_resolver::ConfigMigrateLayerError; pub use crate::config_resolver::ConfigMigrationRule; pub use crate::config_resolver::ConfigResolutionContext; pub use crate::config_resolver::migrate; pub use crate::config_resolver::resolve; use crate::file_util::IoResultExt as _; use crate::file_util::PathError; /// Config value or table node. pub type ConfigItem = toml_edit::Item; /// Non-inline table of config key and value pairs. pub type ConfigTable = toml_edit::Table; /// Non-inline or inline table of config key and value pairs. pub type ConfigTableLike<'a> = dyn toml_edit::TableLike + 'a; /// Generic config value. pub type ConfigValue = toml_edit::Value; /// Error that can occur when parsing or loading config variables. #[derive(Debug, Error)] pub enum ConfigLoadError { /// Config file or directory cannot be read. #[error("Failed to read configuration file")] Read(#[source] PathError), /// TOML file or text cannot be parsed. #[error("Configuration cannot be parsed as TOML document")] Parse { /// Source error. #[source] error: Box<toml_edit::TomlError>, /// Source file path. source_path: Option<PathBuf>, }, } /// 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. #[derive(Debug, Error)] pub enum ConfigGetError { /// Config value is not set. #[error("Value not found for {name}")] NotFound { /// Dotted config name path. name: String, }, /// Config value cannot be converted to the expected type. #[error("Invalid type or value for {name}")] Type { /// Dotted config name path. name: String, /// Source error. #[source] error: Box<dyn std::error::Error + Send + Sync>, /// Source file path where the value is defined. source_path: Option<PathBuf>, }, } /// Error that can occur when updating config variable. #[derive(Debug, Error)] pub enum ConfigUpdateError { /// Non-table value exists at parent path, which shouldn't be removed. #[error("Would overwrite non-table value with parent table {name}")] WouldOverwriteValue { /// Dotted config name path. name: String, }, /// Non-inline table exists at the path, which shouldn't be overwritten by a /// value. #[error("Would overwrite entire table {name}")] WouldOverwriteTable { /// Dotted config name path. name: String, }, /// Non-inline table exists at the path, which shouldn't be deleted. #[error("Would delete entire table {name}")] WouldDeleteTable { /// Dotted config name path. name: String, }, } /// Extension methods for `Result<T, ConfigGetError>`. pub trait ConfigGetResultExt<T> { /// Converts `NotFound` error to `Ok(None)`, leaving other errors. fn optional(self) -> Result<Option<T>, ConfigGetError>; } impl<T> ConfigGetResultExt<T> for Result<T, ConfigGetError> { fn optional(self) -> Result<Option<T>, ConfigGetError> { match self { Ok(value) => Ok(Some(value)), Err(ConfigGetError::NotFound { .. }) => Ok(None), Err(err) => Err(err), } } } /// Dotted config name path. #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct ConfigNamePathBuf(Vec<toml_edit::Key>); impl ConfigNamePathBuf { /// Creates an empty path pointing to the root table. /// /// This isn't a valid TOML key expression, but provided for convenience. pub fn root() -> Self { Self(vec![]) } /// Returns true if the path is empty (i.e. pointing to the root table.) pub fn is_root(&self) -> bool { self.0.is_empty() } /// Returns true if the `base` is a prefix of this path. pub fn starts_with(&self, base: impl AsRef<[toml_edit::Key]>) -> bool { self.0.starts_with(base.as_ref()) } /// Returns iterator of path components (or keys.) pub fn components(&self) -> slice::Iter<'_, toml_edit::Key> { self.0.iter() } /// Appends the given `key` component. pub fn push(&mut self, key: impl Into<toml_edit::Key>) { self.0.push(key.into()); } } // Help obtain owned value from ToConfigNamePath::Output. If we add a slice // type (like &Path for PathBuf), this will be From<&ConfigNamePath>. impl From<&Self> for ConfigNamePathBuf { fn from(value: &Self) -> Self { value.clone() } } impl<K: Into<toml_edit::Key>> FromIterator<K> for ConfigNamePathBuf { fn from_iter<I: IntoIterator<Item = K>>(iter: I) -> Self { let keys = iter.into_iter().map(|k| k.into()).collect(); Self(keys) } } impl FromStr for ConfigNamePathBuf { type Err = toml_edit::TomlError; fn from_str(s: &str) -> Result<Self, Self::Err> { // TOML parser ensures that the returned vec is not empty. toml_edit::Key::parse(s).map(ConfigNamePathBuf) } } impl AsRef<[toml_edit::Key]> for ConfigNamePathBuf { fn as_ref(&self) -> &[toml_edit::Key] { &self.0 } } impl Display for ConfigNamePathBuf { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut components = self.0.iter().fuse(); if let Some(key) = components.next() { write!(f, "{key}")?; } components.try_for_each(|key| write!(f, ".{key}")) } } /// Value that can be converted to a dotted config name path. /// /// This is an abstraction to specify a config name path in either a string or a /// parsed form. It's similar to `Into<T>`, but the output type `T` is /// constrained by the source type. pub trait ToConfigNamePath: Sized { /// Path type to be converted from `Self`. type Output: Borrow<ConfigNamePathBuf> + Into<ConfigNamePathBuf>; /// Converts this object into a dotted config name path. fn into_name_path(self) -> Self::Output; } impl ToConfigNamePath for ConfigNamePathBuf { type Output = Self; fn into_name_path(self) -> Self::Output { self } } impl ToConfigNamePath for &ConfigNamePathBuf { type Output = Self; fn into_name_path(self) -> Self::Output { self } } impl ToConfigNamePath for &'static str { // This can be changed to ConfigNamePathStr(str) if allocation cost matters. type Output = ConfigNamePathBuf; /// Parses this string into a dotted config name path. /// /// The string must be a valid TOML dotted key. A static str is required to /// prevent API misuse. fn into_name_path(self) -> Self::Output { self.parse() .expect("valid TOML dotted key must be provided") } } impl<const N: usize> ToConfigNamePath for [&str; N] { type Output = ConfigNamePathBuf; fn into_name_path(self) -> Self::Output { self.into_iter().collect() } } impl<const N: usize> ToConfigNamePath for &[&str; N] { type Output = ConfigNamePathBuf; fn into_name_path(self) -> Self::Output { self.as_slice().into_name_path() } } impl ToConfigNamePath for &[&str] { type Output = ConfigNamePathBuf; fn into_name_path(self) -> Self::Output { self.iter().copied().collect() } } /// Source of configuration variables in order of precedence. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum ConfigSource { /// Default values (which has the lowest precedence.) Default, /// System configuration files. System, /// Base environment variables. EnvBase, /// User configuration files. User, /// Repo configuration files. Repo, /// Workspace configuration files. Workspace, /// Override environment variables. EnvOverrides, /// Command-line arguments (which has the highest precedence.) CommandArg, } impl Display for ConfigSource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use ConfigSource::*; let c = match self { Default => "default", System => "system", User => "user", Repo => "repo", Workspace => "workspace", CommandArg => "cli", EnvBase | EnvOverrides => "env", }; write!(f, "{c}") } } /// Set of configuration variables with source information. #[derive(Clone, Debug)] pub struct ConfigLayer { /// Source type of this layer. pub source: ConfigSource, /// Source file path of this layer if any. pub path: Option<PathBuf>, /// Configuration variables. pub data: DocumentMut, } impl ConfigLayer { /// Creates new layer with empty data. pub fn empty(source: ConfigSource) -> Self { Self::with_data(source, DocumentMut::new()) } /// Creates new layer with the configuration variables `data`. pub fn with_data(source: ConfigSource, data: DocumentMut) -> Self { Self { source, path: None, data, } } /// Parses TOML document `text` into new layer. pub fn parse(source: ConfigSource, text: &str) -> Result<Self, ConfigLoadError> { let data = Document::parse(text).map_err(|error| ConfigLoadError::Parse { error: Box::new(error), source_path: None, })?; Ok(Self::with_data(source, data.into_mut())) } /// Loads TOML file from the specified `path`. pub fn load_from_file(source: ConfigSource, path: PathBuf) -> Result<Self, ConfigLoadError> { let text = fs::read_to_string(&path) .context(&path) .map_err(ConfigLoadError::Read)?; let data = Document::parse(text).map_err(|error| ConfigLoadError::Parse { error: Box::new(error), source_path: Some(path.clone()), })?; Ok(Self { source, path: Some(path), data: data.into_mut(), }) } fn load_from_dir(source: ConfigSource, path: &Path) -> Result<Vec<Self>, ConfigLoadError> { // TODO: Walk the directory recursively? let mut file_paths: Vec<_> = path .read_dir() .and_then(|dir_entries| { dir_entries .map(|entry| Ok(entry?.path())) .filter_ok(|path| path.is_file() && path.extension() == Some("toml".as_ref())) .try_collect() }) .context(path) .map_err(ConfigLoadError::Read)?; file_paths.sort_unstable(); file_paths .into_iter() .map(|path| Self::load_from_file(source, path)) .try_collect() } /// Returns true if the table has no configuration variables. pub fn is_empty(&self) -> bool { self.data.is_empty() } // Add .get_value(name) if needed. look_up_*() are low-level API. /// Looks up sub table by the `name` path. Returns `Some(table)` if a table /// was found at the path. Returns `Err(item)` if middle or leaf node wasn't /// a table. pub fn look_up_table( &self, name: impl ToConfigNamePath, ) -> Result<Option<&ConfigTableLike<'_>>, &ConfigItem> { match self.look_up_item(name) { Ok(Some(item)) => match item.as_table_like() { Some(table) => Ok(Some(table)), None => Err(item), }, Ok(None) => Ok(None), Err(item) => Err(item), } } /// Looks up item by the `name` path. Returns `Some(item)` if an item /// found at the path. Returns `Err(item)` if middle node wasn't a table. pub fn look_up_item( &self, name: impl ToConfigNamePath, ) -> Result<Option<&ConfigItem>, &ConfigItem> { look_up_item(self.data.as_item(), name.into_name_path().borrow()) } /// Sets `new_value` to the `name` path. Returns old value if any. /// /// This function errors out if attempted to overwrite a non-table middle /// node or a leaf non-inline table. An inline table can be overwritten /// because it's syntactically a value. pub fn set_value( &mut self, name: impl ToConfigNamePath, new_value: impl Into<ConfigValue>, ) -> Result<Option<ConfigValue>, ConfigUpdateError> { let would_overwrite_table = |name| ConfigUpdateError::WouldOverwriteValue { name }; let name = name.into_name_path(); let name = name.borrow(); let (leaf_key, table_keys) = name .0 .split_last() .ok_or_else(|| would_overwrite_table(name.to_string()))?; let parent_table = ensure_table(self.data.as_table_mut(), table_keys) .map_err(|keys| would_overwrite_table(keys.join(".")))?; match parent_table.entry_format(leaf_key) { toml_edit::Entry::Occupied(mut entry) => { if !entry.get().is_value() { return Err(ConfigUpdateError::WouldOverwriteTable { name: name.to_string(), }); } let old_item = entry.insert(toml_edit::value(new_value)); Ok(Some(old_item.into_value().unwrap())) } toml_edit::Entry::Vacant(entry) => { entry.insert(toml_edit::value(new_value)); // Reset whitespace formatting (i.e. insert space before '=') let mut new_key = parent_table.key_mut(leaf_key).unwrap(); new_key.leaf_decor_mut().clear(); Ok(None) } } } /// Deletes value specified by the `name` path. Returns old value if any. /// /// Returns `Ok(None)` if middle node wasn't a table or a value wasn't /// found. Returns `Err` if attempted to delete a non-inline table. An /// inline table can be deleted because it's syntactically a value. pub fn delete_value( &mut self, name: impl ToConfigNamePath, ) -> Result<Option<ConfigValue>, ConfigUpdateError> { let would_delete_table = |name| ConfigUpdateError::WouldDeleteTable { name }; let name = name.into_name_path(); let name = name.borrow(); let mut keys = name.components(); let leaf_key = keys .next_back() .ok_or_else(|| would_delete_table(name.to_string()))?; let Some(parent_table) = keys.try_fold( self.data.as_table_mut() as &mut ConfigTableLike, |table, key| table.get_mut(key)?.as_table_like_mut(), ) else { return Ok(None); }; match parent_table.entry(leaf_key) { toml_edit::Entry::Occupied(entry) => { if !entry.get().is_value() { return Err(would_delete_table(name.to_string())); } let old_item = entry.remove(); Ok(Some(old_item.into_value().unwrap())) } toml_edit::Entry::Vacant(_) => Ok(None), } } /// Inserts tables down to the `name` path. Returns mutable reference to the /// leaf table. /// /// This function errors out if attempted to overwrite a non-table node. In /// file-system analogy, this is equivalent to `std::fs::create_dir_all()`. pub fn ensure_table( &mut self, name: impl ToConfigNamePath,