1use serde_json::Value;
2
3use crate::error::{ValidationIssue, VldError};
4use crate::schema::VldSchema;
5
6mod checks;
7mod formats;
8
9#[cfg(feature = "openapi")]
10use checks::regex_escape;
11use checks::{Kind, StringCheck};
12
13#[derive(Clone)]
14enum StringTransform {
15 Trim,
16 ToLowerCase,
17 ToUpperCase,
18}
19
20#[derive(Clone)]
29pub struct ZString {
30 checks: Vec<StringCheck>,
31 transforms: Vec<StringTransform>,
32 coerce: bool,
33 custom_type_error: Option<String>,
34 messages: crate::primitives::Messages,
35}
36
37impl ZString {
38 pub fn new() -> Self {
39 Self {
40 checks: vec![],
41 transforms: vec![],
42 coerce: false,
43 custom_type_error: None,
44 messages: None,
45 }
46 }
47
48 fn push(mut self, kind: Kind, msg: Option<String>) -> Self {
49 self.checks.push(StringCheck { kind, msg });
50 self
51 }
52
53 pub fn type_error(mut self, msg: impl Into<String>) -> Self {
63 self.custom_type_error = Some(msg.into());
64 self
65 }
66
67 pub fn with_messages<F>(mut self, f: F) -> Self
93 where
94 F: Fn(&str) -> Option<String> + Send + Sync + 'static,
95 {
96 self.messages = Some(std::sync::Arc::new(f));
97 self
98 }
99
100 pub fn min(self, len: usize) -> Self {
102 self.push(Kind::Min(len), None)
103 }
104
105 pub fn min_msg(self, len: usize, msg: impl Into<String>) -> Self {
107 self.push(Kind::Min(len), Some(msg.into()))
108 }
109
110 pub fn max(self, len: usize) -> Self {
112 self.push(Kind::Max(len), None)
113 }
114
115 pub fn max_msg(self, len: usize, msg: impl Into<String>) -> Self {
117 self.push(Kind::Max(len), Some(msg.into()))
118 }
119
120 pub fn len(self, len: usize) -> Self {
122 self.push(Kind::Len(len), None)
123 }
124
125 pub fn len_msg(self, len: usize, msg: impl Into<String>) -> Self {
127 self.push(Kind::Len(len), Some(msg.into()))
128 }
129
130 #[cfg(feature = "regex")]
134 #[cfg_attr(docsrs, doc(cfg(feature = "regex")))]
135 pub fn regex(self, re: regex_lite::Regex) -> Self {
136 self.push(Kind::Regex(re), None)
137 }
138
139 #[cfg(feature = "regex")]
143 #[cfg_attr(docsrs, doc(cfg(feature = "regex")))]
144 pub fn regex_msg(self, re: regex_lite::Regex, msg: impl Into<String>) -> Self {
145 self.push(Kind::Regex(re), Some(msg.into()))
146 }
147
148 pub fn starts_with(self, prefix: impl Into<String>) -> Self {
150 self.push(Kind::StartsWith(prefix.into()), None)
151 }
152
153 pub fn starts_with_msg(self, prefix: impl Into<String>, msg: impl Into<String>) -> Self {
155 self.push(Kind::StartsWith(prefix.into()), Some(msg.into()))
156 }
157
158 pub fn ends_with(self, suffix: impl Into<String>) -> Self {
160 self.push(Kind::EndsWith(suffix.into()), None)
161 }
162
163 pub fn ends_with_msg(self, suffix: impl Into<String>, msg: impl Into<String>) -> Self {
165 self.push(Kind::EndsWith(suffix.into()), Some(msg.into()))
166 }
167
168 pub fn contains(self, sub: impl Into<String>) -> Self {
170 self.push(Kind::Contains(sub.into()), None)
171 }
172
173 pub fn contains_msg(self, sub: impl Into<String>, msg: impl Into<String>) -> Self {
175 self.push(Kind::Contains(sub.into()), Some(msg.into()))
176 }
177
178 pub fn non_empty(self) -> Self {
180 self.push(Kind::NonEmpty, None)
181 }
182
183 pub fn non_empty_msg(self, msg: impl Into<String>) -> Self {
185 self.push(Kind::NonEmpty, Some(msg.into()))
186 }
187
188 pub fn trim(mut self) -> Self {
190 self.transforms.push(StringTransform::Trim);
191 self
192 }
193
194 pub fn to_lowercase(mut self) -> Self {
196 self.transforms.push(StringTransform::ToLowerCase);
197 self
198 }
199
200 pub fn to_uppercase(mut self) -> Self {
202 self.transforms.push(StringTransform::ToUpperCase);
203 self
204 }
205
206 pub fn coerce(mut self) -> Self {
208 self.coerce = true;
209 self
210 }
211}
212
213impl Default for ZString {
214 fn default() -> Self {
215 Self::new()
216 }
217}
218
219impl VldSchema for ZString {
220 type Output = String;
221
222 fn parse_value(&self, value: &Value) -> Result<String, VldError> {
223 let type_err = || crate::primitives::type_error(&self.custom_type_error, "string", value);
224
225 let mut s = match value {
226 Value::String(s) => s.clone(),
227 Value::Number(n) if self.coerce => n.to_string(),
228 Value::Bool(b) if self.coerce => b.to_string(),
229 _ => return Err(type_err()),
230 };
231
232 for t in &self.transforms {
233 s = match t {
234 StringTransform::Trim => s.trim().to_string(),
235 StringTransform::ToLowerCase => s.to_lowercase(),
236 StringTransform::ToUpperCase => s.to_uppercase(),
237 };
238 }
239
240 let mut received: Option<Value> = None;
243 let mut errors = VldError::new();
244 for check in &self.checks {
245 if let Some(code) = check.kind.failure(&s) {
246 let received = received.get_or_insert_with(|| Value::String(s.clone()));
247 let bulk = || self.messages.as_ref().and_then(|f| f(check.kind.key()));
248 let msg = check.msg.clone().or_else(bulk);
249 errors.issues.push(
250 ValidationIssue::with_override(code, msg.as_ref()).with_received(received),
251 );
252 }
253 }
254
255 if errors.is_empty() {
256 Ok(s)
257 } else {
258 Err(errors)
259 }
260 }
261}
262
263#[cfg(feature = "openapi")]
270impl crate::openapi::JsonSchema for ZString {
271 fn json_schema(&self) -> serde_json::Value {
272 let mut schema = serde_json::json!({"type": "string"});
273 let (mut mins, mut maxs) = (vec![], vec![]);
274 let mut formats: Vec<&str> = vec![];
275 let mut patterns: Vec<String> = vec![];
276 for check in &self.checks {
277 match &check.kind {
278 Kind::Min(n) => mins.push(*n),
279 Kind::Max(n) => maxs.push(*n),
280 Kind::Len(n) => {
281 mins.push(*n);
282 maxs.push(*n);
283 }
284 Kind::NonEmpty => mins.push(1),
285 Kind::StartsWith(p) => patterns.push(format!("^{}", regex_escape(p))),
286 Kind::EndsWith(p) => patterns.push(format!("{}$", regex_escape(p))),
287 Kind::Contains(p) => patterns.push(regex_escape(p)),
288 #[cfg(feature = "regex")]
289 Kind::Regex(re) => patterns.push(re.as_str().to_string()),
290 Kind::Format(f) => {
291 if !formats.contains(&f.schema_format()) {
292 formats.push(f.schema_format());
293 }
294 }
295 }
296 }
297 crate::primitives::length_schema(&mut schema, ("minLength", "maxLength"), mins, maxs);
298 let mut all_of = vec![];
299 let mut formats = formats.into_iter();
300 if let Some(f) = formats.next() {
301 schema["format"] = serde_json::json!(f);
302 }
303 all_of.extend(formats.map(|f| serde_json::json!({ "format": f })));
304 let mut patterns = patterns.into_iter();
305 if let Some(p) = patterns.next() {
306 schema["pattern"] = serde_json::json!(p);
307 }
308 all_of.extend(patterns.map(|p| serde_json::json!({ "pattern": p })));
309 if !all_of.is_empty() {
310 schema["allOf"] = serde_json::Value::Array(all_of);
311 }
312 schema
313 }
314}
315
316#[cfg(feature = "openapi")]
317impl crate::openapi::CollectNestedSchemas for ZString {}