confex-core 0.1.0

Core engine for confex - source merging, hot-reload, migrations, and encryption
Documentation
use std::collections::BTreeMap;
use std::str::FromStr;

#[derive(Debug, Clone, PartialEq, Default)]
pub enum ConfigValue {
    #[default]
    Null,
    Bool(bool),
    Integer(i64),
    Float(f64),
    String(String),
    Array(Vec<ConfigValue>),
    Table(BTreeMap<String, ConfigValue>),
}

impl ConfigValue {
    #[must_use]
    pub fn as_table(&self) -> Option<&BTreeMap<String, ConfigValue>> {
        match self {
            Self::Table(t) => Some(t),
            _ => None,
        }
    }

    #[must_use]
    pub fn as_table_mut(&mut self) -> Option<&mut BTreeMap<String, ConfigValue>> {
        match self {
            Self::Table(t) => Some(t),
            _ => None,
        }
    }

    #[must_use]
    pub fn is_null(&self) -> bool {
        matches!(self, Self::Null)
    }
}

impl From<bool> for ConfigValue {
    fn from(v: bool) -> Self {
        Self::Bool(v)
    }
}

impl From<i64> for ConfigValue {
    fn from(v: i64) -> Self {
        Self::Integer(v)
    }
}

impl From<f64> for ConfigValue {
    fn from(v: f64) -> Self {
        Self::Float(v)
    }
}

impl From<String> for ConfigValue {
    fn from(v: String) -> Self {
        Self::String(v)
    }
}

impl From<&str> for ConfigValue {
    fn from(v: &str) -> Self {
        Self::String(v.to_owned())
    }
}

impl FromStr for ConfigValue {
    type Err = std::convert::Infallible;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.eq_ignore_ascii_case("true") {
            return Ok(Self::Bool(true));
        }
        if s.eq_ignore_ascii_case("false") {
            return Ok(Self::Bool(false));
        }
        if let Ok(i) = s.parse::<i64>() {
            return Ok(Self::Integer(i));
        }
        if let Ok(f) = s.parse::<f64>() {
            return Ok(Self::Float(f));
        }
        Ok(Self::String(s.to_owned()))
    }
}

#[cfg(feature = "json")]
impl From<serde_json::Value> for ConfigValue {
    fn from(v: serde_json::Value) -> Self {
        match v {
            serde_json::Value::Null => Self::Null,
            serde_json::Value::Bool(b) => Self::Bool(b),
            serde_json::Value::Number(n) => n
                .as_i64()
                .map(Self::Integer)
                .or_else(|| n.as_f64().map(Self::Float))
                .unwrap_or(Self::Null),
            serde_json::Value::String(s) => Self::String(s),
            serde_json::Value::Array(arr) => Self::Array(arr.into_iter().map(Self::from).collect()),
            serde_json::Value::Object(map) => {
                Self::Table(map.into_iter().map(|(k, v)| (k, Self::from(v))).collect())
            }
        }
    }
}

#[cfg(feature = "json")]
impl From<ConfigValue> for serde_json::Value {
    fn from(v: ConfigValue) -> Self {
        match v {
            ConfigValue::Null => Self::Null,
            ConfigValue::Bool(b) => Self::Bool(b),
            ConfigValue::Integer(i) => Self::Number(i.into()),
            ConfigValue::Float(f) => {
                serde_json::Number::from_f64(f).map_or(Self::Null, Self::Number)
            }
            ConfigValue::String(s) => Self::String(s),
            ConfigValue::Array(arr) => Self::Array(arr.into_iter().map(Into::into).collect()),
            ConfigValue::Table(map) => {
                Self::Object(map.into_iter().map(|(k, v)| (k, v.into())).collect())
            }
        }
    }
}

#[cfg(feature = "toml")]
impl From<toml::Value> for ConfigValue {
    fn from(v: toml::Value) -> Self {
        match v {
            toml::Value::Boolean(b) => ConfigValue::Bool(b),
            toml::Value::Integer(i) => ConfigValue::Integer(i),
            toml::Value::Float(f) => ConfigValue::Float(f),
            toml::Value::String(s) => ConfigValue::String(s),
            toml::Value::Array(arr) => {
                ConfigValue::Array(arr.into_iter().map(Self::from).collect())
            }
            toml::Value::Table(map) => {
                ConfigValue::Table(map.into_iter().map(|(k, v)| (k, Self::from(v))).collect())
            }
            toml::Value::Datetime(dt) => ConfigValue::String(dt.to_string()),
        }
    }
}

#[cfg(feature = "toml")]
impl From<ConfigValue> for toml::Value {
    fn from(v: ConfigValue) -> Self {
        match v {
            // TOML has no null; map to empty string as lossy fallback
            ConfigValue::Null => toml::Value::Table(toml::map::Map::default()),
            ConfigValue::Bool(b) => toml::Value::Boolean(b),
            ConfigValue::Integer(i) => toml::Value::Integer(i),
            ConfigValue::Float(f) => toml::Value::Float(f),
            ConfigValue::String(s) => toml::Value::String(s),
            ConfigValue::Array(arr) => {
                toml::Value::Array(arr.into_iter().map(Self::from).collect())
            }
            ConfigValue::Table(map) => {
                toml::Value::Table(map.into_iter().map(|(k, v)| (k, Self::from(v))).collect())
            }
        }
    }
}