contack 0.9.2

A simple and easy contact library.
Documentation
//! # Address
//!
//! This modules contains structures to do with addresses, positions and
//! suchlike.

#[cfg(feature = "read_write")]
use crate::read_write::component::Component;
#[cfg(feature = "read_write")]
use crate::read_write::error::FromComponentError;
#[cfg(feature = "read_write")]
use regex::Regex;
#[cfg(feature = "read_write")]
use std::mem;

#[cfg(feature = "sql")]
use crate::SqlConversionError;

#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[non_exhaustive]
/// A simple structure to hold address information
pub struct Address {
    /// This is the street where the address points to.
    pub street: Option<String>,

    /// This is the locality/city where the address points to.
    pub locality: Option<String>,

    /// This is the region/county where the address points to.
    pub region: Option<String>,

    /// This is the code where the address is. This may be a zip/postal code.
    pub code: Option<String>,

    /// This is the country where ths address points to.
    pub country: Option<String>,

    /// This is the longitude and latitude of the address.
    pub geo: Option<Geo>,
}

#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[non_exhaustive]
/// A simple structure to hold Geo location.
pub struct Geo {
    /// This is the longitude for the geo-location.
    pub longitude: f64,

    /// This is the latitude for the geo-location.
    pub latitude: f64,
}

impl Address {
    /// Creates a new address
    #[must_use]
    pub const fn new(
        street: Option<String>,
        locality: Option<String>,
        region: Option<String>,
        code: Option<String>,
        country: Option<String>,
    ) -> Self {
        Self {
            street,
            locality,
            region,
            code,
            country,
            geo: None,
        }
    }
}

impl Geo {
    /// Creates a `Geo` from longitue and latitude.
    ///
    /// Both are floats and are given in the order `lontigude`
    /// and `latitude`.
    ///
    /// # Examples
    ///
    /// Create a new `Geo` at null island.
    ///
    /// ```rust
    /// use contack::Geo;
    /// let geo = Geo::new(0.0, 0.0);
    /// ```
    ///
    #[must_use]
    pub const fn new(longitude: f64, latitude: f64) -> Self {
        Self {
            longitude,
            latitude,
        }
    }
}

#[cfg(feature = "sql")]
#[derive(Debug, Clone, Default)]
pub(crate) struct AddressRaw {
    pub street: Option<String>,
    pub locality: Option<String>,
    pub region: Option<String>,
    pub code: Option<String>,
    pub country: Option<String>,
    pub geo_long: Option<f64>,
    pub geo_lat: Option<f64>,
}

#[cfg(feature = "sql")]
pub(crate) type AddressRawTuple = (
    Option<String>,
    Option<String>,
    Option<String>,
    Option<String>,
    Option<String>,
    (Option<f64>, Option<f64>),
);

#[cfg(feature = "sql")]
impl AddressRaw {
    /// Creates a new `AddressRaw`
    pub(crate) const fn new(
        street: Option<String>,
        locality: Option<String>,
        region: Option<String>,
        code: Option<String>,
        country: Option<String>,
        geo_long: Option<f64>,
        geo_lat: Option<f64>,
    ) -> Self {
        Self {
            street,
            locality,
            region,
            code,
            country,
            geo_long,
            geo_lat,
        }
    }
}

#[cfg(feature = "sql")]
impl Address {
    /// Converts an address to something that can be used by a `SqlContact`.
    pub(crate) fn to_sql_raw(address: Option<Self>) -> AddressRawTuple {
        match address {
            None => (None, None, None, None, None, (None, None)),
            Some(adr) => (
                adr.street,
                adr.locality,
                adr.region,
                adr.code,
                adr.country,
                match adr.geo {
                    None => (None, None),
                    Some(geo) => (Some(geo.longitude), Some(geo.latitude)),
                },
            ),
        }
    }
}

