iq 0.5.1

introspection with dynamic queries
Documentation
use {
    crate::{
        diver::Diver,
        errors::IqInternalError,
        *,
    },
    serde::{
        Serialize,
        de::DeserializeOwned,
    },
};

/// Format for the extracted value
///
/// Warning: this enum is expected to change. Prefer to use the public
/// functions to extract values.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IqFormat {
    /// Exctract as Display, but only if the value is a "primitive"
    Primitive,
    /// Extract as JSON
    Json,
    /// Extract as JSON, pretty
    JsonPretty,
    /// Extract the size of the array/map/struct/tupple/string at the end of the path
    Size,
}

/// Extract a string from a structure at a given path, with a given format.
///
/// If the path is not found, return None.
///
/// May theorethically return an error (eg if structure serialization fails),
/// but most users should probably use one of the simpler other functions.
pub fn extract_string_checked<T: Serialize, P: IqPath>(
    source: &T,
    path: P,
    format: IqFormat,
) -> Result<Option<String>, IqError> {
    let keys: Vec<&str> = path.keys().collect();
    let mut diver = Diver::new(&keys, format);
    if keys.is_empty() {
        match format {
            IqFormat::Primitive => {
                diver.set_return_next_primitive();
            }
            IqFormat::Json => {
                return serde_json::to_string(source)
                    .map_err(IqError::Json)
                    .map(Some);
            }
            IqFormat::JsonPretty => {
                return serde_json::to_string_pretty(source)
                    .map_err(IqError::Json)
                    .map(Some);
            }
            IqFormat::Size => {
                return Ok(Sizer::count(source).map(|n| n.to_string()));
            }
        }
    }
    match source.serialize(&mut diver) {
        Ok(()) => Ok(None), // Not found
        Err(IqInternalError::Found(json)) => Ok(Some(json)),
        Err(IqInternalError::Message(msg)) => Err(IqError::Serde(msg)),
        Err(IqInternalError::Json(err)) => Err(IqError::Json(err)),
        Err(IqInternalError::IndexExpected) => Ok(None), // path doesn't match
        Err(IqInternalError::LengthRequired) => Ok(None), // not expected
        Err(IqInternalError::OutOfBounds) => Ok(None),   // path doesn't match
        Err(IqInternalError::Count(n)) => Ok(Some(n.to_string())),
        Err(IqInternalError::NoCount) => Ok(None),
    }
}
/// Extract a string from a structure at a given path, with a given format.
///
/// If the path is not found, return None.
///
/// This function also returns None if the the `Serialize` implementation fails,
/// which should not happen with a standard implementation.
pub fn extract_string<T: Serialize, P: IqPath>(
    source: &T,
    path: P,
    format: IqFormat,
) -> Option<String> {
    extract_string_checked(source, path, format).unwrap_or(None)
}

/// Extract a value as JSON
pub fn extract_json<T: Serialize, P: IqPath>(
    source: &T,
    path: P,
) -> Option<String> {
    extract_string(source, path, IqFormat::Json)
}

/// Extract a value as JSON, pretty
pub fn extract_json_pretty<T: Serialize, P: IqPath>(
    source: &T,
    path: P,
) -> Option<String> {
    extract_string(source, path, IqFormat::JsonPretty)
}

/// Extract a "primitive" value (including strings, simple enum variants, etc)
/// as a string using the `Display` implementation of the deep value.
pub fn extract_primitive<T: Serialize, P: IqPath>(
    source: &T,
    path: P,
) -> Option<String> {
    extract_string(source, path, IqFormat::Primitive)
}

/// Extract a value, which must implement `Deserialize`, from a value, at
/// the given path.
///
/// This function uses a JSON representation of the deep value as intermediate
/// step, which adds some (usually light) overload but also allows to extract
/// in a different type than the real type of the deep value.
pub fn extract_value<T: Serialize, P: IqPath, V: DeserializeOwned>(
    source: &T,
    path: P,
) -> Result<Option<V>, IqError> {
    let json = extract_string_checked(source, path, IqFormat::Json)?;
    let value = json.map(|json| serde_json::from_str(&json)).transpose()?;
    Ok(value)
}

/// Extract the size of the array/map/struct/tupple/string at the end of the path
pub fn extract_size<T: Serialize, P: IqPath>(
    source: &T,
    path: P,
) -> Option<usize> {
    let keys: Vec<&str> = path.keys().collect();
    if keys.first().map_or(true, |s| s.is_empty()) {
        return Sizer::count(source);
    }
    let mut diver = Diver::new(&keys, IqFormat::Size);
    match source.serialize(&mut diver) {
        Err(IqInternalError::Count(n)) => Some(n),
        Err(IqInternalError::NoCount) => None, // not countable
        _ => None,                             // not found, or object not serializable
    }
}

