mqtt-protocol 0.3.0

MQTT Protocol Library
Documentation
use std::io::{Read, Write};
use std::fmt;
use std::error::Error;
use std::ops::Deref;
use std::mem;
use std::convert::Into;

use regex::Regex;

use {Encodable, Decodable};
use encodable::StringEncodeError;

const VALIDATE_TOPIC_FILTER_REGEX: &'static str =
    r"^(#|((\+|\$?[^/\$\+#]+)?(/(\+|[^/\$\+#]+))*?(/(\+|#|[^/\$\+#]+))?))$";

#[derive(Debug, Eq, PartialEq, Clone)]
pub struct TopicFilter(String);

impl TopicFilter {
    pub fn new_checked<S: Into<String>>(topic: S) -> Result<TopicFilter, TopicFilterError> {
        let topic = topic.into();
        let re = Regex::new(VALIDATE_TOPIC_FILTER_REGEX).unwrap();
        if topic.is_empty() || topic.as_bytes().len() > 65535 || !re.is_match(&topic[..]) {
            Err(TopicFilterError::InvalidTopicFilter(topic))
        } else {
            Ok(TopicFilter(topic))
        }
    }

    pub fn new<S: Into<String>>(topic: S) -> TopicFilter {
        TopicFilter(topic.into())
    }
}

impl<'a> Encodable<'a> for TopicFilter {
    type Err = TopicFilterError;

    fn encode<W: Write>(&self, writer: &mut W) -> Result<(), TopicFilterError> {
        (&self.0[..]).encode(writer).map_err(TopicFilterError::StringEncodeError)
    }

    fn encoded_length(&self) -> u32 {
        (&self.0[..]).encoded_length()
    }
}

impl<'a> Decodable<'a> for TopicFilter {
    type Err = TopicFilterError;
    type Cond = ();

    fn decode_with<R: Read>(reader: &mut R, _rest: Option<()>) -> Result<TopicFilter, TopicFilterError> {
        let topic_filter: String = try!(Decodable::decode(reader).map_err(TopicFilterError::StringEncodeError));
        TopicFilter::new_checked(topic_filter)
    }
}

impl Deref for TopicFilter {
    type Target = TopicFilterRef;

    fn deref(&self) -> &TopicFilterRef {
        TopicFilterRef::new(&self.0)
    }
}

#[derive(Debug, Eq, PartialEq)]
pub struct TopicFilterRef(str);

impl TopicFilterRef {
    pub fn new_checked<S: AsRef<str> + ?Sized>(topic: &S) -> Result<&TopicFilterRef, TopicFilterError> {
        let re = Regex::new(VALIDATE_TOPIC_FILTER_REGEX).unwrap();
        let topic = topic.as_ref();
        if topic.is_empty() || topic.as_bytes().len() > 65535 || !re.is_match(&topic[..]) {
            Err(TopicFilterError::InvalidTopicFilter(topic.to_owned()))
        } else {
            Ok(unsafe { mem::transmute(topic) })
        }
    }

    pub fn new<S: AsRef<str> + ?Sized>(topic: &S) -> &TopicFilterRef {
        unsafe { mem::transmute(topic.as_ref()) }
    }
}

impl Deref for TopicFilterRef {
    type Target = str;

    fn deref(&self) -> &str {
        &self.0
    }
}

#[derive(Debug)]
pub enum TopicFilterError {
    StringEncodeError(StringEncodeError),
    InvalidTopicFilter(String),
}

impl fmt::Display for TopicFilterError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            &TopicFilterError::StringEncodeError(ref err) => err.fmt(f),
            &TopicFilterError::InvalidTopicFilter(ref topic) => write!(f, "Invalid topic filter ({})", topic),
        }
    }
}

impl Error for TopicFilterError {
    fn description(&self) -> &str {
        match self {
            &TopicFilterError::StringEncodeError(ref err) => err.description(),
            &TopicFilterError::InvalidTopicFilter(..) => "Invalid topic filter",
        }
    }

    fn cause(&self) -> Option<&Error> {
        match self {
            &TopicFilterError::StringEncodeError(ref err) => Some(err),
            &TopicFilterError::InvalidTopicFilter(..) => None,
        }
    }
}

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

    #[test]
    fn test_topic_filter_validate() {
        let topic = "#".to_owned();
        TopicFilter::new_checked(topic).unwrap();

        let topic = "sport/tennis/player1".to_owned();
        TopicFilter::new_checked(topic).unwrap();

        let topic = "sport/tennis/player1/ranking".to_owned();
        TopicFilter::new_checked(topic).unwrap();

        let topic = "sport/tennis/player1/#".to_owned();
        TopicFilter::new_checked(topic).unwrap();

        let topic = "#".to_owned();
        TopicFilter::new_checked(topic).unwrap();

        let topic = "sport/tennis/#".to_owned();
        TopicFilter::new_checked(topic).unwrap();

        let topic = "sport/tennis#".to_owned();
        assert!(TopicFilter::new_checked(topic).is_err());

        let topic = "sport/tennis/#/ranking".to_owned();
        assert!(TopicFilter::new_checked(topic).is_err());

        let topic = "+".to_owned();
        TopicFilter::new_checked(topic).unwrap();

        let topic = "+/tennis/#".to_owned();
        TopicFilter::new_checked(topic).unwrap();

        let topic = "sport+".to_owned();
        assert!(TopicFilter::new_checked(topic).is_err());

        let topic = "sport/+/player1".to_owned();
        TopicFilter::new_checked(topic).unwrap();

        let topic = "+/+".to_owned();
        TopicFilter::new_checked(topic).unwrap();

        let topic = "$SYS/#".to_owned();
        TopicFilter::new_checked(topic).unwrap();

        let topic = "$SYS".to_owned();
        TopicFilter::new_checked(topic).unwrap();
    }
}