diffo 0.2.0

Semantic diffing for Rust structs via serde
Documentation
use serde_value::Value;

/// Extension trait for Value to provide convenient accessor methods.
pub trait ValueExt {
    /// Get a field value from a Map by string key.
    ///
    /// # Examples
    ///
    /// ```
    /// use diffo::ValueExt;
    /// # use serde_value::Value;
    /// # use std::collections::BTreeMap;
    ///
    /// # let mut map = BTreeMap::new();
    /// # map.insert(Value::String("id".into()), Value::I64(42));
    /// # let value = Value::Map(map);
    /// if let Some(id) = value.get_field("id") {
    ///     // Use id value
    /// }
    /// ```
    fn get_field(&self, key: &str) -> Option<&Value>;

    /// Try to extract as a string.
    fn as_string(&self) -> Option<&str>;

    /// Try to extract as an i64.
    fn as_i64(&self) -> Option<i64>;

    /// Try to extract as a u64.
    fn as_u64(&self) -> Option<u64>;

    /// Try to extract as an f64.
    fn as_f64(&self) -> Option<f64>;

    /// Try to extract as a bool.
    fn as_bool(&self) -> Option<bool>;
}

impl ValueExt for Value {
    fn get_field(&self, key: &str) -> Option<&Value> {
        match self {
            Value::Map(map) => {
                // Try to find the key
                map.iter()
                    .find(|(k, _)| {
                        if let Value::String(s) = k {
                            s == key
                        } else {
                            false
                        }
                    })
                    .map(|(_, v)| v)
            }
            _ => None,
        }
    }

    fn as_string(&self) -> Option<&str> {
        match self {
            Value::String(s) => Some(s),
            _ => None,
        }
    }

    fn as_i64(&self) -> Option<i64> {
        match self {
            Value::I8(n) => Some(*n as i64),
            Value::I16(n) => Some(*n as i64),
            Value::I32(n) => Some(*n as i64),
            Value::I64(n) => Some(*n),
            _ => None,
        }
    }

    fn as_u64(&self) -> Option<u64> {
        match self {
            Value::U8(n) => Some(*n as u64),
            Value::U16(n) => Some(*n as u64),
            Value::U32(n) => Some(*n as u64),
            Value::U64(n) => Some(*n),
            _ => None,
        }
    }

    fn as_f64(&self) -> Option<f64> {
        match self {
            Value::F32(f) => Some(*f as f64),
            Value::F64(f) => Some(*f),
            _ => None,
        }
    }

    fn as_bool(&self) -> Option<bool> {
        match self {
            Value::Bool(b) => Some(*b),
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::BTreeMap;

    #[test]
    fn test_get_field() {
        let mut map = BTreeMap::new();
        map.insert(Value::String("name".into()), Value::String("Alice".into()));
        map.insert(Value::String("id".into()), Value::I64(42));

        let value = Value::Map(map);

        assert_eq!(value.get_field("name").unwrap().as_string(), Some("Alice"));
        assert_eq!(value.get_field("id").unwrap().as_i64(), Some(42));
        assert!(value.get_field("missing").is_none());
    }

    #[test]
    fn test_as_string() {
        let value = Value::String("hello".into());
        assert_eq!(value.as_string(), Some("hello"));

        let value = Value::I64(42);
        assert_eq!(value.as_string(), None);
    }

    #[test]
    fn test_as_i64() {
        assert_eq!(Value::I8(10).as_i64(), Some(10));
        assert_eq!(Value::I16(100).as_i64(), Some(100));
        assert_eq!(Value::I32(1000).as_i64(), Some(1000));
        assert_eq!(Value::I64(10000).as_i64(), Some(10000));
        assert_eq!(Value::String("nope".into()).as_i64(), None);
    }

    #[test]
    fn test_as_u64() {
        assert_eq!(Value::U8(10).as_u64(), Some(10));
        assert_eq!(Value::U16(100).as_u64(), Some(100));
        assert_eq!(Value::U32(1000).as_u64(), Some(1000));
        assert_eq!(Value::U64(10000).as_u64(), Some(10000));
        assert_eq!(Value::String("nope".into()).as_u64(), None);
    }

    #[test]
    fn test_as_f64() {
        assert_eq!(Value::F32(3.14).as_f64(), Some(3.14f32 as f64));
        assert_eq!(Value::F64(2.718).as_f64(), Some(2.718));
        assert_eq!(Value::I64(42).as_f64(), None);
    }

    #[test]
    fn test_as_bool() {
        assert_eq!(Value::Bool(true).as_bool(), Some(true));
        assert_eq!(Value::Bool(false).as_bool(), Some(false));
        assert_eq!(Value::I64(1).as_bool(), None);
    }

    #[test]
    fn test_get_field_not_a_map() {
        let value = Value::String("not a map".into());
        assert!(value.get_field("anything").is_none());
    }
}