did_doc 0.2.2

Library for loading/saving DID documents.
Documentation
use crate::error::{DidError, DidErrorKind};

use std::{collections::BTreeMap, str::FromStr};

use nom::{
    bytes::complete::{is_a, is_not, tag, take_while},
    character::complete::char,
    combinator::{map, map_res, opt},
    multi::separated_list,
    sequence::preceded,
    IResult,
};

#[derive(Debug)]
pub struct DidUri {
    pub id: String,
    pub method: String,
    pub params: Option<BTreeMap<String, String>>,
    pub query: Option<BTreeMap<String, String>>,
    pub fragment: Option<String>,
}

impl FromStr for DidUri {
    type Err = DidError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match parse_did_string(s.as_bytes()) {
            Ok((_, d)) => Ok(d),
            Err(_) => Err(DidError::from_kind(DidErrorKind::InvalidDidUri)),
        }
    }
}

impl Clone for DidUri {
    fn clone(&self) -> Self {
        DidUri {
            id: self.id.clone(),
            method: self.method.clone(),
            params: self.params.clone(),
            query: self.query.clone(),
            fragment: self.fragment.clone(),
        }
    }
}

impl std::fmt::Display for DidUri {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        let mut params = String::new();
        if let Some(p) = &self.params {
            params.push(';');
            params.push_str(
                &p.iter()
                    .map(|(k, v)| format!("{}={}", k, v))
                    .collect::<Vec<String>>()
                    .join(";"),
            );
        }
        let mut query = String::new();
        if let Some(q) = &self.query {
            query.push('?');
            query.push_str(
                &q.iter()
                    .map(|(k, v)| format!("{}={}", k, v))
                    .collect::<Vec<String>>()
                    .join("&"),
            );
        }
        let mut fragment = String::new();
        if let Some(f) = &self.fragment {
            fragment = format!("#{}", f);
        }

        write!(
            f,
            "did:{}:{}{}{}{}",
            self.method, self.id, params, query, fragment
        )
    }
}

fn parse_did_string(i: &[u8]) -> IResult<&[u8], DidUri> {
    let (i, _) = tag("did:")(i)?;
    let (i, method) = map(take_while(is_did_method_char), std::str::from_utf8)(i)?;
    let (i, _) = char(':')(i)?;
    let (i, id) = map(take_while(is_did_id_char), std::str::from_utf8)(i)?;
    let (i, params) = opt(did_params)(i)?;
    let (i, query) = opt(did_query)(i)?;
    let (i, fragment) = opt(did_fragment)(i)?;

    Ok((
        i,
        DidUri {
            id: id.unwrap().to_string(),
            method: method.unwrap().to_string(),
            params: params.map(|m| {
                m.into_iter()
                    .map(|(k, v)| (k.to_string(), v.to_string()))
                    .collect()
            }),
            query: query.map(|m| {
                m.into_iter()
                    .map(|(k, v)| (k.to_string(), v.to_string()))
                    .collect()
            }),
            fragment: fragment.map(|s| s.to_string()),
        },
    ))
}
fn is_did_method_char(c: u8) -> bool {
    let c = c as char;
    c.is_ascii_lowercase() || c.is_ascii_digit()
}
fn is_did_id_char(c: u8) -> bool {
    let c = c as char;
    c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-'
}

fn did_params(i: &[u8]) -> IResult<&[u8], BTreeMap<&str, &str>> {
    let (i, lst) = preceded(char(';'), separated_list(char(';'), param_item))(i)?;

    Ok((i, lst.into_iter().collect()))
}
fn param_item(i: &[u8]) -> IResult<&[u8], (&str, &str)> {
    let (i, key) = param_token(i)?;
    let (i, _) = char('=')(i)?;
    let (i, val) = param_token(i)?;
    Ok((i, (key, val)))
}
fn param_token(i: &[u8]) -> IResult<&[u8], &str> {
    map_res(
        is_a("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%.-_:"),
        std::str::from_utf8,
    )(i)
}

fn did_query(i: &[u8]) -> IResult<&[u8], BTreeMap<&str, &str>> {
    let (i, lst) = preceded(char('?'), separated_list(char('&'), query_item))(i)?;

    Ok((i, lst.into_iter().collect()))
}
fn query_item(i: &[u8]) -> IResult<&[u8], (&str, &str)> {
    let (i, key) = query_token(i)?;
    let (i, _) = char('=')(i)?;
    let (i, val) = query_token(i)?;
    Ok((i, (key, val)))
}
fn query_token(i: &[u8]) -> IResult<&[u8], &str> {
    map_res(is_not("&=:#[]"), std::str::from_utf8)(i)
}

