rust-version 0.1.0

Crate for parsing Rust versions.
Documentation
use std::char::from_digit;
use std::convert::TryFrom;
use std::error::Error;
use std::fmt;
use std::str::from_utf8_unchecked;
use std::io::{self, Read};
use std::str::from_utf8;

use data_encoding::HEXLOWER_PERMISSIVE;
use either::{Either, Left, Right};
use reqwest::{self, Client};
use reqwest::header::{Accept, qitem};

/// Git commit from the rust-lang/rust repository.
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Commit {
    bytes: [u8; 20],
}
impl From<[u8; 20]> for Commit {
    fn from(bytes: [u8; 20]) -> Commit {
        Commit { bytes }
    }
}
impl From<Commit> for [u8; 20] {
    fn from(c: Commit) -> [u8; 20] {
        c.bytes
    }
}
impl<'a> TryFrom<&'a [u8]> for Commit {
    type Error = ParseCommitError<'a>;
    fn try_from(bytes: &'a [u8]) -> Result<Commit, ParseCommitError<'a>> {
        match bytes.len() {
            40 => {
                let mut buf = [0; 20];
                if HEXLOWER_PERMISSIVE.decode_mut(bytes, &mut buf).is_ok() {
                    Ok(Commit { bytes: buf })
                } else {
                    Err(ParseCommitError::Format(bytes))
                }
            }
            20 => {
                let mut buf = [0; 20];
                buf.copy_from_slice(bytes);
                Ok(Commit { bytes: buf })
            }
            _ => {
                let s = from_utf8(bytes).map_err(
                    |_| ParseCommitError::Format(bytes),
                )?;

                let mut res = do catch {
                    Client::new()?
                        .get(&format!(
                            "https://api.github.com/repos/rust-lang/rust/commits/{}",
                            s
                        ))?
                        .header(Accept(vec![
                            qitem(
                                "application/vnd.github.VERSION.sha".parse().unwrap()
                            ),
                        ]))
                        .send()?
                        .error_for_status()
                }.map_err(|e| ParseCommitError::Nonexistent(bytes, Left(e)))?;

                let mut buf = [0; 40];
                res.read_exact(&mut buf[..]).map_err(|e| {
                    ParseCommitError::Nonexistent(bytes, Right(e))
                })?;
                Commit::try_from(&buf[..]).map_err(|_| {
                    ParseCommitError::Nonexistent(
                        bytes,
                        Right(io::Error::new(
                            io::ErrorKind::InvalidData,
                            "could not parse commit from GitHub",
                        )),
                    )
                })
            }
        }
    }
}
impl<'a> TryFrom<&'a str> for Commit {
    type Error = ParseCommitError<'a>;
    fn try_from(s: &'a str) -> Result<Commit, ParseCommitError<'a>> {
        match s.len() {
            20 => Commit::try_from({
                let mut c = s.chars();
                c.next_back();
                c.as_str()
            }),
            _ => Commit::try_from(s.as_bytes()),
        }
    }
}
impl fmt::Debug for Commit {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(self, f)
    }
}
impl fmt::Display for Commit {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut disp = [0; 40];
        for (src, dest) in self.bytes.iter().zip(disp.chunks_mut(2)) {
            dest[0] = from_digit(u32::from((src & 0xF0) >> 4), 16).unwrap() as u8;
            dest[1] = from_digit(u32::from(src & 0x0F), 16).unwrap() as u8;
        }
        f.pad(unsafe { from_utf8_unchecked(&disp) })
    }
}

/// Error encountered when parsing a [`Commit`].
///
/// [`Commit`]: struct.Commit.html
pub enum ParseCommitError<'a> {
    /// Could not parse the given bytes as a string.
    Format(&'a [u8]),

    /// Only a partial commit was given, but the commit didn't exist.
    Nonexistent(&'a [u8], Either<reqwest::Error, io::Error>),
}
impl<'a> fmt::Debug for ParseCommitError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ParseCommitError::Format(bytes) => {
                f.debug_tuple("ParseCommitError::Format")
                    .field(&String::from_utf8_lossy(bytes))
                    .finish()
            }
            ParseCommitError::Nonexistent(bytes, ref err) => {
                f.debug_tuple("ParseCommitError::Nonexistent")
                    .field(&String::from_utf8_lossy(bytes))
                    .field(err)
                    .finish()
            }
        }
    }
}
impl<'a> fmt::Display for ParseCommitError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ParseCommitError::Format(bytes) => {
                write!(
                    f,
                    "{:?} was not a valid commit string",
                    String::from_utf8_lossy(bytes)
                )
            }
            ParseCommitError::Nonexistent(bytes, ref err) => {
                write!(
                    f,
                    "{:?} is not a full commit string, and an error occurred on lookup: {}",
                    String::from_utf8_lossy(bytes),
                    err
                )
            }
        }
    }
}
impl<'a> Error for ParseCommitError<'a> {
    fn description(&self) -> &str {
        match *self {
            ParseCommitError::Format(_) => "was not a valid commit string",
            ParseCommitError::Nonexistent(_, _) => {
                "was not a full commit string, and a lookup from GitHub failed"
            }
        }
    }
    fn cause(&self) -> Option<&Error> {
        match *self {
            ParseCommitError::Format(_) => None,
            ParseCommitError::Nonexistent(_, Left(ref err)) => Some(err),
            ParseCommitError::Nonexistent(_, Right(ref err)) => Some(err),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::convert::TryFrom;

    use super::{Commit, ParseCommitError};

    #[test]
    fn parse_display() {
        let orig = "1234567890abcdef1234567890abcdef12345678";
        assert_eq!(Commit::try_from(orig).unwrap().to_string(), orig);
    }

    #[test]
    fn parse_invalid() {
        match Commit::try_from("1234567890abcdef123456789xabcdef12345678") {
            Err(ParseCommitError::Format(b"1234567890abcdef123456789xabcdef12345678")) => (),
            e => panic!("{:?}", e),
        }
    }

    #[test]
    fn partial_valid() {
        assert_eq!(
            Commit::try_from("f3d6973f4").unwrap().to_string(),
            "f3d6973f41a7d1fb83029c9c0ceaf0f5d4fd7208"
        );
    }

    #[test]
    fn partial_invalid() {
        match Commit::try_from("123456789") {
            Err(ParseCommitError::Nonexistent(b"123456789", _)) => (),
            e => panic!("{:?}", e),
        }
        match Commit::try_from("whoops") {
            Err(ParseCommitError::Nonexistent(b"whoops", _)) => (),
            e => panic!("{:?}", e),
        }
    }
}