1366 lines · 45.3 KB · Rust
RawDownload
// 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,    /// 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",            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,    ) -> Result<&mut ConfigTableLike<'_>, ConfigUpdateError> {        let would_overwrite_table = |name| ConfigUpdateError::WouldOverwriteValue { name };        let name = name.into_name_path();

Lines 1–500 of 1366.