rust-version 0.1.0

Crate for parsing Rust versions.
Documentation
use std::cmp::Ordering;
use std::convert::TryFrom;
use std::error::Error;
use std::fmt;
use std::io;
use std::process::Command;

use beta::{BetaNum, ParseBetaError};

/// Rust version channel.
///
/// Channels only implement `PartialEq` and `PartialOrd` because betas can't be fully compared. See
/// [`BetaNum`] for more information.
///
/// [`BetaNum`]: struct.BetaNum.html
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum Channel {
    /// Stable build.
    Stable,

    /// Beta build.
    Beta(BetaNum),

    /// Nightly build.
    Nightly,
}
impl Channel {
    /// Checks if this channel is "stable".
    pub fn is_stable(self) -> bool {
        match self {
            Channel::Stable => true,
            _ => false,
        }
    }

    /// Checks if this channel is "nightly".
    pub fn is_nightly(self) -> bool {
        match self {
            Channel::Nightly => true,
            _ => false,
        }
    }

    /// Checks if this channel is "beta".
    pub fn is_beta(self) -> bool {
        match self {
            Channel::Beta(_) => true,
            _ => false,
        }
    }

    /// Checks if this channel is "beta.`n`".
    pub fn is_beta_num(self, n: u8) -> bool {
        match self {
            Channel::Beta(m) => n == m,
            _ => false,
        }
    }

    /// A string for the type of channel, i.e. stable, beta, or nightly.
    pub fn as_type_str(self) -> &'static str {
        match self {
            Channel::Stable => "stable",
            Channel::Beta(_) => "beta",
            Channel::Nightly => "nightly",
        }
    }

    /// Updates this channel via rustup.
    pub fn rustup_update(&self) -> io::Result<()> {
        let success = Command::new("rustup")
            .args(&["update", self.as_type_str()])
            .status()?
            .success();
        if success {
            Ok(())
        } else {
            Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "invalid exit code",
            ))
        }
    }

    /// Creates an `Ord` version of this `Channel`, usually for testing.
    pub fn comparable(self) -> OrdChannel {
        OrdChannel(self)
    }
}
impl fmt::Display for Channel {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Channel::Stable => f.pad("stable"),
            Channel::Beta(b) => fmt::Display::fmt(&b, f),
            Channel::Nightly => f.pad("nightly"),
        }
    }
}
impl From<BetaNum> for Channel {
    fn from(beta: BetaNum) -> Channel {
        Channel::Beta(beta)
    }
}
pub use self::Channel::*;

/// `Ord` version of `Channel`, for testing.
#[derive(Copy, Clone, Debug)]
pub struct OrdChannel(pub Channel);
impl PartialEq<Channel> for OrdChannel {
    fn eq(&self, rhs: &Channel) -> bool {
        match (self.0, *rhs) {
            (Stable, Stable) |
            (Nightly, Nightly) => true,
            (Beta(n), Beta(m)) => n.num() == m.num(),
            _ => false,
        }
    }
}
impl PartialEq for OrdChannel {
    fn eq(&self, rhs: &OrdChannel) -> bool {
        self == &rhs.0
    }
}
impl PartialEq<OrdChannel> for Channel {
    fn eq(&self, rhs: &OrdChannel) -> bool {
        rhs == self
    }
}
impl Eq for OrdChannel {}
impl PartialOrd<Channel> for OrdChannel {
    fn partial_cmp(&self, rhs: &Channel) -> Option<Ordering> {
        match (self.0, *rhs) {
            (Beta(n), Beta(m)) => n.partial_cmp(&m),
            (lhs, rhs) => lhs.partial_cmp(&rhs),
        }
    }
}
impl PartialOrd<OrdChannel> for Channel {
    fn partial_cmp(&self, rhs: &OrdChannel) -> Option<Ordering> {
        rhs.partial_cmp(self).map(Ordering::reverse)
    }
}
impl PartialOrd for OrdChannel {
    fn partial_cmp(&self, rhs: &OrdChannel) -> Option<Ordering> {
        self.partial_cmp(&rhs.0)
    }
}
impl Ord for OrdChannel {
    fn cmp(&self, rhs: &OrdChannel) -> Ordering {
        self.partial_cmp(rhs).unwrap()
    }
}

