1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! Tags for TMI messages

use std::fmt;
use std::hash::BuildHasherDefault;
use std::{collections::HashMap, u32};

/// [`Tags`] is type alias for a [`HashMap`] whose keys are [`&str`] and values [`TagValue`].
/// Uses slice [`&str`] instead of owned [`String`] in order to avoid data duplication.
/// Also uses a custom hasher which implements the 'Fnv 1a' hash function.
///
/// # Examples
///
/// ```
/// # use tmi_parser::*;
/// let mut map = Tags::default();
/// map.insert("hello", TagValue::String("world"));
/// # assert_eq!(*map.get("hello").unwrap(), TagValue::String("world"));
/// ````
pub type Tags<'a> = HashMap<&'a str, TagValue<'a>, BuildHasherDefault<hash::TagsHasher>>;

/// [`TagsHasher`] is an internal implementation of the 'Fnv 1a' hash function.
/// It lives under the private module hash and was made only for improving [`Tags`] performance.
/// Since [`Tags`] will contain few items in average, the faster the hash, the better the performance,
mod hash {
    use std::hash::Hasher;

    pub struct TagsHasher(u64);

    impl Default for TagsHasher {
        #[inline]
        fn default() -> TagsHasher {
            TagsHasher(14695981039346656037)
        }
    }

    impl Hasher for TagsHasher {
        #[inline]
        fn finish(&self) -> u64 {
            self.0
        }

        #[inline]
        fn write(&mut self, bytes: &[u8]) {
            let TagsHasher(mut hash) = *self;

            for byte in bytes.iter() {
                hash ^= *byte as u64;
                hash = hash.wrapping_mul(1099511628211);
            }

            *self = TagsHasher(hash);
        }
    }
}

/// Possible values of message tags.
#[derive(Debug, PartialEq)]
pub enum TagValue<'a> {
    /// Represents a parsed sequence of numbers of type u32.
    /// Used for the `color` tag.
    Color(u32),
    /// Represents a parsed sequence of numbers of type u32.
    /// Used for generic numeric string.
    Number(u32),
    /// Represents a parsed sequence of numbers of type u64.
    /// Used for the `tmi-sent-ts` tag.
    Timestamp(u64),
    /// Boolean values represents literal "1" (true) or "0" (false).
    ///
    /// Note that a single digit number "1" or "0" may be represented
    /// as a Boolean value instead of a Number value.
    /// Type conversion should therefore be done by the user code.
    Boolean(bool),
    /// Strings represent an unparsed string literal.
    String(&'a str),
    /// None represents literal empty string "".
    None,
}

impl<'a> TagValue<'a> {
    /// Returns a TagValue variant based on the given [`&str`].
    pub fn new(val: &'a str) -> TagValue<'a> {
        match val {
            "" => TagValue::None,
            "0" => TagValue::Boolean(false),
            "1" => TagValue::Boolean(true),
            _ => {
                if let Ok(num) = val.parse::<u32>() {
                    TagValue::Number(num)
                } else if let Ok(tm) = val.parse::<u64>() {
                    TagValue::Timestamp(tm)
                } else if val.starts_with('#') {
                    // Try to convert hexadecimal values, used by the 'color' tag, to Number.
                    if let Ok(num) = u32::from_str_radix(&val[1..], 16) {
                        TagValue::Color(num)
                    } else {
                        TagValue::String(val)
                    }
                } else {
                    TagValue::String(val)
                }
            }
        }
    }
}

impl<'a> fmt::Display for TagValue<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TagValue::Color(num) => write!(f, "#{:06X}", num),
            TagValue::Number(num) => write!(f, "{}", num),
            TagValue::Timestamp(num) => write!(f, "{}", num),
            TagValue::Boolean(false) => write!(f, "0"),
            TagValue::Boolean(true) => write!(f, "1"),
            TagValue::String(val) => write!(f, "{}", val),
            TagValue::None => write!(f, ""),
        }
    }
}