contack 0.9.2

A simple and easy contact library.
Documentation
//! Components represent a single line of VCard.
//!
//! See the [`Component`] documentation for more.

use super::escape;
use std::collections::HashMap;
use std::fmt;
use std::fmt::Write;
use std::str::FromStr;
pub mod error;
pub use error::*;
use lazy_static::lazy_static;

/// A Component represents one single line of VCard. It has a name, it might
/// have a group, and some parameters.
///
/// This is how it will look when turned into a string:
/// ```vcard
/// GROUP.NAME;PARAM=VAL;PARAM2=VAL2:VALUE1;VALUE2
/// ```
///
/// I would like to point out that Component does not stop you doing stupid
/// things. Please do not add '=' signs to values (for the same reason you
/// don't do that normally.)
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct Component {
    /// This is the group of the VCard. Must be alphanumeric + '-'.
    pub(crate) group: Option<String>,
    /// This is the name of the VCard property.
    pub name: String,
    /// These are the parameters `PARAM=VAL`.
    pub parameters: HashMap<String, String>,
    /// The values of the Component. They are an array
    /// of arrays. Major properties are separated by
    /// `;` and minor properties by `,`.
    pub values: Vec<Vec<String>>,
}

impl Component {
    /// Creates a component from a name.
    ///
    /// # Example
    ///
    /// Create a `FN` component
    ///
    /// ```rust
    ///
    /// use contack::read_write::Component;
    /// assert_eq!(
    ///     Component::new("FN".to_string()).to_string(),
    ///     "FN:\r\n"
    /// )
    /// ```
    #[must_use]
    pub fn new(name: String) -> Self {
        Self {
            name,
            ..Self::default()
        }
    }

    /// Sets the group to be the given value. Returns false on a
    /// group containing characters not included in [a-zA-Z0-9-],
    /// indicating the value has not been set.
    pub fn set_group(&mut self, group: Option<String>) -> bool {
        if let Some(ref group) = group {
            // Asserts all the chars are in the correct character.
            for (_, c) in group.char_indices() {
                if !(c.is_alphanumeric() || c == '-') {
                    return false;
                }
            }
        }

        if let Some(group) = group {
            if group.is_empty() {
                self.group = None;
            } else {
                self.group = Some(group);
            }
        } else {
            self.group = group;
        }
        true
    }

    /// Get's this' group
    #[must_use]
    pub const fn get_group(&self) -> &Option<String> {
        &self.group
    }
}

impl fmt::Display for Component {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut data = String::new();
        // If we have a group add it
        if let Some(group) = &self.group {
            write!(data, "{}.", group)?;
        }

        // Add the name
        write!(data, "{}", self.name)?;

        data.push(';');

        // Add the parameters
        for (key, val) in &self.parameters {
            write!(data, "{}=\"{}\";", key.as_str(), val.as_str())?;
        }

        // Remove the ending semicolon
        data.pop();

        // Indicate that we are moving onto values
        data.push(':');

        // Add the values
        for (i, val) in self.values.iter().enumerate() {
            for subval in val {
                write!(data, "{},", escape::escape_property(subval.as_str()))?;
            }
            if !val.is_empty() {
                // Remove the comma
                data.pop();
            }

            // And add a semicolon
            if i != self.values.len() - 1 {
                data.push(';');
            }
        }

        // Fold the data
        data = escape::fold_line(data);

        // Add the \r\n (Eugh Windows)
        data.push_str("\r\n");

        write!(f, "{}", data)
    }
}

impl FromStr for Component {
    type Err = ComponentParseError;

    fn from_str(string: &str) -> Result<Self, Self::Err> {
        use regex::Regex;

        // Unfold the line
        let string = escape::unfold_line(string);

        lazy_static! {
            static ref REGEX: Regex = Regex::new(r#"^((?P<group>[a-zA-Z0-9-]+)\.)?(?P<name>[a-zA-Z0-9-]+)[:;]?(?P<params>([a-zA-Z0-9-]+=(".+?"|[^"]+?);?)*?):(?P<values>.*)"#).unwrap();
        }

        let caps = match REGEX.captures(&string) {
            Some(caps) => caps,
            None => return Err(ComponentParseError::Invalid(string.clone())),
        };

        return Ok(Self {
            group: caps.name("group").map(|x| x.as_str().to_string()),
            name: match caps.name("name") {
                Some(x) => x.as_str().to_string().to_uppercase(),
                None => return Err(ComponentParseError::NoName),
            },
            parameters: caps
                .name("params")
                .map(|x| escape::get_parameters(x.as_str().to_string()))
                .unwrap_or_default()
                .into_iter()
                .map(|(k, v)| (k.to_uppercase(), v))
                .collect(),
            values: match caps.name("values") {
                Some(x) => escape::get_values(x.as_str().to_string()),
                None => return Err(ComponentParseError::NoValue),
            },
        });
    }
}