#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum JqueryVersionFamily {
Jquery1,
Jquery2,
Jquery3,
Jquery4,
}
impl JqueryVersionFamily {
#[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),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JquerySelector(String);
impl JquerySelector {
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()))
}
#[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)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JqueryPluginName(String);
impl JqueryPluginName {
pub fn new(input: &str) -> Result<Self, JqueryTextError> {
validate_label(input).map(Self)
}
#[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)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JqueryEventName(String);
impl JqueryEventName {
pub fn new(input: &str) -> Result<Self, JqueryTextError> {
validate_label(input).map(Self)
}
#[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)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JqueryEffectName(String);
impl JqueryEffectName {
pub fn new(input: &str) -> Result<Self, JqueryTextError> {
validate_label(input).map(Self)
}
#[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)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum JqueryAjaxMethod {
Get,
Post,
Put,
Patch,
Delete,
}
impl JqueryAjaxMethod {
#[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),
}
}
}
#[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(())
}
}