wary/options/
lowercase.rs

1//! Rule and transformer for ensuring that a string is entirely lowercase.
2//!
3//! See [`Lowercase`] for more information.
4
5use crate::toolbox::rule::*;
6
7#[doc(hidden)]
8pub type Rule<Mode> = Lowercase<Mode>;
9#[doc(hidden)]
10pub type Transformer<Mode> = Lowercase<Mode>;
11
12#[derive(Debug, thiserror::Error, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize))]
14pub enum Error {
15	#[error("expected lowercase character at position {position}")]
16	Lowercase { position: usize },
17}
18
19impl Error {
20	#[must_use]
21	pub(crate) fn code(&self) -> &'static str {
22		match self {
23			Self::Lowercase { .. } => "lowercase",
24		}
25	}
26
27	#[cfg(feature = "alloc")]
28	#[must_use]
29	pub(crate) fn message(&self) -> Cow<'static, str> {
30		match self {
31			Self::Lowercase { position } => {
32				format!("expected lowercase character at position {position}")
33			}
34		}
35		.into()
36	}
37
38	#[cfg(not(feature = "alloc"))]
39	pub(crate) fn message(&self) -> &'static str {
40		match self {
41			Self::Lowercase { .. } => "expected lowercase character",
42		}
43	}
44}
45
46pub struct Ascii;
47
48/// Rule and transformer for ensuring that a string is entirely lowercase.
49///
50/// # Example
51///
52/// ```
53/// use wary::Wary;
54///
55/// #[derive(Wary)]
56/// struct Person {
57///   #[validate(lowercase)]
58///   name: String,
59///   #[validate(lowercase(ascii))]
60///   greeting: String,
61///   #[transform(lowercase)]
62///   message: String,
63/// }
64///
65/// let mut person = Person {
66///   name: "hello".into(),
67///   greeting: "hello".into(),
68///   message: "HELLO".into(),
69/// };
70///
71/// assert!(person.wary(&()).is_ok());
72/// assert_eq!(person.message, "hello");
73/// ```
74#[must_use]
75pub struct Lowercase<Mode> {
76	mode: PhantomData<Mode>,
77}
78
79impl Lowercase<Unset> {
80	#[inline]
81	pub const fn new() -> Self {
82		Self { mode: PhantomData }
83	}
84
85	/// # Rule
86	///
87	/// Ensures that the input is entirely lowercase in ascii.
88	///
89	/// # Transformer
90	///
91	/// Uses [`str::make_ascii_lowercase`] to convert in-place instead
92	/// of requiring a new allocation with [`str::to_lowercase`].
93	#[inline]
94	pub const fn ascii(self) -> Lowercase<Ascii> {
95		Lowercase { mode: PhantomData }
96	}
97}
98
99impl<I: ?Sized> crate::Rule<I> for Lowercase<Unset>
100where
101	I: AsRef<str>,
102{
103	type Context = ();
104
105	#[inline]
106	fn validate(&self, _ctx: &Self::Context, item: &I) -> Result<()> {
107		let string = item.as_ref();
108
109		for (idx, ch) in string.chars().enumerate() {
110			if !ch.is_lowercase() && !ch.is_whitespace() {
111				return Err(Error::Lowercase { position: idx }.into());
112			}
113		}
114
115		Ok(())
116	}
117}
118
119impl<I: ?Sized> crate::Rule<I> for Lowercase<Ascii>
120where
121	I: AsRef<str>,
122{
123	type Context = ();
124
125	#[inline]
126	fn validate(&self, _ctx: &Self::Context, item: &I) -> Result<()> {
127		let string = item.as_ref();
128
129		for (idx, ch) in string.chars().enumerate() {
130			if !ch.is_ascii_lowercase() && !ch.is_ascii_whitespace() {
131				return Err(Error::Lowercase { position: idx }.into());
132			}
133		}
134
135		Ok(())
136	}
137}
138
139#[cfg(feature = "alloc")]
140impl crate::Transformer<String> for Lowercase<Unset> {
141	type Context = ();
142
143	#[inline]
144	fn transform(&self, _ctx: &Self::Context, item: &mut String) {
145		*item = item.to_lowercase();
146	}
147}
148
149impl<I> crate::Transformer<I> for Lowercase<Ascii>
150where
151	I: AsMut<str>,
152{
153	type Context = ();
154
155	#[inline]
156	fn transform(&self, _ctx: &Self::Context, item: &mut I) {
157		item.as_mut().make_ascii_lowercase();
158	}
159}
160
161#[cfg(test)]
162mod test {
163	use super::Lowercase;
164	use crate::toolbox::test::*;
165
166	#[test]
167	fn test_lowercase_rule() {
168		let rule = Lowercase::new();
169		let input = "ὈΔΥΣΣΕΎΣ hello".to_string();
170
171		assert!(rule.validate(&(), &input).is_err());
172
173		let rule = Lowercase::new().ascii();
174		let input = "ὈΔΥΣΣΕΎΣ".to_string();
175
176		assert!(rule.validate(&(), &input).is_err());
177
178		let rule = Lowercase::new().ascii();
179		let input = "hello world".to_string();
180
181		assert!(rule.validate(&(), &input).is_ok());
182	}
183
184	#[test]
185	fn test_lowercase_transformer() {
186		let rule = Lowercase::new();
187		let mut input = "ὈΔΥΣΣΕΎΣ HELLO".to_string();
188
189		rule.transform(&(), &mut input);
190		assert_eq!(input, "ὀδυσσεύς hello");
191
192		let rule = Lowercase::new().ascii();
193		let mut input = "ßeLLO".to_string();
194
195		rule.transform(&(), &mut input);
196		assert_eq!(input, "ßello");
197	}
198}