/// Extract the size of the array/map/struct/tupple/string of the given value
pub fn size_of<T: Serialize>(source: &T) -> Option<usize> {
    Sizer::count(source)
}

#[test]
fn test_extract_by_index() {
    #[derive(Debug, PartialEq, Serialize, Clone)]
    struct Thing {
        pub coord: (&'static str, i16),
        pub name: String,
        pub v: Vec<i16>,
    }
    #[derive(Debug, PartialEq, Serialize, Clone)]
    struct Container {
        pub things: Vec<Thing>,
    }
    let t1 = Thing {
        coord: ("Earth", 4),
        name: "some name".to_string(),
        v: vec![1, 2, 3, 4],
    };
    let t2 = Thing {
        coord: ("Mars", 7),
        name: "other name".to_string(),
        v: vec![],
    };
    let con = Container {
        things: vec![t1.clone(), t2.clone()],
    };
    assert_eq!(
        con.extract_primitive("things.1.coord.0"),
        Some("Mars".to_string())
    );
    assert_eq!(
        con.extract_primitive("things.-1.coord.0"),
        Some("Mars".to_string())
    );
    assert_eq!(
        con.extract_primitive("things.-2.v.2"),
        Some("3".to_string())
    );
    assert_eq!(
        con.extract_primitive("things.-2.v.-4"),
        Some("1".to_string())
    );
    assert_eq!(con.extract_primitive("things.-2.v.-5"), None);
    assert_eq!(con.extract_primitive("things.-1.v.-1"), None);
}

#[test]
fn test_extract_size() {
    #[derive(Debug, PartialEq, Serialize)]
    struct Thing {
        pub coord: (&'static str, i16),
        pub name: String,
        pub v: Vec<i16>,
    }
    let thing = Thing {
        coord: ("Earth", 4),
        name: "some name".to_string(),
        v: vec![1, 2, 3, 4],
    };
    assert_eq!(extract_size(&thing, "coord").unwrap(), 2);
    assert_eq!(extract_size(&thing, "coord.0").unwrap(), 5);
    assert_eq!(extract_size(&thing, vec!["coord", "0"]).unwrap(), 5);
    assert_eq!(extract_size(&thing, "name").unwrap(), 9);
    assert_eq!(extract_size(&thing, "v").unwrap(), 4);
    assert_eq!(extract_size(&thing, "").unwrap(), 3);
    assert_eq!(extract_size(&thing, vec![]).unwrap(), 3);
}

#[test]
fn test_extract_value_on_empty_path() {
    #[derive(Debug, PartialEq, Serialize, serde::Deserialize)]
    struct Apple {
        name: String,
        v: Vec<i16>,
    }
    let apple = Apple {
        name: "some name".to_string(),
        v: vec![1, 2, 3, 4],
    };
    let extracted = extract_value(&apple, vec![]).unwrap();
    assert_eq!(extracted, Some(apple));
}

// Test a few corner cases of trying to extract objects as primitives, which should fail and return
// None, not an error.
#[test]
fn test_invalid_extract_string() {
    #[derive(Debug, PartialEq, Serialize)]
    #[serde(rename_all = "PascalCase")]
    struct Principal {
        federated: String,
    }
    #[allow(dead_code)]
    #[derive(Debug, PartialEq, serde::Serialize)]
    #[serde(rename_all = "PascalCase")]
    enum PrincipalFilter {
        Principal(Principal),
        NotPrincipal(Principal),
    }
    #[derive(Debug, PartialEq, Serialize)]
    #[serde(rename_all = "PascalCase")]
    struct Statement {
        #[serde(flatten, skip_serializing_if = "Option::is_none")]
        principal_filter: Option<PrincipalFilter>,
    }
    let statement = Statement {
        principal_filter: Some(PrincipalFilter::Principal(Principal {
            federated: "some name".to_string(),
        })),
    };
    let v: Option<String> = statement.extract_primitive("Principal.Federated");
    assert!(v.is_some()); // should succeed, as the value is a string
    let v: Option<String> = statement.extract_primitive("Principal");
    assert_eq!(v, None); // try to extract an object as a primitive, should fail
    let v: Option<String> = statement.extract_primitive(vec![]);
    assert_eq!(v, None); // try to extract the source object as a primitive, should fail
}