impl<'a> TryFrom<&'a str> for Channel {
    type Error = ParseChannelError<'a>;
    fn try_from(s: &'a str) -> Result<Channel, ParseChannelError<'a>> {
        Channel::try_from(s.as_bytes())
    }
}
impl<'a> TryFrom<&'a [u8]> for Channel {
    type Error = ParseChannelError<'a>;
    fn try_from(bytes: &'a [u8]) -> Result<Channel, ParseChannelError<'a>> {
        if bytes == b"stable" {
            Ok(Channel::Stable)
        } else if bytes == b"nightly" {
            Ok(Channel::Nightly)
        } else {
            BetaNum::try_from(bytes).map(Channel::Beta).map_err(
                From::from,
            )
        }
    }
}

/// Error encountered when parsing a [`Channel`].
///
/// [`Channel`]: enum.Channel.html
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub enum ParseChannelError<'a> {
    /// The string wasn't `"stable"`, `"beta"` or `"beta.N"`, or `"nightly"`.
    Format(&'a [u8]),

    /// The number in `"beta.N"` could not be parsed as a positive number.
    Number(&'a [u8]),

    /// The number in `"beta.N"` was too large.
    Overflow(&'a [u8]),
}
impl<'a> From<ParseBetaError<'a>> for ParseChannelError<'a> {
    fn from(err: ParseBetaError<'a>) -> ParseChannelError<'a> {
        match err {
            ParseBetaError::Format(bytes) => ParseChannelError::Format(bytes),
            ParseBetaError::Number(bytes) => ParseChannelError::Number(bytes),
            ParseBetaError::Overflow(bytes) => ParseChannelError::Overflow(bytes),
        }
    }
}
impl<'a> fmt::Debug for ParseChannelError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ParseChannelError::Format(bytes) => {
                f.debug_tuple("ParseChannelError::Format")
                    .field(&String::from_utf8_lossy(bytes))
                    .finish()
            }
            ParseChannelError::Number(bytes) => {
                f.debug_tuple("ParseChannelError::Number")
                    .field(&String::from_utf8_lossy(bytes))
                    .finish()
            }
            ParseChannelError::Overflow(bytes) => {
                f.debug_tuple("ParseChannelError::Overflow")
                    .field(&String::from_utf8_lossy(bytes))
                    .finish()
            }
        }
    }
}
impl<'a> fmt::Display for ParseChannelError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ParseChannelError::Format(bytes) => {
                write!(
                    f,
                    "could not parse {:?} as a valid Rust channel",
                    String::from_utf8_lossy(bytes)
                )
            }
            ParseChannelError::Number(bytes) => {
                write!(
                    f,
                    "could not parse {:?} as a positive number",
                    String::from_utf8_lossy(bytes)
                )
            }
            ParseChannelError::Overflow(bytes) => {
                write!(
                    f,
                    "could not parse {:?}; was greater than 255",
                    String::from_utf8_lossy(bytes)
                )
            }
        }
    }
}
impl<'a> Error for ParseChannelError<'a> {
    fn description(&self) -> &str {
        match *self {
            ParseChannelError::Format(_) => "could not parse as a valid Rust channel",
            ParseChannelError::Number(_) => {
                "could not parse \"beta.N\" with \"N\" as a positive number"
            }
            ParseChannelError::Overflow(_) => {
                "could not parse \"beta.N\" because \"N\" was greater than 255"
            }
        }
    }
}

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

    use super::{Channel, ParseChannelError, BetaNum};

    #[test]
    fn parse() {
        assert_eq!(Channel::try_from("stable").unwrap(), Channel::Stable);
        assert_eq!(
            Channel::try_from("beta").unwrap().comparable(),
            Channel::Beta(BetaNum::ambiguous())
        );
        assert_eq!(
            Channel::try_from("beta.2").unwrap().comparable(),
            Channel::Beta(BetaNum::new(2))
        );
        assert_eq!(
            Channel::try_from("beta.255").unwrap().comparable(),
            Channel::Beta(BetaNum::new(255))
        );
        assert_eq!(Channel::try_from("nightly").unwrap(), Channel::Nightly);
    }

    #[test]
    fn parse_fail() {
        assert_eq!(
            Channel::try_from("blorp"),
            Err(ParseChannelError::Format(b"blorp"))
        );
        assert_eq!(
            Channel::try_from("beta.1000"),
            Err(ParseChannelError::Overflow(b"1000"))
        );
        assert_eq!(
            Channel::try_from("beta.whoops"),
            Err(ParseChannelError::Number(b"whoops"))
        );
        assert_eq!(
            Channel::try_from("beta.-10"),
            Err(ParseChannelError::Number(b"-10"))
        );
    }
}