use regex::Regex;
#[cfg(feature = "v2")]
use std::collections::HashSet;
use crate::validation::{Context, Options, PushError, ValidateWithContext};
pub fn validate_email<T>(email: &Option<String>, ctx: &mut Context<T>, path: String) {
if let Some(email) = email
&& !email.contains('@')
{
ctx.error(
path,
format_args!("must be a valid email address, found `{email}`"),
);
}
}
#[cfg(any(feature = "v2", feature = "v3_0", feature = "v3_1"))]
const HTTP: &str = "http://";
#[cfg(any(feature = "v2", feature = "v3_0", feature = "v3_1"))]
const HTTPS: &str = "https://";
#[cfg(any(feature = "v2", feature = "v3_0", feature = "v3_1"))]
pub fn validate_optional_url<T>(url: &Option<String>, ctx: &mut Context<T>, path: String) {
if let Some(url) = url {
validate_required_url(url, ctx, path);
}
}
pub fn validate_optional_uri<T>(uri: &Option<String>, ctx: &mut Context<T>, path: String) {
let Some(uri) = uri else { return };
if ctx.is_option(Options::IgnoreInvalidUrls) {
return;
}
if uri.is_empty() {
ctx.error(path, "must be a valid URI, found ``");
return;
}
if has_uri_unsafe_bytes(uri) {
ctx.error(path, format_args!("must be a valid URI, found `{uri}`"));
}
}
pub fn has_uri_unsafe_bytes(s: &str) -> bool {
s.bytes()
.any(|b| b.is_ascii_whitespace() || b.is_ascii_control())
}
pub fn validate_required_uri<T>(uri: &String, ctx: &mut Context<T>, path: String) {
validate_required_string(uri, ctx, path.clone());
if uri.is_empty() || ctx.is_option(Options::IgnoreInvalidUrls) {
return;
}
if has_uri_unsafe_bytes(uri) {
ctx.error(path, format_args!("must be a valid URI, found `{uri}`"));
}
}
#[cfg(any(feature = "v2", feature = "v3_0", feature = "v3_1"))]
pub fn validate_required_url<T>(url: &String, ctx: &mut Context<T>, path: String) {
if !ctx.is_option(Options::IgnoreEmptyExternalDocumentationUrl) {
validate_required_string(url, ctx, path.clone());
}
if url.is_empty() || ctx.is_option(Options::IgnoreInvalidUrls) {
return;
}
if !url.starts_with(HTTP) && !url.starts_with(HTTPS) {
ctx.error(path, format_args!("must be a valid URL, found `{url}`"));
}
}
pub fn validate_required_string<T>(s: &str, ctx: &mut Context<T>, path: String) {
if s.is_empty() {
ctx.error(path, "must not be empty");
}
}
pub fn validate_string_matches<T>(s: &str, pattern: &Regex, ctx: &mut Context<T>, path: String) {
if !pattern.is_match(s) {
ctx.error(
path,
format_args!("must match pattern `{pattern}`, found `{s}`"),
);
}
}
#[cfg(feature = "v2")]
pub fn validate_optional_string_matches<T>(
s: &Option<String>,
pattern: &Regex,
ctx: &mut Context<T>,
path: String,
) {
if let Some(s) = s {
validate_string_matches(s, pattern, ctx, path);
}
}
pub fn validate_pattern<T>(pattern: &str, ctx: &mut Context<T>, path: String) {
match Regex::new(pattern) {
Ok(_) => {}
Err(e) => ctx.error(path, format_args!("pattern `{pattern}` is invalid: {e}")),
}
}
#[cfg(feature = "v2")]
pub fn validate_unique_by<T, K, S, F>(items: &[T], ctx: &mut Context<S>, path: String, key: F)
where
K: Eq + std::hash::Hash,
F: Fn(&T) -> K,
{
let mut seen: HashSet<K> = HashSet::new();
for (i, item) in items.iter().enumerate() {
if !seen.insert(key(item)) {
ctx.error(format!("{path}[{i}]"), "duplicate value");
}
}
}
pub fn validate_not_visited<T, D>(
obj: &D,
ctx: &mut Context<T>,
ignore_option: Options,
path: String,
) where
D: ValidateWithContext<T>,
{
if ctx.visit(path.clone()) {
if !ctx.is_option(ignore_option) {
ctx.error(path.clone(), "unused");
}
obj.validate_with_context(ctx, path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validation::ValidationErrorsExt;
#[cfg(any(feature = "v2", feature = "v3_0", feature = "v3_1"))]
#[test]
fn test_validate_url() {
let mut ctx = Context::new(&(), Options::new());
validate_required_url(
&String::from("http://example.com"),
&mut ctx,
String::from("test_url"),
);
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
let mut ctx = Context::new(&(), Options::new());
validate_required_url(
&String::from("https://example.com"),
&mut ctx,
String::from("test_url"),
);
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
let mut ctx = Context::new(&(), Options::new());
validate_required_url(&String::from("foo-bar"), &mut ctx, String::from("test_url"));
assert!(
ctx.errors
.has_exact("test_url: must be a valid URL, found `foo-bar`"),
"expected error: {:?}",
ctx.errors
);
let mut ctx = Context::new(&(), Options::only(&Options::IgnoreInvalidUrls));
validate_required_url(&String::from("foo-bar"), &mut ctx, String::from("test_url"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
let mut ctx = Context::new(&(), Options::new());
validate_optional_url(&None, &mut ctx, String::from("test_url"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
let mut ctx = Context::new(&(), Options::new());
validate_optional_url(
&Some(String::from("http://example.com")),
&mut ctx,
String::from("test_url"),
);
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
let mut ctx = Context::new(&(), Options::new());
validate_optional_url(
&Some(String::from("https://example.com")),
&mut ctx,
String::from("test_url"),
);
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
let mut ctx = Context::new(&(), Options::new());
validate_optional_url(
&Some(String::from("foo-bar")),
&mut ctx,
String::from("test_url"),
);
assert!(
ctx.errors
.has_exact("test_url: must be a valid URL, found `foo-bar`"),
"expected error: {:?}",
ctx.errors
);
let mut ctx = Context::new(&(), Options::only(&Options::IgnoreInvalidUrls));
validate_optional_url(
&Some(String::from("foo-bar")),
&mut ctx,
String::from("test_url"),
);
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
}
#[test]
fn validate_optional_uri_rejects_control_chars_and_whitespace() {
for s in ["bad\turi", "with\nnewline", "with\x01ctl", "with\x7fdel"] {
let mut ctx = Context::new(&(), Options::new());
validate_optional_uri(&Some(s.to_owned()), &mut ctx, "u".to_owned());
assert!(
ctx.errors.mentions("must be a valid URI"),
"expected error for `{s:?}`: {:?}",
ctx.errors
);
}
let mut ctx = Context::new(&(), Options::new());
validate_optional_uri(
&Some("urn:example:dialect".to_owned()),
&mut ctx,
"u".to_owned(),
);
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
}
}