nym-config 1.21.1

Config related helpers and functions
Documentation
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0

use handlebars::{Handlebars, TemplateRenderError};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::{fs, io};

pub use helpers::{parse_urls, OptionalSet};
pub use toml::de::Error as TomlDeError;

pub mod defaults;
pub mod error;
pub mod helpers;
pub mod legacy_helpers;
pub mod serde_helpers;

pub const NYM_DIR: &str = ".nym";
pub const DEFAULT_NYM_APIS_DIR: &str = "nym-api";
pub const DEFAULT_CONFIG_DIR: &str = "config";
pub const DEFAULT_DATA_DIR: &str = "data";
pub const DEFAULT_CONFIG_FILENAME: &str = "config.toml";

#[cfg(feature = "dirs")]
pub fn must_get_home() -> PathBuf {
    if let Some(home_dir) = std::env::var_os("NYM_HOME_DIR") {
        home_dir.into()
    } else {
        dirs::home_dir().expect("Failed to evaluate $HOME value")
    }
}

#[cfg(feature = "dirs")]
pub fn may_get_home() -> Option<PathBuf> {
    if let Some(home_dir) = std::env::var_os("NYM_HOME_DIR") {
        Some(home_dir.into())
    } else {
        dirs::home_dir()
    }
}

pub trait NymConfigTemplate: Serialize {
    fn template(&self) -> &'static str;

    fn format_to_string(&self) -> String {
        // it is responsibility of whoever is implementing the trait to ensure the template is valid
        Handlebars::new()
            .render_template(self.template(), &self)
            .unwrap()
    }

    fn format_to_writer<W: Write>(&self, writer: W) -> io::Result<()> {
        if let Err(err) = Handlebars::new().render_template_to_write(self.template(), &self, writer)
        {
            match err {
                TemplateRenderError::IOError(err, _) => return Err(err),
                other_err => {
                    // it is responsibility of whoever is implementing the trait to ensure the template is valid
                    panic!("invalid template: {other_err}")
                }
            }
        }

        Ok(())
    }
}

pub fn save_formatted_config_to_file<C, P>(config: &C, path: P) -> io::Result<()>
where
    C: NymConfigTemplate,
    P: AsRef<Path>,
{
    let path = path.as_ref();
    log::info!("saving config file to {}", path.display());

    if let Some(parent) = path.parent() {
        create_dir_all(parent)?;
    }

    let file = File::create(path)?;

    // TODO: check for whether any of our configs store anything sensitive
    // and change that to 0o644 instead
    #[cfg(target_family = "unix")]
    {
        use std::os::unix::fs::PermissionsExt;

        let mut perms = fs::metadata(path)?.permissions();
        perms.set_mode(0o600);
        fs::set_permissions(path, perms)?;
    }

    config.format_to_writer(file)
}

pub fn save_unformatted_config_to_file<C, P>(
    config: &C,
    path: P,
) -> Result<(), error::NymConfigTomlError>
where
    C: Serialize + ?Sized,
    P: AsRef<Path>,
{
    let path = path.as_ref();
    log::info!("saving config file to {}", path.display());

    if let Some(parent) = path.parent() {
        create_dir_all(parent)?;
    }

    let mut file = File::create(path)?;

    // TODO: check for whether any of our configs store anything sensitive
    // and change that to 0o644 instead
    #[cfg(target_family = "unix")]
    {
        use std::os::unix::fs::PermissionsExt;

        let mut perms = fs::metadata(path)?.permissions();
        perms.set_mode(0o600);
        fs::set_permissions(path, perms)?;
    }

    // let serde format the TOML in whatever ugly way it chooses
    // TODO: in https://docs.rs/toml/latest/toml/fn.to_string_pretty.html it recommends using
    //   https://docs.rs/toml_edit/latest/toml_edit/struct.DocumentMut.html to preserve formatting
    let toml_string = toml::to_string_pretty(config)?;

    Ok(file.write_all(toml_string.as_bytes())?)
}

