use-jquery 0.0.1

jQuery metadata primitives for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

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

/// jQuery version-family labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum JqueryVersionFamily {
    Jquery1,
    Jquery2,
    Jquery3,
    Jquery4,
}

impl JqueryVersionFamily {
    /// Returns the version-family label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Jquery1 => "jquery1",
            Self::Jquery2 => "jquery2",
            Self::Jquery3 => "jquery3",
            Self::Jquery4 => "jquery4",
        }
    }
}

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

impl FromStr for JqueryVersionFamily {
    type Err = JqueryTextError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "jquery1" | "1" => Ok(Self::Jquery1),
            "jquery2" | "2" => Ok(Self::Jquery2),
            "jquery3" | "3" => Ok(Self::Jquery3),
            "jquery4" | "4" => Ok(Self::Jquery4),
            _ => Err(JqueryTextError::UnknownLabel),
        }
    }
}

/// Validated non-empty jQuery selector text.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JquerySelector(String);

impl JquerySelector {
    /// Creates jQuery selector metadata.
    ///
    /// # Errors
    ///
    /// Returns [`JqueryTextError`] when `input` is empty or contains control characters.
    pub fn new(input: &str) -> Result<Self, JqueryTextError> {
        let trimmed = input.trim();
        if trimmed.is_empty() {
            return Err(JqueryTextError::Empty);
        }
        if let Some(character) = trimmed.chars().find(|character| character.is_control()) {
            return Err(JqueryTextError::InvalidCharacter { character });
        }
        Ok(Self(trimmed.to_string()))
    }

    /// Returns the selector text.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

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

impl FromStr for JquerySelector {
    type Err = JqueryTextError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        Self::new(input)
    }
}

impl TryFrom<&str> for JquerySelector {
    type Error = JqueryTextError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

/// Validated jQuery plugin name metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JqueryPluginName(String);

impl JqueryPluginName {
    /// Creates jQuery plugin name metadata.
    ///
    /// # Errors
    ///
    /// Returns [`JqueryTextError`] when `input` is empty, contains whitespace, or has unsupported characters.
    pub fn new(input: &str) -> Result<Self, JqueryTextError> {
        validate_label(input).map(Self)
    }

    /// Returns the plugin name.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

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

impl FromStr for JqueryPluginName {
    type Err = JqueryTextError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        Self::new(input)
    }
}

impl TryFrom<&str> for JqueryPluginName {
    type Error = JqueryTextError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

/// Validated jQuery event name metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JqueryEventName(String);

impl JqueryEventName {
    /// Creates jQuery event name metadata.
    ///
    /// # Errors
    ///
    /// Returns [`JqueryTextError`] when `input` is empty, contains whitespace, or has unsupported characters.
    pub fn new(input: &str) -> Result<Self, JqueryTextError> {
        validate_label(input).map(Self)
    }

    /// Returns the event name.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

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

impl FromStr for JqueryEventName {
    type Err = JqueryTextError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        Self::new(input)
    }
}

impl TryFrom<&str> for JqueryEventName {
    type Error = JqueryTextError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

/// Validated jQuery effect name metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JqueryEffectName(String);

impl JqueryEffectName {
    /// Creates jQuery effect name metadata.
    ///
    /// # Errors
    ///
    /// Returns [`JqueryTextError`] when `input` is empty, contains whitespace, or has unsupported characters.
    pub fn new(input: &str) -> Result<Self, JqueryTextError> {
        validate_label(input).map(Self)
    }

