graphrun 0.1.0

Durable workflow engine
Documentation
use crate::error::{Error, Result};
use crate::limits::MAX_DATA_DEPTH;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Value {
    Null,
    Bool(bool),
    Int(i64),
    String(String),
    Array(Vec<Value>),
    Object(BTreeMap<String, Value>),
}

impl Value {
    pub fn from_json(json: serde_json::Value) -> Result<Self> {
        from_json(json, 0)
    }

    pub fn to_json(&self) -> serde_json::Value {
        match self {
            Self::Null => serde_json::Value::Null,
            Self::Bool(v) => serde_json::Value::Bool(*v),
            Self::Int(v) => serde_json::Value::Number((*v).into()),
            Self::String(v) => serde_json::Value::String(v.clone()),
            Self::Array(items) => {
                serde_json::Value::Array(items.iter().map(Self::to_json).collect())
            }
            Self::Object(fields) => serde_json::Value::Object(
                fields
                    .iter()
                    .map(|(k, v)| (k.clone(), v.to_json()))
                    .collect(),
            ),
        }
    }

    pub fn as_i64(&self) -> Option<i64> {
        match self {
            Self::Int(v) => Some(*v),
            _ => None,
        }
    }

    pub fn as_str(&self) -> Option<&str> {
        match self {
            Self::String(v) => Some(v),
            _ => None,
        }
    }

    pub fn pointer(&self, path: &str) -> Result<&Value> {
        if path.is_empty() {
            return Ok(self);
        }
        if !path.starts_with('/') {
            return Err(Error::invalid(format!(
                "JSON Pointer {path} must start with /"
            )));
        }
        let mut current = self;
        for raw in path.split('/').skip(1) {
            let token = unescape_pointer(raw);
            current = match current {
                Self::Object(map) => map.get(&token).ok_or_else(|| {
                    Error::invalid(format!("missing object field {token} at {path}"))
                })?,
                Self::Array(items) => {
                    let index: usize = token.parse().map_err(|_| {
                        Error::invalid(format!("array index {token} is not an integer"))
                    })?;
                    items.get(index).ok_or_else(|| {
                        Error::invalid(format!("array index {index} is out of range"))
                    })?
                }
                _ => {
                    return Err(Error::invalid(format!(
                        "cannot project {token} through a scalar"
                    )));
                }
            };
        }
        Ok(current)
    }
}

fn unescape_pointer(token: &str) -> String {
    token.replace("~1", "/").replace("~0", "~")
}

fn from_json(json: serde_json::Value, depth: usize) -> Result<Value> {
    if depth > MAX_DATA_DEPTH {
        return Err(Error::invalid("value exceeds maximum nesting depth"));
    }
    match json {
        serde_json::Value::Null => Ok(Value::Null),
        serde_json::Value::Bool(v) => Ok(Value::Bool(v)),
        serde_json::Value::Number(n) => {
            if let Some(v) = n.as_i64() {
                Ok(Value::Int(v))
            } else {
                Err(Error::invalid(format!(
                    "non-integer number {n} is not supported"
                )))
            }
        }
        serde_json::Value::String(v) => Ok(Value::String(v)),
        serde_json::Value::Array(items) => {
            let mut out = Vec::with_capacity(items.len());
            for item in items {
                out.push(from_json(item, depth + 1)?);
            }
            Ok(Value::Array(out))
        }
        serde_json::Value::Object(map) => {
            let mut out = BTreeMap::new();
            for (key, item) in map {
                out.insert(key, from_json(item, depth + 1)?);
            }
            Ok(Value::Object(out))
        }
    }
}

pub fn canonical_json(value: &serde_json::Value) -> Result<Vec<u8>> {
    let mut out = Vec::new();
    write_canonical(&mut out, value, 0)?;
    Ok(out)
}

fn write_canonical(out: &mut Vec<u8>, value: &serde_json::Value, depth: usize) -> Result<()> {
    if depth > MAX_DATA_DEPTH {
        return Err(Error::invalid("canonical JSON exceeds maximum depth"));
    }
    match value {
        serde_json::Value::Null => out.extend_from_slice(b"null"),
        serde_json::Value::Bool(true) => out.extend_from_slice(b"true"),
        serde_json::Value::Bool(false) => out.extend_from_slice(b"false"),
        serde_json::Value::Number(n) => {
            if let Some(v) = n.as_i64() {
                out.extend_from_slice(v.to_string().as_bytes());
            } else {
                return Err(Error::invalid(format!(
                    "non-integer number {n} cannot be canonicalized"
                )));
            }
        }
        serde_json::Value::String(s) => {
            let encoded = serde_json::to_string(s)
                .map_err(|err| Error::invalid(format!("string encode: {err}")))?;
            out.extend_from_slice(encoded.as_bytes());
        }
        serde_json::Value::Array(items) => {
            out.push(b'[');
            for (i, item) in items.iter().enumerate() {
                if i > 0 {
                    out.push(b',');
                }
                write_canonical(out, item, depth + 1)?;
            }
            out.push(b']');
        }
        serde_json::Value::Object(map) => {
            let mut keys: Vec<&String> = map.keys().collect();
            keys.sort();
            out.push(b'{');
            for (i, key) in keys.iter().enumerate() {
                if i > 0 {
                    out.push(b',');
                }
                let encoded = serde_json::to_string(*key)
                    .map_err(|err| Error::invalid(format!("key encode: {err}")))?;
                out.extend_from_slice(encoded.as_bytes());
                out.push(b':');
                write_canonical(out, &map[*key], depth + 1)?;
            }
            out.push(b'}');
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn canonical_sorts_object_keys() {
        let value = json!({"b": 1, "a": 2});
        assert_eq!(
            String::from_utf8(canonical_json(&value).unwrap()).unwrap(),
            r#"{"a":2,"b":1}"#
        );
    }
}