pub fn deserialize_config_from_toml_str<C>(raw: &str) -> Result<C, TomlDeError>
where
    C: DeserializeOwned,
{
    toml::from_str(raw)
}

pub fn read_config_from_toml_file<C, P>(path: P) -> io::Result<C>
where
    C: DeserializeOwned,
    P: AsRef<Path>,
{
    log::trace!(
        "trying to read config file from {}",
        path.as_ref().display()
    );
    let content = fs::read_to_string(path)?;

    // TODO: should we be preserving original error type instead?
    deserialize_config_from_toml_str(&content).map_err(io::Error::other)
}

//
//
//
// pub trait NymConfig: Default + Serialize + DeserializeOwned {
//     fn template(&self) -> &'static str;
//
//     fn config_file_name() -> String {
//         "config.toml".to_string()
//     }
//
//     fn default_root_directory() -> PathBuf;
//
//     // default, most probable, implementations; can be easily overridden where required
//     fn default_config_directory(id: &str) -> PathBuf {
//         Self::default_root_directory()
//             .join(id)
//             .join(DEFAULT_CONFIG_DIR)
//     }
//
//     fn default_data_directory(id: &str) -> PathBuf {
//         Self::default_root_directory()
//             .join(id)
//             .join(DEFAULT_DATA_DIR)
//     }
//
//     fn default_config_file_path(id: &str) -> PathBuf {
//         Self::default_config_directory(id).join(Self::config_file_name())
//     }
//
//     // We provide a second set of functions that tries to not panic.
//
//     fn try_default_root_directory() -> Option<PathBuf>;
//
//     fn try_default_config_directory(id: &str) -> Option<PathBuf> {
//         Self::try_default_root_directory().map(|d| d.join(id).join(DEFAULT_CONFIG_DIR))
//     }
//
//     fn try_default_data_directory(id: &str) -> Option<PathBuf> {
//         Self::try_default_root_directory().map(|d| d.join(id).join(DEFAULT_DATA_DIR))
//     }
//
//     fn try_default_config_file_path(id: &str) -> Option<PathBuf> {
//         Self::try_default_config_directory(id).map(|d| d.join(Self::config_file_name()))
//     }
//
//     fn root_directory(&self) -> PathBuf;
//     fn config_directory(&self) -> PathBuf;
//     fn data_directory(&self) -> PathBuf;
//
//     fn save_to_file(&self, custom_location: Option<PathBuf>) -> io::Result<()> {
//         Ok(())
//         // let reg = Handlebars::new();
//         // // it's whoever is implementing the trait responsibility to make sure you can execute your own template on your data
//         // let templated_config = reg.render_template(Self::template(), self).unwrap();
//         //
//         // // make sure the whole directory structure actually exists
//         // match custom_location.clone() {
//         //     Some(loc) => {
//         //         if let Some(parent_dir) = loc.parent() {
//         //             fs::create_dir_all(parent_dir)
//         //         } else {
//         //             Ok(())
//         //         }
//         //     }
//         //     None => fs::create_dir_all(self.config_directory()),
//         // }?;
//         //
//         // let location = custom_location
//         //     .unwrap_or_else(|| self.config_directory().join(Self::config_file_name()));
//         // log::info!("Configuration file will be saved to {:?}", location);
//         //
//         // cfg_if::cfg_if! {
//         //     if #[cfg(unix)] {
//         //         fs::write(location.clone(), templated_config)?;
//         //         let mut perms = fs::metadata(location.clone())?.permissions();
//         //         perms.set_mode(0o600);
//         //         fs::set_permissions(location, perms)?;
//         //     } else {
//         //         fs::write(location, templated_config)?;
//         //     }
//         // }
//         //
//         // Ok(())
//     }
//
//     fn load_from_file(id: &str) -> io::Result<Self> {
//         let file = Self::default_config_file_path(id);
//         log::trace!("Loading from file: {:#?}", file);
//         let config_contents = fs::read_to_string(file)?;
//
//         toml::from_str(&config_contents)
//             .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
//     }
// }