warpgrapher 0.10.0

Automate web service creation with GraphQL and Graph Databases
Documentation
use crate::Error;
use juniper::{DefaultScalarValue, FromInputValue, InputValue};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::fmt::Result as FmtResult;
use std::fmt::{Display, Formatter};
use uuid::Uuid;

/// Intermediate data structure for serialized values, allowing for translation between the values
/// returned by the back-end database (serde_json for cypher-based databases, and a
/// library-specific seralized format for Cosmos and Gremlin DBs), and the serde_json format used
/// to return data to the client.
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::value::Value;
///
/// let v = Value::Bool(true);
/// ```
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum Value {
    Array(Vec<Value>),
    Bool(bool),
    Float64(f64),
    Int64(i64),
    Map(HashMap<String, Value>),
    Null,
    String(String),
    UInt64(u64),
    Uuid(Uuid),
}

impl Display for Value {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        write!(
            f,
            "{}",
            match &self {
                Value::Array(v) => {
                    let s = v
                        .iter()
                        .enumerate()
                        .fold("[".to_string(), |mut acc, (i, val)| {
                            if i > 0 {
                                acc.push_str(", ");
                            }

                            acc.push_str(&*format!("{}", val));
                            acc
                        });
                    s + "]"
                }
                Value::Bool(b) => b.to_string(),
                Value::Float64(f) => f.to_string(),
                Value::Int64(i) => i.to_string(),
                Value::Map(m) => {
                    let s =
                        m.iter()
                            .enumerate()
                            .fold("[".to_string(), |mut acc, (i, (key, val))| {
                                if i > 0 {
                                    acc.push_str(", ");
                                }

                                acc.push_str(&*format!("({}, {})", key, val));
                                acc
                            });

                    s + "]"
                }
                Value::Null => "{}".to_string(),
                Value::String(s) => s.to_string(),
                Value::UInt64(u) => u.to_string(),
                Value::Uuid(uuid) => uuid.to_hyphenated().to_string(),
            }
        )
    }
}

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

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

impl From<HashMap<String, Value>> for Value {
    fn from(map: HashMap<String, Value>) -> Self {
        Value::Map(map)
    }
}

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

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

impl From<u64> for Value {
    fn from(v: u64) -> Self {
        Value::UInt64(v)
    }
}

impl From<Uuid> for Value {
    fn from(v: Uuid) -> Self {
        Value::Uuid(v)
    }
}

impl From<Vec<Value>> for Value {
    fn from(v: Vec<Value>) -> Self {
        Value::Array(v)
    }
}

impl FromInputValue for Value {
    fn from_input_value(v: &InputValue) -> Option<Self> {
        match v {
            InputValue::Scalar(scalar) => Some(match scalar {
                DefaultScalarValue::Int(i) => Value::Int64(i64::from(*i)),
                DefaultScalarValue::Float(f) => Value::Float64(*f),
                DefaultScalarValue::String(s) => Value::String(s.to_string()),
                DefaultScalarValue::Boolean(b) => Value::Bool(*b),
            }),
            _ => match serde_json::to_value(v) {
                Err(_) => None,
                Ok(serde_value) => match Value::try_from(serde_value) {
                    Ok(value) => Some(value),
                    _ => None,
                },
            },
        }
    }
}

impl PartialEq for Value {
    fn eq(&self, other: &Value) -> bool {
        match (self, other) {
            (Value::Array(a), Value::Array(oa)) => a == oa,
            (Value::Bool(b), Value::Bool(ob)) => b == ob,
            (Value::Float64(f), Value::Float64(of)) => f == of,
            (Value::Int64(i), Value::Int64(oi)) => i == oi,
            (Value::Map(m), Value::Map(om)) => m == om,
            (Value::Null, Value::Null) => true,
            (Value::String(s), Value::String(os)) => s == os,
            (Value::UInt64(i), Value::UInt64(oi)) => i == oi,
            (_, _) => false,
        }
    }
}

impl TryFrom<serde_json::Value> for Value {
    type Error = Error;

    fn try_from(value: serde_json::Value) -> Result<Value, Error> {
        match value {
            serde_json::Value::Array(a) => Ok(Value::Array(
                a.into_iter()
                    .map(|val| val.try_into())
                    .collect::<Result<Vec<_>, _>>()?,
            )),
            serde_json::Value::Bool(b) => Ok(Value::Bool(b)),
            serde_json::Value::Null => Ok(Value::Null),
            serde_json::Value::Number(n) => {
                if let Some(i) = n.as_i64() {
                    Ok(Value::Int64(i))
                } else if let Some(i) = n.as_u64() {
                    Ok(Value::UInt64(i))
                } else if let Some(f) = n.as_f64() {
                    Ok(Value::Float64(f))
                } else {
                    Err(Error::TypeConversionFailed {
                        src: "serde_json::Value::Number".to_string(),
                        dst: "Value".to_string(),
                    })
                }
            }
            serde_json::Value::String(s) => Ok(Value::String(s)),
            serde_json::Value::Object(m) => Ok(Value::Map(
                m.into_iter()
                    .map(|(k, v)| {
                        let val = v.try_into()?;
                        Ok((k, val))
                    })
                    .collect::<Result<HashMap<String, Value>, Error>>()?,
            )),
        }
    }
}

