Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle src profile config.rs
use std::io::Write;
use std::path::{Path, PathBuf};
use std::{fmt, fs, io};

use serde::Serialize as _;
use serde_json as json;
use thiserror::Error;

use crate::explorer::Explorer;
use crate::node::Alias;
use crate::node::config::DefaultSeedingPolicy;
use crate::node::policy::{Policy, Scope};
use crate::{cli, node, web};

#[derive(Debug, Error)]
#[error("writing configuration to {path:?} failed: {kind}")]
pub struct WriteError {
    path: PathBuf,
    kind: WriteErrorKind,
}

impl WriteError {
    fn open_file<P>(path: P, err: io::Error) -> Self
    where
        P: AsRef<Path>,
    {
        Self {
            path: path.as_ref().to_path_buf(),
            kind: WriteErrorKind::OpenFile { err },
        }
    }

    fn to_json<P>(path: P, err: serde_json::Error) -> Self
    where
        P: AsRef<Path>,
    {
        Self {
            path: path.as_ref().to_path_buf(),
            kind: WriteErrorKind::ToJson { err },
        }
    }

    fn write_file<P>(path: P, err: io::Error) -> Self
    where
        P: AsRef<Path>,
    {
        Self {
            path: path.as_ref().to_path_buf(),
            kind: WriteErrorKind::WriteFile { err },
        }
    }

    fn validate<P>(path: P, err: serde_json::Error) -> Self
    where
        P: AsRef<Path>,
    {
        Self {
            path: path.as_ref().to_path_buf(),
            kind: WriteErrorKind::Validate { err },
        }
    }

    fn serialize_json<P>(path: P, err: serde_json::Error) -> Self
    where
        P: AsRef<Path>,
    {
        Self {
            path: path.as_ref().to_path_buf(),
            kind: WriteErrorKind::SerializeJson { err },
        }
    }
}

#[derive(Debug, Error)]
pub enum WriteErrorKind {
    #[error("could not open due to {err}")]
    OpenFile {
        #[source]
        err: io::Error,
    },
    #[error("could not convert to JSON due to {err}")]
    ToJson {
        #[source]
        err: json::Error,
    },
    #[error("could not write to file due to {err}")]
    WriteFile {
        #[source]
        err: io::Error,
    },
    #[error("validation failure due to {err}")]
    Validate {
        #[source]
        err: serde_json::Error,
    },
    #[error("could not serialize due to {err}")]
    SerializeJson {
        #[source]
        err: serde_json::Error,
    },
}

#[derive(Debug, Error)]
pub enum InitError {
    #[error("failed to initialize configuration file {path:?}: {err}")]
    Write {
        path: PathBuf,
        #[source]
        err: WriteError,
    },
}

#[derive(Debug, Error)]
pub enum LoadError {
    #[error(
        "failed to open configuration file {path:?}: {err}, perhaps you need to initialise one `rad config init --alias <alias>`"
    )]
    File {
        path: PathBuf,
        #[source]
        err: io::Error,
    },
    #[error("failed to parse JSON of configuration file {path:?}: {err}")]
    Json {
        path: PathBuf,
        #[source]
        err: serde_json::Error,
    },
}

/// Local Radicle configuration.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Config {
    /// Public explorer. This is used for generating links.
    #[serde(default)]
    pub public_explorer: Explorer,
    /// Preferred seeds. These seeds will be used for explorer links
    /// and in other situations when a seed needs to be chosen.
    #[serde(default)]
    pub preferred_seeds: Vec<node::config::ConnectAddress>,
    /// Web configuration.
    #[serde(default)]
    pub web: web::Config,
    /// CLI configuration.
    #[serde(default)]
    pub cli: cli::Config,
    /// Node configuration.
    pub node: node::Config,
}

impl Config {
    /// Create a new, default configuration.
    pub fn new(alias: Alias) -> Self {
        let node = node::Config::new(alias);

        Self {
            public_explorer: Explorer::default(),
            preferred_seeds: node.network.public_seeds(),
            web: web::Config::default(),
            cli: cli::Config::default(),
            node,
        }
    }

    /// Initialize a new configuration. Fails if the path already exists.
    pub fn init(alias: Alias, path: &Path) -> Result<Self, InitError> {
        let cfg = Config::new(alias);
        cfg.write(path).map_err(|err| InitError::Write {
            path: path.to_path_buf(),
            err,
        })?;
        Ok(cfg)
    }

    /// Load a configuration from the given path.
    pub fn load(path: &Path) -> Result<Self, LoadError> {
        let file = fs::File::open(path).map_err(|err| LoadError::File {
            path: path.to_path_buf(),
            err,
        })?;
        let mut cfg: Self = json::from_reader(file).map_err(|err| LoadError::Json {
            path: path.to_path_buf(),
            err,
        })?;

        // Handle deprecated policy configuration.
        // Nb. This will override "seedingPolicy" if set! This code should be removed after 1.0.
        if let (Some(p), Some(s)) = (cfg.node.extra.get("policy"), cfg.node.extra.get("scope")) {
            if let (Ok(policy), Ok(scope)) = (
                json::from_value::<Policy>(p.clone()),
                json::from_value::<Scope>(s.clone()),
            ) {
                log::warn!(target: "radicle", "Overwriting `seedingPolicy` configuration");
                cfg.node.seeding_policy = match policy {
                    Policy::Allow => DefaultSeedingPolicy::Allow {
                        scope: node::config::Scope::explicit(scope),
                    },
                    Policy::Block => DefaultSeedingPolicy::Block,
                }
            }
        }
        Ok(cfg)
    }