fn did_fragment(i: &[u8]) -> IResult<&[u8], &str> {
    preceded(char('#'), map_res(is_not(":#[]"), std::str::from_utf8))(i)
}

#[cfg(test)]
mod resolve_method_tests {
    use super::*;

    use crate::error::DidErrorKind;

    #[test]
    fn test_did_method_from_str_valid() {
        let did = DidUri::from_str("did:git:akjsdhgaksdjhgasdkgh");
        assert!(did.is_ok());
        let did = did.unwrap();
        assert_eq!(did.id, "akjsdhgaksdjhgasdkgh".to_string());
        assert_eq!(did.method, "git".to_string());
        assert!(did.fragment.is_none());
        assert!(did.params.is_none());
        assert!(did.query.is_none());

        let did = DidUri::from_str("did:git:");
        assert!(did.is_ok());
        let did = did.unwrap();
        assert_eq!(did.id, "".to_string());

        let did = DidUri::from_str("did:sov:123456ygbvgfred;pool=mainnet;key=gdsadsfgdsfah");
        assert!(did.is_ok());
        let did = did.unwrap();
        assert!(did.params.is_some());
        let params = did.params.unwrap();
        assert_eq!(params.len(), 2);
        assert_eq!(params.get("pool"), Some(&"mainnet".to_string()));
        assert_eq!(params.get("key"), Some(&"gdsadsfgdsfah".to_string()));

        assert!(DidUri::from_str("did:sov:builder:aksjdhgaksjdhgaskdgjh").is_ok());
        assert!(DidUri::from_str("did:sov:test:aksjdhgaksjdhgaskdgjh").is_ok());
        let did = DidUri::from_str(
            "did:git:12345678jhasdg;file=Users_janedoe_.git?key=ham&value=meat#1-2-3",
        );

        assert!(did.is_ok());
        let did = did.unwrap();
        assert!(did.params.is_some());
        assert!(did.query.is_some());
        assert!(did.fragment.is_some());
        let params = &did.params.clone().unwrap();
        let query = &did.query.clone().unwrap();

        assert_eq!(params.get("file"), Some(&"Users_janedoe_.git".to_string()));
        assert_eq!(query.get("key"), Some(&"ham".to_string()));
        assert_eq!(query.get("value"), Some(&"meat".to_string()));
        assert_eq!(&did.fragment.clone().unwrap(), &"1-2-3".to_string());
        assert_eq!(
            did.to_string(),
            "did:git:12345678jhasdg;file=Users_janedoe_.git?key=ham&value=meat#1-2-3".to_string()
        );
    }

    #[test]
    fn test_did_params() {
        let p = b";a=b;c=d";
        let d = did_params(p).unwrap().1;
        assert_eq!(d.get("a"), Some(&"b"));
        assert_eq!(d.get("c"), Some(&"d"));
        let p = b";a=b";
        let d = did_params(p).unwrap().1;
        assert_eq!(d.get("a"), Some(&"b"));
        assert_eq!(d.get("c"), None);
    }

    #[test]
    fn test_did_fragment() {
        let fragment = b"#first-page";
        let d = did_fragment(fragment).unwrap().1;
        assert_eq!(d, std::str::from_utf8(&fragment[1..]).unwrap());
    }

    #[test]
    fn test_did_query() {
        let q = b"?a=b&c=d";
        let d = did_query(q).unwrap().1;
        assert_eq!(d.get("a"), Some(&"b"));
        assert_eq!(d.get("c"), Some(&"d"));
        let q = b"?%61=%62";
        let d = did_query(q).unwrap().1;
        assert_eq!(d.get("%61"), Some(&"%62"));
    }

    #[test]
    fn test_did_method_from_str_invalid() {
        for s in &["did:", "https://example.org", "did:git", "did:sov"] {
            let res = DidUri::from_str(s);
            match res {
                Ok(_) => assert!(false),
                Err(e) => assert_eq!(e.kind(), DidErrorKind::InvalidDidUri),
            };
        }
    }

}