1use enumset::EnumSet;
2use regex::Regex;
3use std::collections::HashSet;
4use std::fmt;
5
6use crate::validation::{Error, Options};
7
8pub trait ValidateWithContext<T> {
12 fn validate_with_context(&self, ctx: &mut Context<T>, path: String);
13}
14
15#[derive(Debug, Clone, PartialEq)]
16pub struct Context<'a, T> {
17 pub spec: &'a T,
18 pub visited: HashSet<String>,
19 pub errors: Vec<String>,
20 pub options: EnumSet<Options>,
21}
22
23pub trait PushError<T> {
24 fn error(&mut self, path: String, args: T);
25}
26
27impl<T> PushError<&str> for Context<'_, T> {
28 fn error(&mut self, path: String, msg: &str) {
29 if msg.starts_with('.') {
30 self.errors.push(format!("{path}{msg}"));
31 } else {
32 self.errors.push(format!("{path}: {msg}"));
33 }
34 }
35}
36
37impl<T> PushError<String> for Context<'_, T> {
38 fn error(&mut self, path: String, msg: String) {
39 self.error(path, msg.as_str());
40 }
41}
42
43impl<T> PushError<fmt::Arguments<'_>> for Context<'_, T> {
44 fn error(&mut self, path: String, args: fmt::Arguments<'_>) {
45 self.error(path, args.to_string().as_str());
46 }
47}
48
49impl<T> Context<'_, T> {
50 pub fn reset(&mut self) {
51 self.visited.clear();
52 self.errors.clear();
53 }
54
55 pub fn visit(&mut self, path: String) -> bool {
56 self.visited.insert(path)
57 }
58
59 pub fn is_visited(&self, path: &str) -> bool {
60 self.visited.contains(path)
61 }
62
63 pub fn is_option(&self, option: Options) -> bool {
64 self.options.contains(option)
65 }
66}
67
68impl Context<'_, ()> {
69 pub fn new<T>(spec: &T, options: EnumSet<Options>) -> Context<'_, T> {
70 Context {
71 spec,
72 visited: HashSet::new(),
73 errors: Vec::new(),
74 options,
75 }
76 }
77}
78
79impl<'a, T> From<Context<'a, T>> for Result<(), Error> {
80 fn from(val: Context<'a, T>) -> Self {
81 if val.errors.is_empty() {
82 Ok(())
83 } else {
84 Err(Error { errors: val.errors })
85 }
86 }
87}
88
89pub fn validate_email<T>(email: &Option<String>, ctx: &mut Context<T>, path: String) {
92 if let Some(email) = email
93 && !email.contains('@')
94 {
95 ctx.error(
96 path,
97 format_args!("must be a valid email address, found `{email}`"),
98 );
99 }
100}
101
102const HTTP: &str = "http://";
103const HTTPS: &str = "https://";
104
105pub fn validate_optional_url<T>(url: &Option<String>, ctx: &mut Context<T>, path: String) {
109 if let Some(url) = url {
110 validate_required_url(url, ctx, path);
111 }
112}
113
114pub fn validate_required_url<T>(url: &String, ctx: &mut Context<T>, path: String) {
117 if !ctx.is_option(Options::IgnoreEmptyExternalDocumentationUrl) {
118 validate_required_string(url, ctx, path.clone());
119 }
120 if url.is_empty() || ctx.is_option(Options::IgnoreInvalidUrls) {
122 return;
123 }
124
125 if !url.starts_with(HTTP) && !url.starts_with(HTTPS) {
127 ctx.error(path, format_args!("must be a valid URL, found `{url}`"));
128 }
129}
130
131pub fn validate_required_string<T>(s: &str, ctx: &mut Context<T>, path: String) {
134 if s.is_empty() {
135 ctx.error(path, "must not be empty");
136 }
137}
138
139pub fn validate_string_matches<T>(s: &str, pattern: &Regex, ctx: &mut Context<T>, path: String) {
142 if !pattern.is_match(s) {
143 ctx.error(
144 path,
145 format_args!("must match pattern `{pattern}`, found `{s}`"),
146 );
147 }
148}
149
150pub fn validate_optional_string_matches<T>(
152 s: &Option<String>,
153 pattern: &Regex,
154 ctx: &mut Context<T>,
155 path: String,
156) {
157 if let Some(s) = s {
158 validate_string_matches(s, pattern, ctx, path);
159 }
160}
161
162pub fn validate_pattern<T>(pattern: &str, ctx: &mut Context<T>, path: String) {
165 match Regex::new(pattern) {
166 Ok(_) => {}
167 Err(e) => ctx.error(path, format_args!("pattern `{pattern}` is invalid: {e}")),
168 }
169}
170
171pub fn validate_not_visited<T, D>(
176 obj: &D,
177 ctx: &mut Context<T>,
178 ignore_option: Options,
179 path: String,
180) where
181 D: ValidateWithContext<T>,
182{
183 if ctx.visit(path.clone()) {
184 if !ctx.is_option(ignore_option) {
185 ctx.error(path.clone(), "unused");
186 }
187 obj.validate_with_context(ctx, path);
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_validate_url() {
197 let mut ctx = Context::new(&(), Options::new());
198 validate_required_url(
199 &String::from("http://example.com"),
200 &mut ctx,
201 String::from("test_url"),
202 );
203 assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
204
205 let mut ctx = Context::new(&(), Options::new());
206 validate_required_url(
207 &String::from("https://example.com"),
208 &mut ctx,
209 String::from("test_url"),
210 );
211 assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
212
213 let mut ctx = Context::new(&(), Options::new());
214 validate_required_url(&String::from("foo-bar"), &mut ctx, String::from("test_url"));
215 assert!(
216 ctx.errors
217 .contains(&"test_url: must be a valid URL, found `foo-bar`".to_string()),
218 "expected error: {:?}",
219 ctx.errors
220 );
221
222 let mut ctx = Context::new(&(), Options::only(&Options::IgnoreInvalidUrls));
223 validate_required_url(&String::from("foo-bar"), &mut ctx, String::from("test_url"));
224 assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
225
226 let mut ctx = Context::new(&(), Options::new());
227 validate_optional_url(&None, &mut ctx, String::from("test_url"));
228 assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
229
230 let mut ctx = Context::new(&(), Options::new());
231 validate_optional_url(
232 &Some(String::from("http://example.com")),
233 &mut ctx,
234 String::from("test_url"),
235 );
236 assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
237
238 let mut ctx = Context::new(&(), Options::new());
239 validate_optional_url(
240 &Some(String::from("https://example.com")),
241 &mut ctx,
242 String::from("test_url"),
243 );
244 assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
245
246 let mut ctx = Context::new(&(), Options::new());
247 validate_optional_url(
248 &Some(String::from("foo-bar")),
249 &mut ctx,
250 String::from("test_url"),
251 );
252 assert!(
253 ctx.errors
254 .contains(&"test_url: must be a valid URL, found `foo-bar`".to_string()),
255 "expected error: {:?}",
256 ctx.errors
257 );
258
259 let mut ctx = Context::new(&(), Options::only(&Options::IgnoreInvalidUrls));
260 validate_optional_url(
261 &Some(String::from("foo-bar")),
262 &mut ctx,
263 String::from("test_url"),
264 );
265 assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
266 }
267}