    /// Write configuration to disk.
    pub fn write(&self, path: &Path) -> Result<(), WriteError> {
        let contents = json::to_vec_pretty(self).map_err(|err| WriteError::to_json(path, err))?;
        fs::write(path, contents).map_err(|err| WriteError::write_file(path, err))
    }

    /// Get the user alias.
    pub fn alias(&self) -> &Alias {
        &self.node.alias
    }
}

/// Offers utility functions for editing the configuration. Validates on write.
#[derive(Debug, Clone)]
#[deprecated]
pub struct RawConfig(json::Value);

#[derive(Debug, Error)]
#[allow(deprecated)]
pub enum ModifyError {
    #[error("the path provided was empty")]
    EmptyPath,
    #[error("could not find an element at the path '{path}'")]
    NotFound { path: ConfigPath },
    #[error("the element at the path '{path}' is not a JSON object")]
    NotObject { path: ConfigPath },
    #[error("the element at the path '{path}' is not a JSON array")]
    NotArray { path: ConfigPath },
    #[error("the parent element of '{key}' is not a JSON object")]
    Upsert { key: String },
}

#[allow(deprecated)]
impl RawConfig {
    /// Creates a temporary configuration, by reading a configuration file from disk.
    pub fn from_file(path: &Path) -> Result<Self, WriteError> {
        let file = fs::File::open(path).map_err(|err| WriteError::open_file(path, err))?;
        let config = json::from_reader(file).map_err(|err| WriteError::to_json(path, err))?;
        Ok(RawConfig(config))
    }

    /// Get a mutable reference to a configuration value by path, if it exists.
    pub fn get_mut(&mut self, config_path: &ConfigPath) -> Option<&mut json::Value> {
        config_path
            .iter()
            .try_fold(&mut self.0, |current, part| current.get_mut(part))
    }

    /// Delete the specified path.
    pub fn unset(&mut self, config_path: &ConfigPath) -> Result<json::Value, ModifyError> {
        let last = config_path.last().ok_or(ModifyError::EmptyPath)?;
        let parent = match config_path.parent() {
            Some(parent_path) => {
                self.get_mut(&parent_path)
                    .ok_or_else(|| ModifyError::NotFound {
                        path: config_path.clone(),
                    })?
            }
            None => &mut self.0,
        };

        parent
            .as_object_mut()
            .ok_or_else(|| ModifyError::NotObject {
                path: config_path.clone(),
            })?
            .remove(last);

        Ok(json::Value::Null)
    }

    pub fn push(
        &mut self,
        config_path: &ConfigPath,
        value: ConfigValue,
    ) -> Result<json::Value, ModifyError> {
        if let Some(element) = self.get_mut(config_path) {
            let mut arr = element
                .as_array()
                .ok_or_else(|| ModifyError::NotArray {
                    path: config_path.clone(),
                })?
                .to_vec();
            arr.push(value.into());
            *element = json::Value::Array(arr);

            Ok(element.clone())
        } else {
            self.upsert(config_path, json::Value::Array(vec![value.into()]))
        }
    }

    pub fn remove(
        &mut self,
        config_path: &ConfigPath,
        value: ConfigValue,
    ) -> Result<json::Value, ModifyError> {
        let element = self
            .get_mut(config_path)
            .ok_or_else(|| ModifyError::NotFound {
                path: config_path.clone(),
            })?;
        let arr = element
            .as_array_mut()
            .ok_or_else(|| ModifyError::NotArray {
                path: config_path.clone(),
            })?;
        let value = json::Value::from(value);

        arr.retain(|el| el != &value);
        *element = json::Value::Array(arr.to_owned());

        Ok(element.clone())
    }

    pub fn set(
        &mut self,
        config_path: &ConfigPath,
        value: ConfigValue,
    ) -> Result<json::Value, ModifyError> {
        if let Some(element) = self.get_mut(config_path) {
            *element = value.into();
            Ok(element.clone())
        } else {
            self.upsert(config_path, value)
        }
    }

    /// Writes the configuration, including extra values, to disk. Errors if the config is not
    /// valid.
    pub fn write(&self, path: &Path) -> Result<(), WriteError> {
        let _valid_config: Config = self
            .clone()
            .try_into()
            .map_err(|err| WriteError::validate(path, err))?;
        let file = fs::OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open(path)
            .map_err(|err| WriteError::open_file(path, err))?;

        self.write_file(path, file)
    }