    /// Returns the effect name.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

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

impl FromStr for JqueryEffectName {
    type Err = JqueryTextError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        Self::new(input)
    }
}

impl TryFrom<&str> for JqueryEffectName {
    type Error = JqueryTextError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

/// Common jQuery AJAX method labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum JqueryAjaxMethod {
    Get,
    Post,
    Put,
    Patch,
    Delete,
}

impl JqueryAjaxMethod {
    /// Returns the AJAX method label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Get => "GET",
            Self::Post => "POST",
            Self::Put => "PUT",
            Self::Patch => "PATCH",
            Self::Delete => "DELETE",
        }
    }
}

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

impl FromStr for JqueryAjaxMethod {
    type Err = JqueryTextError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "get" => Ok(Self::Get),
            "post" => Ok(Self::Post),
            "put" => Ok(Self::Put),
            "patch" => Ok(Self::Patch),
            "delete" | "del" => Ok(Self::Delete),
            _ => Err(JqueryTextError::UnknownLabel),
        }
    }
}

/// Error returned when jQuery metadata text is invalid.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum JqueryTextError {
    Empty,
    ContainsWhitespace,
    InvalidCharacter { character: char },
    UnknownLabel,
}

impl fmt::Display for JqueryTextError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => formatter.write_str("jQuery metadata text cannot be empty"),
            Self::ContainsWhitespace => {
                formatter.write_str("jQuery metadata text cannot contain whitespace")
            }
            Self::InvalidCharacter { character } => {
                write!(formatter, "invalid jQuery metadata character `{character}`")
            }
            Self::UnknownLabel => formatter.write_str("unknown jQuery metadata label"),
        }
    }
}

impl Error for JqueryTextError {}

fn validate_label(input: &str) -> Result<String, JqueryTextError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(JqueryTextError::Empty);
    }
    if trimmed.chars().any(char::is_whitespace) {
        return Err(JqueryTextError::ContainsWhitespace);
    }
    if let Some(character) = trimmed
        .chars()
        .find(|character| !is_label_character(*character))
    {
        return Err(JqueryTextError::InvalidCharacter { character });
    }
    Ok(trimmed.to_string())
}

const fn is_label_character(character: char) -> bool {
    character.is_ascii_alphanumeric() || matches!(character, '.' | ':' | '_' | '-')
}

fn normalized_label(input: &str) -> Result<String, JqueryTextError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(JqueryTextError::Empty);
    }
    Ok(trimmed
        .chars()
        .filter(|character| !matches!(character, '-' | '_' | ' '))
        .flat_map(char::to_lowercase)
        .collect())
}

#[cfg(test)]
mod tests {
    use super::{
        JqueryAjaxMethod, JqueryEffectName, JqueryEventName, JqueryPluginName, JquerySelector,
        JqueryTextError, JqueryVersionFamily,
    };

    #[test]
    fn validates_selectors() -> Result<(), JqueryTextError> {
        let selector = JquerySelector::new(".todo-item")?;
        assert_eq!(selector.as_str(), ".todo-item");
        assert_eq!(JquerySelector::new(""), Err(JqueryTextError::Empty));
        assert_eq!(
            JquerySelector::new(".todo\n.item"),
            Err(JqueryTextError::InvalidCharacter { character: '\n' })
        );
        Ok(())
    }

    #[test]
    fn validates_label_text() -> Result<(), JqueryTextError> {
        assert_eq!(JqueryPluginName::new("datepicker")?.as_str(), "datepicker");
        assert_eq!(JqueryEventName::new("click.app")?.as_str(), "click.app");
        assert_eq!(JqueryEffectName::new("fadeIn")?.as_str(), "fadeIn");
        assert_eq!(
            JqueryEventName::new("click app"),
            Err(JqueryTextError::ContainsWhitespace)
        );
        assert_eq!(
            JqueryPluginName::new("plug/in"),
            Err(JqueryTextError::InvalidCharacter { character: '/' })
        );
        Ok(())
    }

    #[test]
    fn parses_labels() -> Result<(), JqueryTextError> {
        assert_eq!(
            "jquery3".parse::<JqueryVersionFamily>()?,
            JqueryVersionFamily::Jquery3
        );
        assert_eq!("POST".parse::<JqueryAjaxMethod>()?, JqueryAjaxMethod::Post);
        assert_eq!(JqueryAjaxMethod::Delete.to_string(), "DELETE");
        Ok(())
    }
}