sqlite-collections 0.1.0

Rust collection types backed by sqlite database files.
Documentation
use core::fmt;
use std::{
    borrow::Cow,
    ops::{Add, AddAssign, Deref},
};

mod error;
pub use error::Error;

/// An identifier wrapper for SQLite identifiers.  This checks validity and
/// wraps the identifier in quotes with escapes for protection.
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub struct Identifier<'a> {
    inner: Cow<'a, str>,
    quoted: String,
}

impl<'a> TryFrom<Cow<'a, str>> for Identifier<'a> {
    type Error = Error;

    fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> {
        if value.find('\0').is_some() {
            return Err(Error::NullCharacter);
        }
        let len = 2 + value.chars().filter(|&c| c == '"').count() + value.len();
        let mut quoted = String::new();
        quoted.reserve_exact(len);
        {
            quoted.push('"');
            let mut value: &str = &value;
            loop {
                match value.find('"') {
                    Some(index) => {
                        quoted.push_str(&value[..=index]);
                        quoted.push('"');

                        value = &value[index + 1..];
                    }
                    None => {
                        quoted.push_str(value);
                        break;
                    }
                }
            }
            quoted.push('"');
        }
        Ok(Identifier {
            inner: value,
            quoted,
        })
    }
}

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

    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
        Identifier::try_from(Cow::Borrowed(value))
    }
}

impl TryFrom<String> for Identifier<'static> {
    type Error = Error;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Identifier::try_from(Cow::Owned(value))
    }
}

impl<'a> From<Identifier<'a>> for Cow<'a, str> {
    fn from(value: Identifier<'a>) -> Self {
        value.inner
    }
}

impl<'a> Deref for Identifier<'a> {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl<'a, T> AsRef<T> for Identifier<'a>
where
    T: ?Sized,
    <Identifier<'a> as Deref>::Target: AsRef<T>,
{
    fn as_ref(&self) -> &T {
        self.deref().as_ref()
    }
}
impl<'a, 'b> Add<&Identifier<'b>> for Identifier<'a> {
    type Output = Identifier<'a>;

    fn add(mut self, rhs: &Identifier) -> Self::Output {
        self += rhs;
        self
    }
}

impl<'a, 'b> AddAssign<&Identifier<'b>> for Identifier<'a> {
    fn add_assign(&mut self, rhs: &Identifier) {
        let inner = self.inner.to_mut();
        inner.reserve_exact(rhs.inner.len());
        *inner += &rhs.inner;

        // Don't need rhs's quotation marks.
        self.quoted.reserve_exact(rhs.quoted.len() - 2);
        self.quoted.pop();
        self.quoted += &rhs.quoted[1..];
    }
}

impl<'a> fmt::Display for Identifier<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.quoted.fmt(f)
    }
}

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

    #[test]
    fn tests() {
        assert_eq!(
            Identifier::try_from("main").unwrap().to_string(),
            String::from("\"main\""),
        );
        assert_eq!(
            Identifier::try_from(String::from("ma\"in"))
                .unwrap()
                .to_string(),
            String::from("\"ma\"\"in\""),
        );
        assert_eq!(
            Identifier::try_from(String::from("\"main"))
                .unwrap()
                .to_string(),
            String::from("\"\"\"main\""),
        );
        assert_eq!(
            Identifier::try_from(String::from("main\""))
                .unwrap()
                .to_string(),
            String::from("\"main\"\"\""),
        );
        assert!(Identifier::try_from(String::from("ma\0in")).is_err());
    }

    #[test]
    fn test_add() {
        assert_eq!(
            (Identifier::try_from(String::from("main")).unwrap()
                + &String::from("_after").try_into().unwrap())
                .to_string(),
            "\"main_after\"",
        );
    }
}