    /// Write to an open file.
    fn write_file(&self, path: &Path, mut file: fs::File) -> Result<(), WriteError> {
        let _valid_config: Config = self
            .clone()
            .try_into()
            .map_err(|err| WriteError::validate(path, err))?;
        let formatter = json::ser::PrettyFormatter::with_indent(b"  ");
        let mut serializer = json::Serializer::with_formatter(&file, formatter);

        self.0
            .serialize(&mut serializer)
            .map_err(|err| WriteError::serialize_json(path, err))?;
        file.write_all(b"\n")
            .map_err(|err| WriteError::write_file(path, err))?;
        file.sync_all()
            .map_err(|err| WriteError::write_file(path, err))?;

        Ok(())
    }

    /// Create an element at the given path, if it doesn't exist yet.
    fn upsert(
        &mut self,
        config_path: &ConfigPath,
        value: impl Into<json::Value>,
    ) -> Result<json::Value, ModifyError> {
        let mut current = &mut self.0;
        for key in config_path.iter() {
            current = match current {
                json::Value::Object(map) => map.entry(key).or_insert_with(|| json::json!({})),
                _ => {
                    return Err(ModifyError::Upsert {
                        key: key.to_owned(),
                    });
                }
            }
        }
        *current = value.into();

        Ok(current.clone())
    }
}

#[allow(deprecated)]
impl TryFrom<RawConfig> for Config {
    type Error = json::Error;

    fn try_from(raw: RawConfig) -> Result<Config, Self::Error> {
        json::from_value(raw.0)
    }
}

/// A struct that ensures all values are safe for JSON serialization, including handling special
/// floating point values like `NaN` and `Infinity`. Use the `From<&str>` implementation to create an instance.
#[deprecated]
#[allow(deprecated)]
pub struct ConfigValue(RawConfigValue);

/// This enum represents raw configuration values and should not be used directly.
/// Use the `ConfigValue` type, which validates values using its `From<&str>` implementation.
#[derive(Debug, Clone)]
#[deprecated]
enum RawConfigValue {
    Integer(i64),
    Float(f64),
    Bool(bool),
    String(String),
}

#[allow(deprecated)]
impl From<&str> for ConfigValue {
    /// Guess the type of a Value.
    fn from(value: &str) -> Self {
        if let Ok(b) = value.parse::<bool>() {
            ConfigValue(RawConfigValue::Bool(b))
        } else if let Ok(n) = value.parse::<i64>() {
            ConfigValue(RawConfigValue::Integer(n))
        } else if let Ok(n) = value.parse::<f64>() {
            // NaN and Infinite can't be properly serialized to JSON
            if n.is_finite() {
                ConfigValue(RawConfigValue::Float(n))
            } else {
                ConfigValue(RawConfigValue::String(value.to_string()))
            }
        } else {
            ConfigValue(RawConfigValue::String(value.to_string()))
        }
    }
}

#[allow(deprecated)]
impl From<String> for ConfigValue {
    fn from(value: String) -> Self {
        value.as_str().into()
    }
}

#[allow(deprecated)]
impl From<ConfigValue> for json::Value {
    fn from(value: ConfigValue) -> Self {
        match value {
            ConfigValue(RawConfigValue::Bool(v)) => json::Value::Bool(v),
            ConfigValue(RawConfigValue::Integer(v)) => json::Value::Number(v.into()),
            ConfigValue(RawConfigValue::Float(v)) => {
                // SAFETY: ConfigValue ensures the Float won't be Infinite or NaN.
                #[allow(clippy::unwrap_used)]
                json::Value::Number(json::Number::from_f64(v).unwrap())
            }
            ConfigValue(RawConfigValue::String(v)) => json::Value::String(v),
        }
    }
}

/// Configuration attribute path.
#[derive(Default, Debug, Clone)]
#[deprecated]
pub struct ConfigPath(Vec<String>);

#[allow(deprecated)]
impl ConfigPath {
    fn parent(&self) -> Option<Self> {
        self.0.split_last().map(|(_, tail)| Self(tail.to_vec()))
    }

    fn last(&self) -> Option<&str> {
        self.0.last().map(AsRef::as_ref)
    }

    fn iter(&self) -> impl Iterator<Item = &str> {
        self.0.iter().map(|s| s.as_str())
    }
}

#[allow(deprecated)]
impl fmt::Display for ConfigPath {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0.join("."))
    }
}

#[allow(deprecated)]
impl From<String> for ConfigPath {
    fn from(value: String) -> Self {
        let parts: Vec<String> = value.split('.').map(|s| s.to_string()).collect();
        ConfigPath(parts)
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
    #[cfg(feature = "schemars")]
    #[test]
    fn schema() {
        use super::Config;
        use crate::prelude::Alias;
        use serde_json::to_value;

        let schema = to_value(schemars::schema_for!(Config)).unwrap();
        let config = to_value(Config::new(Alias::new("schema"))).unwrap();
        jsonschema::validate(&schema, &config)
            .expect("generated configuration should validate under generated JSON Schema");
    }
}