ad4m-client 0.12.0-rc2

Client library wrapping AD4M's GraphQL interface
Documentation
use anyhow::{anyhow, Result};
use std::fmt::Display;

#[derive(Clone, Debug, PartialEq)]
pub enum LiteralValue {
    String(String),
    Number(f64),
    Json(serde_json::Value),
}

impl Display for LiteralValue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LiteralValue::String(string) => write!(f, "{}", string),
            LiteralValue::Number(number) => write!(f, "{}", number),
            LiteralValue::Json(json) => write!(f, "{}", json),
        }
    }
}

pub struct Literal {
    value: Option<LiteralValue>,
    url: Option<String>,
}

impl Literal {
    pub fn from_url(url: String) -> Result<Self> {
        if url.starts_with("literal://") {
            Ok(Self {
                value: None,
                url: Some(url),
            })
        } else {
            Err(anyhow!("Not a literal URL"))
        }
    }

    pub fn from_string(string: String) -> Self {
        Self {
            value: Some(LiteralValue::String(string)),
            url: None,
        }
    }

    pub fn from_number(number: f64) -> Self {
        Self {
            value: Some(LiteralValue::Number(number)),
            url: None,
        }
    }

    pub fn from_json(json: serde_json::Value) -> Self {
        Self {
            value: Some(LiteralValue::Json(json)),
            url: None,
        }
    }

    pub fn to_url(&self) -> Result<String> {
        if let Some(url) = &self.url {
            Ok(url.clone())
        } else if let Some(value) = &self.value {
            match value {
                LiteralValue::String(string) => {
                    let encoded = urlencoding::encode(string);
                    Ok(format!("literal://string:{}", encoded))
                }
                LiteralValue::Number(number) => Ok(format!("literal://number:{}", number)),
                LiteralValue::Json(json) => {
                    let encoded = urlencoding::encode(&json.to_string()).to_string();
                    Ok(format!("literal://json:{}", encoded))
                }
            }
        } else {
            Err(anyhow!("No value or URL"))
        }
    }

    pub fn parse_url(&self) -> Result<LiteralValue> {
        if let Some(url) = &self.url {
            if url.starts_with("literal://") {
                let literal = url.replace("literal://", "");
                if literal.starts_with("string:") {
                    let string = literal.replace("string:", "");
                    let decoded = urlencoding::decode(&string)?;
                    Ok(LiteralValue::String(decoded.into()))
                } else if literal.starts_with("number:") {
                    let number = literal.replace("number:", "");
                    let parsed = number.parse::<f64>()?;
                    Ok(LiteralValue::Number(parsed))
                } else if literal.starts_with("json:") {
                    let json = literal.replace("json:", "");
                    let decoded = urlencoding::decode(&json)?;
                    let parsed = serde_json::from_str::<serde_json::Value>(&decoded)?;
                    Ok(LiteralValue::Json(parsed))
                } else {
                    Err(anyhow!("Unknown literal type"))
                }
            } else {
                Err(anyhow!("Not a literal URL"))
            }
        } else {
            Err(anyhow!("No URL"))
        }
    }

    pub fn get(&self) -> Result<LiteralValue> {
        if let Some(value) = &self.value {
            Ok(value.clone())
        } else if self.url.is_some() {
            self.parse_url()
        } else {
            Err(anyhow!("No value or URL"))
        }
    }

    pub fn convert(&mut self) -> Result<()> {
        if self.value.is_some() {
            self.url = Some(self.to_url()?);
            Ok(())
        } else if self.url.is_some() {
            self.value = Some(self.parse_url()?);
            Ok(())
        } else {
            Err(anyhow!("No value or URL"))
        }
    }
}

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

    #[test]
    fn can_handle_strings() {
        let test_string = "test string";
        let test_url = "literal://string:test%20string";

        let literal = super::Literal::from_string(test_string.into());
        assert_eq!(literal.to_url().unwrap(), test_url);

        let mut literal2 = super::Literal::from_url(test_url.into()).unwrap();
        assert_eq!(
            literal2.get().unwrap(),
            super::LiteralValue::String(test_string.into())
        );

        literal2.convert().expect("Failed to convert");
        assert_eq!(
            literal2.value.unwrap(),
            super::LiteralValue::String(test_string.into())
        );
    }

    #[test]
    fn can_handle_numbers() {
        let test_number = 3.1;
        let test_url = "literal://number:3.1";

        let literal = super::Literal::from_number(test_number);
        assert_eq!(literal.to_url().unwrap(), test_url);

        let mut literal2 = super::Literal::from_url(test_url.into()).unwrap();
        assert_eq!(
            literal2.get().unwrap(),
            super::LiteralValue::Number(test_number)
        );

        literal2.convert().expect("Failed to convert");
        assert_eq!(
            literal2.value.unwrap(),
            super::LiteralValue::Number(test_number)
        );
    }

    #[test]
    fn can_handle_objects() {
        let test_object = json!({
            "testString": "test",
            "testNumber": "1337",
        });
        let test_url =
            "literal://json:%7B%22testNumber%22%3A%221337%22%2C%22testString%22%3A%22test%22%7D";

        let literal = super::Literal::from_json(test_object.clone());
        assert_eq!(literal.to_url().unwrap(), test_url);

        let mut literal2 = super::Literal::from_url(test_url.into()).unwrap();
        assert_eq!(
            literal2.get().unwrap(),
            super::LiteralValue::Json(test_object.clone())
        );

        literal2.convert().expect("Failed to convert");
        assert_eq!(
            literal2.value.unwrap(),
            super::LiteralValue::Json(test_object)
        );
    }

    #[test]
    fn can_handle_special_characters() {
        let test_string = "message(X) :- triple('ad4m://self', _, X).";
        let test_url = "literal://string:message%28X%29%20%3A-%20triple%28%27ad4m%3A%2F%2Fself%27%2C%20_%2C%20X%29.";

        let literal = super::Literal::from_string(test_string.into());
        assert_eq!(literal.to_url().unwrap(), test_url);

        let mut literal2 = super::Literal::from_url(test_url.into()).unwrap();
        assert_eq!(
            literal2.get().unwrap(),
            super::LiteralValue::String(test_string.into())
        );

        literal2.convert().expect("Failed to convert");
        assert_eq!(
            literal2.value.unwrap(),
            super::LiteralValue::String(test_string.into())
        );
    }
}