use-js-keyword 0.0.1

JavaScript keyword and reserved-word primitives for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

use core::{fmt, str::FromStr};
use std::error::Error;

/// Common JavaScript keywords.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum JsKeyword {
    Await,
    Break,
    Case,
    Catch,
    Class,
    Const,
    Continue,
    Debugger,
    Default,
    Delete,
    Do,
    Else,
    Export,
    Extends,
    Finally,
    For,
    Function,
    If,
    Import,
    In,
    Instanceof,
    Let,
    New,
    Return,
    Super,
    Switch,
    This,
    Throw,
    Try,
    Typeof,
    Var,
    Void,
    While,
    With,
    Yield,
}

impl JsKeyword {
    /// Returns the lowercase keyword label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Await => "await",
            Self::Break => "break",
            Self::Case => "case",
            Self::Catch => "catch",
            Self::Class => "class",
            Self::Const => "const",
            Self::Continue => "continue",
            Self::Debugger => "debugger",
            Self::Default => "default",
            Self::Delete => "delete",
            Self::Do => "do",
            Self::Else => "else",
            Self::Export => "export",
            Self::Extends => "extends",
            Self::Finally => "finally",
            Self::For => "for",
            Self::Function => "function",
            Self::If => "if",
            Self::Import => "import",
            Self::In => "in",
            Self::Instanceof => "instanceof",
            Self::Let => "let",
            Self::New => "new",
            Self::Return => "return",
            Self::Super => "super",
            Self::Switch => "switch",
            Self::This => "this",
            Self::Throw => "throw",
            Self::Try => "try",
            Self::Typeof => "typeof",
            Self::Var => "var",
            Self::Void => "void",
            Self::While => "while",
            Self::With => "with",
            Self::Yield => "yield",
        }
    }
}

impl fmt::Display for JsKeyword {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for JsKeyword {
    type Err = JsKeywordParseError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_word(input)?.as_str() {
            "await" => Ok(Self::Await),
            "break" => Ok(Self::Break),
            "case" => Ok(Self::Case),
            "catch" => Ok(Self::Catch),
            "class" => Ok(Self::Class),
            "const" => Ok(Self::Const),
            "continue" => Ok(Self::Continue),
            "debugger" => Ok(Self::Debugger),
            "default" => Ok(Self::Default),
            "delete" => Ok(Self::Delete),
            "do" => Ok(Self::Do),
            "else" => Ok(Self::Else),
            "export" => Ok(Self::Export),
            "extends" => Ok(Self::Extends),
            "finally" => Ok(Self::Finally),
            "for" => Ok(Self::For),
            "function" => Ok(Self::Function),
            "if" => Ok(Self::If),
            "import" => Ok(Self::Import),
            "in" => Ok(Self::In),
            "instanceof" => Ok(Self::Instanceof),
            "let" => Ok(Self::Let),
            "new" => Ok(Self::New),
            "return" => Ok(Self::Return),
            "super" => Ok(Self::Super),
            "switch" => Ok(Self::Switch),
            "this" => Ok(Self::This),
            "throw" => Ok(Self::Throw),
            "try" => Ok(Self::Try),
            "typeof" => Ok(Self::Typeof),
            "var" => Ok(Self::Var),
            "void" => Ok(Self::Void),
            "while" => Ok(Self::While),
            "with" => Ok(Self::With),
            "yield" => Ok(Self::Yield),
            _ => Err(JsKeywordParseError::Unknown),
        }
    }
}

/// JavaScript reserved words, including keywords and common future reserved words.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum JsReservedWord {
    Keyword(JsKeyword),
    Enum,
    Implements,
    Interface,
    Package,
    Private,
    Protected,
    Public,
    Static,
}

impl JsReservedWord {
    /// Returns the lowercase reserved-word label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Keyword(keyword) => keyword.as_str(),
            Self::Enum => "enum",
            Self::Implements => "implements",
            Self::Interface => "interface",
            Self::Package => "package",
            Self::Private => "private",
            Self::Protected => "protected",
            Self::Public => "public",
            Self::Static => "static",
        }
    }
}

impl fmt::Display for JsReservedWord {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for JsReservedWord {
    type Err = JsKeywordParseError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        if let Ok(keyword) = input.parse::<JsKeyword>() {
            return Ok(Self::Keyword(keyword));
        }

        match normalized_word(input)?.as_str() {
            "enum" => Ok(Self::Enum),
            "implements" => Ok(Self::Implements),
            "interface" => Ok(Self::Interface),
            "package" => Ok(Self::Package),
            "private" => Ok(Self::Private),
            "protected" => Ok(Self::Protected),
            "public" => Ok(Self::Public),
            "static" => Ok(Self::Static),
            _ => Err(JsKeywordParseError::Unknown),
        }
    }
}

/// Error returned when parsing JavaScript vocabulary fails.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum JsKeywordParseError {
    Empty,
    Unknown,
}

impl fmt::Display for JsKeywordParseError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => formatter.write_str("JavaScript word cannot be empty"),
            Self::Unknown => formatter.write_str("unknown JavaScript keyword or reserved word"),
        }
    }
}

impl Error for JsKeywordParseError {}

/// Returns whether `input` is one of the common JavaScript keywords.
#[must_use]
pub fn is_js_keyword(input: &str) -> bool {
    input.parse::<JsKeyword>().is_ok()
}

/// Returns whether `input` is a keyword or common reserved word.
#[must_use]
pub fn is_js_reserved_word(input: &str) -> bool {
    input.parse::<JsReservedWord>().is_ok()
}

fn normalized_word(input: &str) -> Result<String, JsKeywordParseError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        Err(JsKeywordParseError::Empty)
    } else {
        Ok(trimmed.to_ascii_lowercase())
    }
}

#[cfg(test)]
mod tests {
    use super::{
        JsKeyword, JsKeywordParseError, JsReservedWord, is_js_keyword, is_js_reserved_word,
    };

    #[test]
    fn parses_keywords() -> Result<(), JsKeywordParseError> {
        let keyword: JsKeyword = "Class".parse()?;
        assert_eq!(keyword, JsKeyword::Class);
        assert_eq!(keyword.as_str(), "class");
        assert!(is_js_keyword("await"));
        Ok(())
    }

    #[test]
    fn checks_reserved_words() -> Result<(), JsKeywordParseError> {
        let reserved: JsReservedWord = "interface".parse()?;
        assert_eq!(reserved.as_str(), "interface");
        assert!(is_js_reserved_word("private"));
        assert!(!is_js_reserved_word("component"));
        Ok(())
    }
}