#[cfg(feature = "sql")]
impl AddressRaw {
    /// This should only be used internally.
    ///
    /// # Errors
    ///
    /// Fails if given an imcomplete geo, requires both
    /// `longitude` and `latitude`.
    pub(crate) fn try_from_sql_raw(
        self,
    ) -> Result<Option<Address>, SqlConversionError> {
        let address = Address {
            // Set up the basic fields
            street: self.street,
            locality: self.locality,
            region: self.region,
            code: self.code,
            country: self.country,

            // Set up the Geo
            geo: {
                match (self.geo_long, self.geo_lat) {
                    (Some(longitude), Some(latitude)) => Some(Geo {
                        longitude,
                        latitude,
                    }),
                    (None, None) => None,
                    _ => return Err(SqlConversionError::IncompleteGeo),
                }
            },
        };

        // Check if the address has all fields.
        if address.street.is_none()
            && address.locality.is_none()
            && address.region.is_none()
            && address.code.is_none()
            && address.country.is_none()
            && address.geo.is_none()
        {
            Ok(None)
        } else {
            Ok(Some(address))
        }
    }
}

#[cfg(feature = "read_write")]
impl From<Address> for Component {
    fn from(adr: Address) -> Self {
        Self {
            // No Group
            group: None,

            // The parameters will probably be appeneded to later, to add
            // a type=(work|home), if done through Contact
            parameters: match adr.geo {
                Some(org) => {
                    let mut hashmap = std::collections::HashMap::new();
                    hashmap.insert(
                        "geo".to_string(),
                        format!("{},{}", org.longitude, org.latitude),
                    );
                    hashmap
                }
                None => std::collections::HashMap::default(),
            },

            name: "ADR".to_string(),

            values: vec![
                vec![],
                vec![],
                adr.street.map(|x| vec![x]).unwrap_or_default(),
                adr.locality.map(|x| vec![x]).unwrap_or_default(),
                adr.region.map(|x| vec![x]).unwrap_or_default(),
                adr.code.map(|x| vec![x]).unwrap_or_default(),
                adr.country.map(|x| vec![x]).unwrap_or_default(),
            ],
        }
    }
}

#[cfg(feature = "read_write")]
impl TryFrom<Component> for Address {
    type Error = FromComponentError;

    fn try_from(mut comp: Component) -> Result<Self, Self::Error> {
        lazy_static! {
            static ref RE_ORG: Regex =
                Regex::new(r#"\d+(\.\d+)?,\d+(\.\d+)?"#).unwrap();
        }

        // Get rid of extended address and postbox, because, supposedly
        // they are not longer supported because they are inconsistent.
        Ok(Self {
            street: mem::take(&mut comp.values.get_mut(2)).and_then(Vec::pop),
            locality: mem::take(&mut comp.values.get_mut(3)).and_then(Vec::pop),
            region: mem::take(&mut comp.values.get_mut(4)).and_then(Vec::pop),
            code: mem::take(&mut comp.values.get_mut(5)).and_then(Vec::pop),
            country: mem::take(&mut comp.values.get_mut(6)).and_then(Vec::pop),
            geo: match comp.parameters.get("GEO") {
                Some(geo) => {
                    // Find the GEO part, removing the URI part
                    let geo = RE_ORG
                        .find(geo)
                        .ok_or(FromComponentError::InvalidRegex)?
                        .as_str();

                    // Split it by the comma, this should give us the
                    // long and lat.
                    let split: Vec<String> =
                        geo.split(',').map(|x| x.trim().to_owned()).collect();

                    // Checks that it gives two values
                    if split.len() != 2 {
                        return Err(FromComponentError::NotEnoughValues);
                    }

                    // Build the Geo
                    Some(Geo {
                        longitude: split[0]
                            .parse()
                            .map_err(FromComponentError::ParseFloatError)?,
                        latitude: split[1]
                            .parse()
                            .map_err(FromComponentError::ParseFloatError)?,
                    })
                }
                None => None,
            },
        })
    }
}