1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
51pub struct JquerySelector(String);
52
53impl JquerySelector {
54 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
101pub struct JqueryPluginName(String);
102
103impl JqueryPluginName {
104 pub fn new(input: &str) -> Result<Self, JqueryTextError> {
110 validate_label(input).map(Self)
111 }
112
113 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
144pub struct JqueryEventName(String);
145
146impl JqueryEventName {
147 pub fn new(input: &str) -> Result<Self, JqueryTextError> {
153 validate_label(input).map(Self)
154 }
155
156 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
187pub struct JqueryEffectName(String);
188
189impl JqueryEffectName {
190 pub fn new(input: &str) -> Result<Self, JqueryTextError> {
196 validate_label(input).map(Self)
197 }
198
199 #[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#[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 #[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#[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}