serde-version 0.5.1

Versioning support for Serde
Documentation
use serde::{de, Deserialize, Serialize, Serializer};
use std::borrow::Cow;
use std::convert::TryFrom;
use std::fmt::Display;
use std::ops::Deref;

/// An uri identifying a version group.
///
/// Usually a version group is made of both an api group and a version.
/// ```rust
/// # use serde_version::VersionGroupURI;
/// # use std::convert::TryInto;
/// #
/// // This is the uri with api group 'my_api_group' and version 'my_version'
/// let uri: VersionGroupURI = "my_api_group:my_version".try_into().unwrap();
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct VersionGroupURI<'a> {
    source: Cow<'a, str>,
    index: usize,
}
impl<'a> VersionGroupURI<'a> {
    pub fn api_group(&self) -> &str {
        &self.source[..self.index]
    }
    pub fn version(&self) -> &str {
        &self.source[(self.index + 1)..]
    }
}
impl<'a> Serialize for VersionGroupURI<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.source)
    }
}

impl<'a> Display for VersionGroupURI<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        write!(f, "{}", self.source)
    }
}

impl<'a> VersionGroupURI<'a> {
    pub fn to_static(&self) -> VersionGroupURI<'static> {
        VersionGroupURI {
            source: Cow::Owned(self.source.clone().into_owned()),
            index: self.index,
        }
    }
}

impl<'a> ToString for VersionGroupURI<'a> {
    fn to_string(&self) -> String {
        self.source.clone().into_owned()
    }
}

#[derive(Debug, Eq, PartialEq, Hash, Clone, Fail)]
#[fail(display = "Invalid format {}, expected \"api_group:version\"", source)]
pub struct TryFromError {
    pub source: String,
}
impl<'a> TryFrom<Cow<'a, str>> for VersionGroupURI<'a> {
    type Error = TryFromError;

    fn try_from(source: Cow<'a, str>) -> Result<Self, Self::Error> {
        if let Some(index) = source.find(':') {
            if index == 0 || index + 1 >= source.len() {
                Err(TryFromError {
                    source: source.into_owned(),
                })
            } else {
                Ok(VersionGroupURI { source, index })
            }
        } else {
            Err(TryFromError {
                source: source.into_owned(),
            })
        }
    }
}
impl<'a> TryFrom<&'a str> for VersionGroupURI<'a> {
    type Error = TryFromError;

    fn try_from(source: &'a str) -> Result<Self, Self::Error> {
        VersionGroupURI::try_from(Cow::Borrowed(source))
    }
}
impl<'a> TryFrom<String> for VersionGroupURI<'a> {
    type Error = TryFromError;

    fn try_from(source: String) -> Result<Self, Self::Error> {
        VersionGroupURI::try_from(Cow::Owned(source))
    }
}

impl<'a, 'de: 'a> Deserialize<'de> for VersionGroupURI<'a> {
    fn deserialize<D>(deserializer: D) -> Result<Self, <D as de::Deserializer<'de>>::Error>
    where
        D: de::Deserializer<'de>,
    {
        struct VersionGroupURIVisitor<'b>(std::marker::PhantomData<&'b u8>);

        impl<'b, 'de: 'b> de::Visitor<'de> for VersionGroupURIVisitor<'b> {
            type Value = VersionGroupURI<'b>;

            fn expecting(
                &self,
                formatter: &mut std::fmt::Formatter,
            ) -> Result<(), std::fmt::Error> {
                formatter.write_str("\"api_group:version\"")
            }

            #[inline]
            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                VersionGroupURI::try_from(Cow::Owned(v.to_string()))
                    .map(std::convert::Into::into)
                    .map_err(|_| de::Error::invalid_value(de::Unexpected::Str(v), &self))
            }

            #[inline]
            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                VersionGroupURI::try_from(Cow::Borrowed(v))
                    .map(std::convert::Into::into)
                    .map_err(|_| de::Error::invalid_value(de::Unexpected::Str(v), &self))
            }

            #[inline]
            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                VersionGroupURI::try_from(Cow::Owned(v)).map_err(|err| {
                    de::Error::invalid_value(de::Unexpected::Str(&err.source), &self)
                })
            }
        }

        deserializer.deserialize_any(VersionGroupURIVisitor::<'a>(std::marker::PhantomData))
    }
}

