orql 0.1.0

A toy SQL parser for a subset of the Oracle dialect.
Documentation
use std::{borrow::Cow, ops::Deref};

/// Represents string literals in their various forms,
/// e.g. `'hello, world'`.
#[derive(Debug, PartialEq, Eq)]
pub enum Text<'s> {
    /// regular text / string literal, e.g. `'...'`
    Regular(Cow<'s, str>),
    /// a quote delimited text literal, e.g. `Q'[...]'`
    Quoted(QuotedText<'s>),
}

impl<'s> Deref for Text<'s> {
    type Target = str;

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

impl<'s> Text<'s> {
    pub fn as_str(&self) -> &str {
        match self {
            Text::Regular(t) => t,
            Text::Quoted(t) => t,
        }
    }
}

impl<'s> std::fmt::Display for Text<'s> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        crate::fmt::Display::fmt(self, f)
    }
}

impl<'s> crate::fmt::Display for Text<'s> {
    fn fmt(&self, f: &mut impl crate::fmt::Formatter) -> std::fmt::Result {
        match self {
            Text::Regular(s) => {
                f.write_char('\'')?;
                let mut last = None::<&str>;
                let mut parts = s.split_inclusive('\'');
                if let Some(first) = parts.next() {
                    f.write_str(first)?;
                    last = Some(first);
                }
                for part in parts {
                    f.write_char('\'')?;
                    f.write_str(part)?;
                    last = Some(part);
                }
                if let Some(last) = last
                    && last.ends_with('\'')
                {
                    f.write_char('\'')?;
                }
                f.write_char('\'')
            }
            Text::Quoted(t) => {
                f.write_str("Q'")?;
                f.write_str(t.as_str_with_delimiters())?;
                f.write_char('\'')
            }
        }
    }
}

/// A quote delimited text literal, e.g. `Q'{...}'`.
#[derive(Debug, PartialEq, Eq)]
// ~ wrapped string is _not_ exposed on purposes; its first and last character
// are the quote delimiters which are an impl details hidden from the public
// ~ XXX probably make this a `Cow<'s, str>` so that modifying the text is easier
pub struct QuotedText<'s>(&'s str);

impl<'s> QuotedText<'s> {
    /// Creates a new quoted text with the first and last character being the
    /// "quote delimiters."
    ///
    /// 1. Panics if `s` is shorter than 2 characters.
    /// 2. Further, is assumed that `s` does _not_ contain the
    ///    `"<end-quote-delimiter>'"` sequence, i.e the end-quote-delimiter
    ///    followed by a quote character.
    pub(crate) fn new_unchecked(s: &'s str) -> Self {
        if cfg!(debug_assertions) {
            Self::try_from(s).unwrap();
        }
        Self(s)
    }

    pub fn quote_delimiters(&self) -> (char, char) {
        let mut chars = self.0.chars();
        (
            chars.next().expect("empty string"),
            chars.last().expect("empty string"),
        )
    }

    pub(crate) fn as_str_with_delimiters(&self) -> &str {
        self.0
    }

    pub fn as_str(&self) -> &str {
        let mut chars = self.0.char_indices();
        let i = chars.next().expect("empty string").1.len_utf8();
        let j = chars.last().expect("empoty string").0;
        &self.0[i..j]
    }
}

/// Possible conversion errors when trying to construct a [QuotedText] via [QuotedText::try_from].
#[derive(Debug, PartialEq, Eq)]
pub enum QuotedTextFromError {
    TooShort,
    InvalidDelimiters,
    InvalidContent,
}

impl std::fmt::Display for QuotedTextFromError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        crate::fmt::Display::fmt(self, f)
    }
}

impl crate::fmt::Display for QuotedTextFromError {
    fn fmt(&self, f: &mut impl crate::fmt::Formatter) -> std::fmt::Result {
        let msg = match self {
            QuotedTextFromError::TooShort => "text too short; must be at least two characters long",
            QuotedTextFromError::InvalidDelimiters => {
                "invalid delimiters; text must be enclosed with the same character or `{...}`, `[...]`, `<...>`, `(...)`"
            }
            QuotedTextFromError::InvalidContent => {
                "invalid text; must not contain end-quote-delimiter followed by single quote"
            }
        };
        f.write_str(msg)
    }
}

