Skip to main content

use_jquery/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// jQuery version-family labels.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum JqueryVersionFamily {
10    Jquery1,
11    Jquery2,
12    Jquery3,
13    Jquery4,
14}
15
16impl JqueryVersionFamily {
17    /// Returns the version-family label.
18    #[must_use]
19    pub const fn as_str(self) -> &'static str {
20        match self {
21            Self::Jquery1 => "jquery1",
22            Self::Jquery2 => "jquery2",
23            Self::Jquery3 => "jquery3",
24            Self::Jquery4 => "jquery4",
25        }
26    }
27}
28
29impl fmt::Display for JqueryVersionFamily {
30    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31        formatter.write_str(self.as_str())
32    }
33}
34
35impl FromStr for JqueryVersionFamily {
36    type Err = JqueryTextError;
37
38    fn from_str(input: &str) -> Result<Self, Self::Err> {
39        match normalized_label(input)?.as_str() {
40            "jquery1" | "1" => Ok(Self::Jquery1),
41            "jquery2" | "2" => Ok(Self::Jquery2),
42            "jquery3" | "3" => Ok(Self::Jquery3),
43            "jquery4" | "4" => Ok(Self::Jquery4),
44            _ => Err(JqueryTextError::UnknownLabel),
45        }
46    }
47}
48
49/// Validated non-empty jQuery selector text.
50#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
51pub struct JquerySelector(String);
52
53impl JquerySelector {
54    /// Creates jQuery selector metadata.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`JqueryTextError`] when `input` is empty or contains control characters.
59    pub fn new(input: &str) -> Result<Self, JqueryTextError> {
60        let trimmed = input.trim();
61        if trimmed.is_empty() {
62            return Err(JqueryTextError::Empty);
63        }
64        if let Some(character) = trimmed.chars().find(|character| character.is_control()) {
65            return Err(JqueryTextError::InvalidCharacter { character });
66        }
67        Ok(Self(trimmed.to_string()))
68    }
69
70    /// Returns the selector text.
71    #[must_use]
72    pub fn as_str(&self) -> &str {
73        &self.0
74    }
75}
76
77impl fmt::Display for JquerySelector {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        formatter.write_str(self.as_str())
80    }
81}
82
83impl FromStr for JquerySelector {
84    type Err = JqueryTextError;
85
86    fn from_str(input: &str) -> Result<Self, Self::Err> {
87        Self::new(input)
88    }
89}
90
91impl TryFrom<&str> for JquerySelector {
92    type Error = JqueryTextError;
93
94    fn try_from(value: &str) -> Result<Self, Self::Error> {
95        Self::new(value)
96    }
97}
98
99/// Validated jQuery plugin name metadata.
100#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
101pub struct JqueryPluginName(String);
102
103impl JqueryPluginName {
104    /// Creates jQuery plugin name metadata.
105    ///
106    /// # Errors
107    ///
108    /// Returns [`JqueryTextError`] when `input` is empty, contains whitespace, or has unsupported characters.
109    pub fn new(input: &str) -> Result<Self, JqueryTextError> {
110        validate_label(input).map(Self)
111    }
112
113    /// Returns the plugin name.
114    #[must_use]
115    pub fn as_str(&self) -> &str {
116        &self.0
117    }
118}
119
120impl fmt::Display for JqueryPluginName {
121    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122        formatter.write_str(self.as_str())
123    }
124}
125
126impl FromStr for JqueryPluginName {
127    type Err = JqueryTextError;
128
129    fn from_str(input: &str) -> Result<Self, Self::Err> {
130        Self::new(input)
131    }
132}
133
134impl TryFrom<&str> for JqueryPluginName {
135    type Error = JqueryTextError;
136
137    fn try_from(value: &str) -> Result<Self, Self::Error> {
138        Self::new(value)
139    }
140}
141
142/// Validated jQuery event name metadata.
143#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
144pub struct JqueryEventName(String);
145
146impl JqueryEventName {
147    /// Creates jQuery event name metadata.
148    ///
149    /// # Errors
150    ///
151    /// Returns [`JqueryTextError`] when `input` is empty, contains whitespace, or has unsupported characters.
152    pub fn new(input: &str) -> Result<Self, JqueryTextError> {
153        validate_label(input).map(Self)
154    }
155
156    /// Returns the event name.
157    #[must_use]
158    pub fn as_str(&self) -> &str {
159        &self.0
160    }
161}
162
163impl fmt::Display for JqueryEventName {
164    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
165        formatter.write_str(self.as_str())
166    }
167}
168
169impl FromStr for JqueryEventName {
170    type Err = JqueryTextError;
171
172    fn from_str(input: &str) -> Result<Self, Self::Err> {
173        Self::new(input)
174    }
175}
176
177impl TryFrom<&str> for JqueryEventName {
178    type Error = JqueryTextError;
179
180    fn try_from(value: &str) -> Result<Self, Self::Error> {
181        Self::new(value)
182    }
183}
184
185/// Validated jQuery effect name metadata.
186#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
187pub struct JqueryEffectName(String);
188
189impl JqueryEffectName {
190    /// Creates jQuery effect name metadata.
191    ///
192    /// # Errors
193    ///
194    /// Returns [`JqueryTextError`] when `input` is empty, contains whitespace, or has unsupported characters.
195    pub fn new(input: &str) -> Result<Self, JqueryTextError> {
196        validate_label(input).map(Self)
197    }
198
199    /// Returns the effect name.
200    #[must_use]
201    pub fn as_str(&self) -> &str {
202        &self.0
203    }
204}
205
206impl fmt::Display for JqueryEffectName {
207    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
208        formatter.write_str(self.as_str())
209    }
210}
211
212impl FromStr for JqueryEffectName {
213    type Err = JqueryTextError;
214
215    fn from_str(input: &str) -> Result<Self, Self::Err> {
216        Self::new(input)
217    }
218}
219
220impl TryFrom<&str> for JqueryEffectName {
221    type Error = JqueryTextError;
222
223    fn try_from(value: &str) -> Result<Self, Self::Error> {
224        Self::new(value)
225    }
226}
227
228/// Common jQuery AJAX method labels.
229#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
230pub enum JqueryAjaxMethod {
231    Get,
232    Post,
233    Put,
234    Patch,
235    Delete,
236}
237
238impl JqueryAjaxMethod {
239    /// Returns the AJAX method label.
240    #[must_use]
241    pub const fn as_str(self) -> &'static str {
242        match self {
243            Self::Get => "GET",
244            Self::Post => "POST",
245            Self::Put => "PUT",
246            Self::Patch => "PATCH",
247            Self::Delete => "DELETE",
248        }
249    }
250}
251
252impl fmt::Display for JqueryAjaxMethod {
253    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
254        formatter.write_str(self.as_str())
255    }
256}
257
258impl FromStr for JqueryAjaxMethod {
259    type Err = JqueryTextError;
260
261    fn from_str(input: &str) -> Result<Self, Self::Err> {
262        match normalized_label(input)?.as_str() {
263            "get" => Ok(Self::Get),
264            "post" => Ok(Self::Post),
265            "put" => Ok(Self::Put),
266            "patch" => Ok(Self::Patch),
267            "delete" | "del" => Ok(Self::Delete),
268            _ => Err(JqueryTextError::UnknownLabel),
269        }
270    }
271}
272
273/// Error returned when jQuery metadata text is invalid.
274#[derive(Clone, Copy, Debug, Eq, PartialEq)]
275pub enum JqueryTextError {
276    Empty,
277    ContainsWhitespace,
278    InvalidCharacter { character: char },
279    UnknownLabel,
280}
281
282impl fmt::Display for JqueryTextError {
283    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
284        match self {
285            Self::Empty => formatter.write_str("jQuery metadata text cannot be empty"),
286            Self::ContainsWhitespace => {
287                formatter.write_str("jQuery metadata text cannot contain whitespace")
288            }
289            Self::InvalidCharacter { character } => {
290                write!(formatter, "invalid jQuery metadata character `{character}`")
291            }
292            Self::UnknownLabel => formatter.write_str("unknown jQuery metadata label"),
293        }
294    }
295}
296
297impl Error for JqueryTextError {}
298
299fn validate_label(input: &str) -> Result<String, JqueryTextError> {
300    let trimmed = input.trim();
301    if trimmed.is_empty() {
302        return Err(JqueryTextError::Empty);
303    }
304    if trimmed.chars().any(char::is_whitespace) {
305        return Err(JqueryTextError::ContainsWhitespace);
306    }
307    if let Some(character) = trimmed
308        .chars()
309        .find(|character| !is_label_character(*character))
310    {
311        return Err(JqueryTextError::InvalidCharacter { character });
312    }
313    Ok(trimmed.to_string())
314}
315
316const fn is_label_character(character: char) -> bool {
317    character.is_ascii_alphanumeric() || matches!(character, '.' | ':' | '_' | '-')
318}
319
320fn normalized_label(input: &str) -> Result<String, JqueryTextError> {
321    let trimmed = input.trim();
322    if trimmed.is_empty() {
323        return Err(JqueryTextError::Empty);
324    }
325    Ok(trimmed
326        .chars()
327        .filter(|character| !matches!(character, '-' | '_' | ' '))
328        .flat_map(char::to_lowercase)
329        .collect())
330}
331
332#[cfg(test)]
333mod tests {
334    use super::{
335        JqueryAjaxMethod, JqueryEffectName, JqueryEventName, JqueryPluginName, JquerySelector,
336        JqueryTextError, JqueryVersionFamily,
337    };
338
339    #[test]
340    fn validates_selectors() -> Result<(), JqueryTextError> {
341        let selector = JquerySelector::new(".todo-item")?;
342        assert_eq!(selector.as_str(), ".todo-item");
343        assert_eq!(JquerySelector::new(""), Err(JqueryTextError::Empty));
344        assert_eq!(
345            JquerySelector::new(".todo\n.item"),
346            Err(JqueryTextError::InvalidCharacter { character: '\n' })
347        );
348        Ok(())
349    }
350
351    #[test]
352    fn validates_label_text() -> Result<(), JqueryTextError> {
353        assert_eq!(JqueryPluginName::new("datepicker")?.as_str(), "datepicker");
354        assert_eq!(JqueryEventName::new("click.app")?.as_str(), "click.app");
355        assert_eq!(JqueryEffectName::new("fadeIn")?.as_str(), "fadeIn");
356        assert_eq!(
357            JqueryEventName::new("click app"),
358            Err(JqueryTextError::ContainsWhitespace)
359        );
360        assert_eq!(
361            JqueryPluginName::new("plug/in"),
362            Err(JqueryTextError::InvalidCharacter { character: '/' })
363        );
364        Ok(())
365    }
366
367    #[test]
368    fn parses_labels() -> Result<(), JqueryTextError> {
369        assert_eq!(
370            "jquery3".parse::<JqueryVersionFamily>()?,
371            JqueryVersionFamily::Jquery3
372        );
373        assert_eq!("POST".parse::<JqueryAjaxMethod>()?, JqueryAjaxMethod::Post);
374        assert_eq!(JqueryAjaxMethod::Delete.to_string(), "DELETE");
375        Ok(())
376    }
377}