/// A set of version group uris.
///
/// To build this type, use the `Into` trait.
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)]
pub struct VersionGroupURIs<'a> {
    #[serde(borrow)]
    v: Vec<VersionGroupURI<'a>>,
}
impl<'a> VersionGroupURIs<'a> {
    fn versions(&self) -> &[VersionGroupURI<'a>] {
        &self.v
    }
}
impl<'a> Deref for VersionGroupURIs<'a> {
    type Target = [VersionGroupURI<'a>];

    fn deref(&self) -> &Self::Target {
        self.versions()
    }
}
impl<'a> From<Vec<VersionGroupURI<'a>>> for VersionGroupURIs<'a> {
    fn from(v: Vec<VersionGroupURI<'a>>) -> Self {
        Self { v }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_test::{assert_de_tokens, Token};

    macro_rules! declare_tests {
        ($(
            $(#[$cfg:meta])*
            $name:ident { $($value:expr => $tokens:expr,)+ }
        )+) => {
            $(
                $(#[$cfg])*
                #[test]
                fn $name() {
                    $(
                        // Test ser/de roundtripping
                        assert_de_tokens(&$value, $tokens);
                    )+
                }
            )+
        }
    }

    declare_tests! {
        test_uri {
            VersionGroupURI { source: Cow::Borrowed("my.api_group:1.0.0"), index: 12 } => &[
                Token::Str("my.api_group:1.0.0"),
            ],
            VersionGroupURI { source: Cow::Borrowed("my.api_group:1.0.0"), index: 12 } => &[
                Token::BorrowedStr("my.api_group:1.0.0"),
            ],
            VersionGroupURI { source: Cow::Borrowed("my.api_group:1.0.0"), index: 12 } => &[
                Token::String("my.api_group:1.0.0"),
            ],
        }
        test_uris {
            VersionGroupURIs { v: vec![
                VersionGroupURI { source: Cow::Borrowed("my.api_group:1.0.0"), index: 12 },
                VersionGroupURI { source: Cow::Borrowed("my.second.api_group:1.2.0"), index: 19 },
            ] } => &[
                Token::Map { len: Some(1) },
                    Token::Str("v"),
                    Token::Seq { len: Some(2) },
                        Token::Str("my.api_group:1.0.0"),
                        Token::Str("my.second.api_group:1.2.0"),
                    Token::SeqEnd,
                Token::MapEnd,
            ],
        }
    }

    #[test]
    fn uri_from_str_works() {
        {
            let uri = VersionGroupURI::try_from("my.api_group:1.0.0").unwrap();
            assert_eq!("my.api_group", uri.api_group());
            assert_eq!("1.0.0", uri.version());
        }
    }

    #[test]
    fn uri_from_string_works() {
        {
            let uri = VersionGroupURI::try_from("my.api_group:1.0.0".to_string()).unwrap();
            assert_eq!("my.api_group", uri.api_group());
            assert_eq!("1.0.0", uri.version());
        }
    }

    #[test]
    fn uri_from_cow_works() {
        {
            let uri = VersionGroupURI::try_from(Cow::Borrowed("my.api_group:1.0.0")).unwrap();
            assert_eq!("my.api_group", uri.api_group());
            assert_eq!("1.0.0", uri.version());
        }
    }

    #[quickcheck]
    fn qc_uri_from_str_works(input: String) {
        qc_uri_from_str_works_fn(&input);
    }

    fn qc_uri_from_str_works_fn(input: &str) {
        let result = VersionGroupURI::try_from(input);

        if let Some(index) = input.find(':') {
            if index == 0 || index + 1 >= input.len() {
                assert!(result.is_err(), "Missing api_group or version.");
            } else {
                if let Ok(uri) = result {
                    assert_eq!(&input[..index], uri.api_group());
                    assert_eq!(&input[(index + 1)..], uri.version());
                } else {
                    unreachable!("Parsing should have succeeded.");
                }
            }
        } else {
            assert!(result.is_err(), "Missing ':' token.");
        }
    }
}