impl<'s> TryFrom<&'s str> for QuotedText<'s> {
    type Error = QuotedTextFromError;

    /// Parses the given string into a [QuotedText].
    fn try_from(s: &'s str) -> Result<Self, Self::Error> {
        let mut chars = s.chars();
        let end_delim = match (chars.next(), chars.last()) {
            (Some(start_delim), Some(end_delim)) => {
                match (start_delim, end_delim) {
                    ('{', '}') | ('[', ']') | ('<', '>') | ('(', ')') => {}
                    (q1, q2) if q1 == q2 => {}
                    _ => {
                        return Err(QuotedTextFromError::InvalidDelimiters);
                    }
                }
                end_delim
            }
            _ => return Err(QuotedTextFromError::TooShort),
        };

        let mut buf = [0u8; 5];
        let n = end_delim.encode_utf8(&mut buf).len();
        buf[n] = b'\'';
        let end_delim_quote = unsafe { str::from_utf8_unchecked(&buf[..n + 1]) };
        if s.contains(end_delim_quote) {
            Err(QuotedTextFromError::InvalidContent)
        } else {
            Ok(Self(s))
        }
    }
}

impl<'s> Deref for QuotedText<'s> {
    type Target = str;

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

impl<'s> std::fmt::Display for QuotedText<'s> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        crate::fmt::Display::fmt(self, f)
    }
}

impl<'s> crate::fmt::Display for QuotedText<'s> {
    fn fmt(&self, f: &mut impl crate::fmt::Formatter) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

#[cfg(test)]
mod tests {
    use crate::ast::Text;

    use super::{QuotedText, QuotedTextFromError};

    #[test]
    fn test_quoted_text() {
        let qt = QuotedText::try_from("{as'df}").unwrap();
        assert_eq!(&format!("{qt}"), "as'df");
        assert_eq!(qt.as_str(), "as'df");
        assert_eq!(&*qt, "as'df");
        assert_eq!(qt.quote_delimiters(), ('{', '}'));
    }

    #[rustfmt::skip]
    #[test]
    fn test_invalid_quoted_text() {
        assert_eq!(Err(QuotedTextFromError::TooShort), QuotedText::try_from(""));
        assert_eq!(Err(QuotedTextFromError::TooShort), QuotedText::try_from("a"));

        assert_eq!(Err(QuotedTextFromError::InvalidDelimiters), QuotedText::try_from(".asdf,"));
        assert_eq!(Err(QuotedTextFromError::InvalidDelimiters), QuotedText::try_from("{adf>"));
        assert_eq!(Err(QuotedTextFromError::InvalidDelimiters), QuotedText::try_from("{adf>"));
        assert_eq!(Err(QuotedTextFromError::InvalidDelimiters), QuotedText::try_from("oasdfx"));

        assert_eq!(Err(QuotedTextFromError::InvalidContent), QuotedText::try_from(",a,'df,"));
        assert_eq!(Err(QuotedTextFromError::InvalidContent), QuotedText::try_from("{a}'df}"));
        assert_eq!(Err(QuotedTextFromError::InvalidContent), QuotedText::try_from("<a>'df>"));
    }

    #[rustfmt::skip]
    #[test]
    fn test_text_regular_display() {
        assert_eq!(
            &format!("{}", Text::Regular("hello world".into())),
            r#"'hello world'"#
        );
        assert_eq!(
            &format!(">>{}<<", Text::Regular("how's life?".into())),
            r#">>'how''s life?'<<"#
        );
        assert_eq!(
            &format!(">>{}<<", Text::Regular(".'_'".into())),
            r#">>'.''_'''<<"#
        );
        assert_eq!(
            &format!(">>{}<<", Text::Regular("".into())),
            r#">>''<<"#
        );
        assert_eq!(
            &format!(">>{}<<", Text::Regular("'".into())),
            r#">>''''<<"#
        );
        assert_eq!(
            &format!(">>{}<<", Text::Regular("''".into())),
            r#">>''''''<<"#
        );
        assert_eq!(
            &format!(">>{}<<", Text::Regular("''".into())),
            r#">>''''''<<"#
        );
    }

    #[test]
    fn test_text_quoted_display() {
        assert_eq!(
            &format!(
                "{}",
                Text::Quoted(QuotedText::try_from("|hello world|").unwrap())
            ),
            r#"Q'|hello world|'"#
        );
    }
}