ichen-openprotocol 0.5.0

iChen Open Protocol access library.
Documentation
use super::TextID;
use derive_more::*;
use lazy_static::*;
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::convert::{TryFrom, TryInto};
use std::net::Ipv4Addr;
use std::num::{NonZeroU16, NonZeroU8};
use std::str::FromStr;

lazy_static! {
    static ref IP_REGEX: Regex =
        Regex::new(r#"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$"#).unwrap();
    static ref TTY_REGEX: Regex = Regex::new(r#"^tty\w+$"#).unwrap();
}

/// A data structure holding a controller's physical address.
///
#[derive(Debug, Display, PartialEq, Eq, Hash, Clone)]
pub enum Address<'a> {
    /// Address unknown.
    #[display(fmt = "0.0.0.0:0")]
    Unknown,
    //
    /// An IP v.4 address plus port.
    #[display(fmt = "{}:{}", _0, _1)]
    IPv4(Ipv4Addr, NonZeroU16),
    //
    /// A Windows COM port.
    #[display(fmt = "COM{}", _0)]
    ComPort(NonZeroU8),
    //
    /// A UNIX-style tty serial port device.
    #[display(fmt = "{}", _0)]
    TtyDevice(TextID<'a>),
}

impl<'a> Address<'a> {
    /// Create a new `Address::IPv4` from an IP address string and port number.
    ///
    /// The IP address cannot be unspecified (e.g. `0.0.0.0`).
    /// The IP port cannot be zero.
    ///
    /// # Errors
    ///
    /// Returns `Err(String)` if:
    /// * The IP address string is invalid,
    /// * The IP address is unspecified (e.g. `0.0.0.0`),
    /// * The IP port is zero.
    ///
    /// ## Error Examples
    ///
    /// ~~~
    /// # use ichen_openprotocol::*;
    /// assert_eq!(Err("invalid IP address: [hello]".into()), Address::new_ipv4("hello", 123));
    /// assert_eq!(Err("IP port cannot be zero".into()), Address::new_ipv4("1.02.003.004", 0));
    /// assert_eq!(Err("invalid null IP address".into()), Address::new_ipv4("0.00.000.0", 123));
    /// ~~~
    ///
    /// # Examples
    ///
    /// ~~~
    /// # use ichen_openprotocol::*;
    /// # use std::str::FromStr;
    /// # use std::net::Ipv4Addr;
    /// # use std::num::NonZeroU16;
    /// # fn main() -> std::result::Result<(), String> {
    /// assert_eq!(
    ///     Address::IPv4(Ipv4Addr::from_str("1.2.3.4").unwrap(), NonZeroU16::new(5).unwrap()),
    ///     Address::new_ipv4("1.02.003.004", 5)?
    /// );
    /// # Ok(())
    /// # }
    /// ~~~
    pub fn new_ipv4(addr: &str, port: u16) -> Result<Self, String> {
        let addr =
            Ipv4Addr::from_str(addr).map_err(|_| format!("invalid IP address: [{}]", addr))?;

        if !addr.is_unspecified() {
            Ok(Self::IPv4(addr, NonZeroU16::new(port).ok_or("IP port cannot be zero")?))
        } else {
            Err("invalid null IP address".into())
        }
    }

    /// Create a new `Address::ComPort` from a Windows serial port number.
    ///
    /// The COM port number cannot be zero.
    ///
    /// # Errors
    ///
    /// Returns `Err(String)` if the COM port number is zero.
    ///
    /// ## Error Examples
    ///
    /// ~~~
    /// # use ichen_openprotocol::*;
    /// assert_eq!(Err("COM port cannot be zero".into()), Address::new_com_port(0));
    /// ~~~
    ///
    /// # Examples
    ///
    /// ~~~
    /// # use ichen_openprotocol::*;
    /// # use std::num::NonZeroU8;
    /// # fn main() -> std::result::Result<(), String> {
    /// assert_eq!(
    ///     Address::ComPort(NonZeroU8::new(5).unwrap()),
    ///     Address::new_com_port(5)?
    /// );
    /// # Ok(())
    /// # }
    /// ~~~
    pub fn new_com_port(port: u8) -> Result<Self, String> {
        Ok(Self::ComPort(NonZeroU8::new(port).ok_or("COM port cannot be zero")?))
    }

    /// Create a new `Address::TtyDevice` from a UNIX-style tty device name.
    ///
    /// The device name should start with `tty`.
    ///
    /// # Errors
    ///
    /// Returns `Err(String)` if the device name is not valid for a tty.
    ///
    /// ## Error Examples
    ///
    /// ~~~
    /// # use ichen_openprotocol::*;
    /// assert_eq!(Err("invalid tty device: [hello]".into()), Address::new_tty_device("hello"));
    /// ~~~
    ///
    /// # Examples
    ///
    /// ~~~
    /// # use ichen_openprotocol::*;
    /// # fn main() -> std::result::Result<(), String> {
    /// # use std::borrow::Cow;
    /// assert_eq!(
    ///     Address::TtyDevice(TextID::new("ttyHello").unwrap()),
    ///     Address::new_tty_device("ttyHello")?
    /// );
    /// # Ok(())
    /// # }
    /// ~~~
    pub fn new_tty_device(device: &'a str) -> Result<Self, String> {
        if TTY_REGEX.is_match(device) {
            Ok(Address::TtyDevice(device.try_into()?))
        } else {
            Err(format!("invalid tty device: [{}]", device))
        }
    }
}

impl<'a> TryFrom<&'a str> for Address<'a> {
    type Error = String;

    /// Parse a text string into an `Address`.
    ///
    /// # Errors
    ///
    /// Returns `Err(String)` if the input string is not recognized as a valid address.
    ///
    /// ## Error Examples
    ///
    /// ~~~
    /// # use ichen_openprotocol::*;
    /// # use std::convert::TryFrom;
    /// // The following should error because port cannot be zero if IP address is not zero
    /// assert_eq!(
    ///     Err("IP port cannot be zero".into()),
    ///     Address::try_from("1.02.003.004:0")
    /// );
    ///
    /// // The following should error because port must be zero if IP address is zero
    /// assert_eq!(
    ///     Err("null IP must have zero port number".into()),
    ///     Address::try_from("0.0.0.0:123")
    /// );
    /// ~~~
    ///
    /// # Examples
    ///
    /// ~~~
    /// # use ichen_openprotocol::*;
    /// # use std::convert::TryFrom;
    /// # use std::borrow::Cow;
    /// # use std::str::FromStr;
    /// # use std::num::{NonZeroU16, NonZeroU8};
    /// # use std::net::Ipv4Addr;
    /// # fn main() -> std::result::Result<(), String> {
    /// assert_eq!(
    ///     Address::IPv4(Ipv4Addr::from_str("1.2.3.4").unwrap(), NonZeroU16::new(5).unwrap()),
    ///     Address::try_from("1.02.003.004:05")?
    /// );
    ///
    /// // 0.0.0.0:0 is OK because both IP address and port are zero
    /// assert_eq!(Address::Unknown, Address::try_from("0.0.0.0:0")?);
    ///
    /// assert_eq!(
    ///     Address::ComPort(NonZeroU8::new(123).unwrap()),
    ///     Address::try_from("COM123")?
    /// );
    ///
    /// assert_eq!(
    ///     Address::TtyDevice(TextID::new("ttyABC").unwrap()),
    ///     Address::try_from("ttyABC")?
    /// );
    /// # Ok(())
    /// # }
    /// ~~~
    fn try_from(item: &'a str) -> std::result::Result<Self, Self::Error> {
        const PREFIX_COM: &str = "COM";

        Ok(match item {
            // Match COM port syntax
            text if text.starts_with(PREFIX_COM) => {
                let port = &text[PREFIX_COM.len()..];
                let port =
                    u8::from_str(port).map_err(|_| format!("invalid COM port: [{}]", port))?;
                Address::new_com_port(port)?
            }
            //
            // Match tty syntax
            text if TTY_REGEX.is_match(text) => Address::new_tty_device(text)?,
            //
            // Match IP:port syntax
            text if IP_REGEX.is_match(text) => {
                // Check IP address validity
                let (address, port) = text.split_at(text.find(':').unwrap());

                let address = Ipv4Addr::from_str(address).map_err(|_| "invalid IP address")?;

                // Check port
                let port = &port[1..];

                match u16::from_str(port) {
                    // Allow port 0 on unspecified addresses only
                    Ok(0) => {
                        if !address.is_unspecified() {
                            return Err("IP port cannot be zero".into());
                        } else {
                            Address::Unknown
                        }
                    }
                    // Port must be 0 on unspecified addresses
                    Ok(p) => {
                        if address.is_unspecified() {
                            return Err("null IP must have zero port number".into());
                        } else {
                            Address::IPv4(address, NonZeroU16::new(p).unwrap())
                        }
                    }
                    // Other errors
                    Err(_) => return Err(format!("invalid IP port: [{}]", port)),
                }
            }
            // Failed to match any address type
            _ => return Err(format!("invalid address: [{}]", item)),
        })
    }
}

impl Serialize for Address<'_> {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        Serialize::serialize(&self.to_string(), serializer)
    }
}

impl<'a, 'de: 'a> Deserialize<'de> for Address<'a> {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let s: &str = Deserialize::deserialize(deserializer)?;
        Address::try_from(s).map_err(|err| serde::de::Error::custom(format!("{}: [{}]", err, s)))
    }
}