impl TryFrom<Value> for bool {
    type Error = Error;

    fn try_from(value: Value) -> Result<bool, Self::Error> {
        if let Value::Bool(b) = value {
            Ok(b)
        } else {
            Err(Error::TypeConversionFailed {
                src: format!("{:#?}", value),
                dst: "bool".to_string(),
            })
        }
    }
}

impl TryFrom<Value> for f64 {
    type Error = Error;

    fn try_from(value: Value) -> Result<f64, Self::Error> {
        if let Value::Int64(i) = value {
            Ok(i as f64)
        } else if let Value::UInt64(i) = value {
            Ok(i as f64)
        } else if let Value::Float64(f) = value {
            Ok(f)
        } else {
            Err(Error::TypeConversionFailed {
                src: format!("{:#?}", value),
                dst: "f64".to_string(),
            })
        }
    }
}

impl TryFrom<Value> for i32 {
    type Error = Error;

    fn try_from(value: Value) -> Result<i32, Self::Error> {
        match value {
            Value::Int64(i) => Ok(i32::try_from(i)?),
            Value::UInt64(i) => Ok(i32::try_from(i)?),
            _ => Err(Error::TypeConversionFailed {
                src: format!("{:#?}", value),
                dst: "i32".to_string(),
            }),
        }
    }
}

impl TryFrom<Value> for String {
    type Error = Error;

    fn try_from(value: Value) -> Result<String, Self::Error> {
        if let Value::String(s) = value {
            Ok(s)
        } else if let Value::Int64(i) = value {
            Ok(i.to_string())
        } else {
            Err(Error::TypeConversionFailed {
                src: format!("{:#?}", value),
                dst: "String".to_string(),
            })
        }
    }
}

impl TryFrom<Value> for serde_json::Value {
    type Error = Error;

    fn try_from(value: Value) -> Result<serde_json::Value, Error> {
        match value {
            Value::Array(a) => Ok(serde_json::Value::Array(
                a.into_iter()
                    .map(|v| v.try_into())
                    .collect::<Result<Vec<_>, Error>>()?,
            )),
            Value::Bool(b) => Ok(serde_json::Value::Bool(b)),
            Value::Float64(f) => Ok(serde_json::Value::Number(
                serde_json::Number::from_f64(f).ok_or_else(|| Error::TypeConversionFailed {
                    src: "Value::Float64".to_string(),
                    dst: "serde_json::Number".to_string(),
                })?,
            )),
            Value::Int64(i) => Ok(serde_json::Value::Number(i.into())),
            Value::Map(hm) => Ok(serde_json::Value::Object(
                hm.into_iter()
                    .map(|(k, v)| {
                        let val = v.try_into()?;
                        Ok((k, val))
                    })
                    .collect::<Result<serde_json::Map<String, serde_json::Value>, Error>>()?,
            )),
            Value::Null => Ok(serde_json::Value::Null),
            Value::String(s) => Ok(serde_json::Value::String(s)),
            Value::UInt64(i) => Ok(serde_json::Value::Number(i.into())),
            Value::Uuid(uuid) => Ok(serde_json::Value::String(uuid.to_hyphenated().to_string())),
        }
    }
}

impl<T> TryFrom<Value> for Vec<T>
where
    T: TryFrom<Value, Error = Error>,
{
    type Error = Error;

    fn try_from(value: Value) -> Result<Self, Self::Error> {
        if let Value::Array(a) = value {
            if let Some(Value::Null) = a.get(0) {
                // If the array null values, return an empty vector, indicating null to Juniper.
                Ok(Vec::new())
            } else {
                // If the array is other than null, try to do the conversation to a Vector.
                a.into_iter()
                    .map(|v| v.try_into())
                    .collect::<Result<Vec<_>, Error>>()
            }
        } else {
            Err(Error::TypeConversionFailed {
                src: format!("{:#?}", value),
                dst: "<T> where T: TryFrom<Value, Error = Error>".to_string(),
            })
        }
    }
}

impl TryFrom<Value> for HashMap<String, Value> {
    type Error = Error;

    fn try_from(value: Value) -> Result<HashMap<String, Value>, Error> {
        match value {
            Value::Map(hm) => Ok(hm),
            _ => Err(Error::TypeConversionFailed {
                src: format!("{:#?}", value),
                dst: "HashMap<String, Value>".to_string(),
            }),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Value;

    /// Passes if the Value implements the Send trait
    #[test]
    fn test_value_send() {
        fn assert_send<T: Send>() {}
        assert_send::<Value>();
    }

    /// Passes if Value implements the Sync trait
    #[test]
    fn test_value_sync() {
        fn assert_sync<T: Sync>() {}
        assert_sync::<Value>();
    }
}