use crate::{ObjectType, emit_error};
use iri_string::{convert::MappedToUri, format::ToDedicatedString, types::IriStr};
use std::{any::type_name, borrow::Cow};
use thiserror::Error;
use tracing::error;
use url::Url;
pub trait Validate: ToString {
fn validate(&self) -> Vec<ValidationError>;
fn is_valid(&self) -> bool {
let result = self.validate();
if result.is_empty() {
true
} else {
error!("[VALIDATION] {:?}", result);
false
}
}
fn check_validity(&self) -> Result<(), ValidationError> {
if self.is_valid() {
Ok(())
} else {
Err(ValidationError::ConstraintViolation(
format!("Instance of '{}' is invalid", type_name::<Self>()).into(),
))
}
}
}
#[derive(Debug, Error)]
pub enum ValidationError {
#[doc(hidden)]
#[error("Empty string: '{0}'")]
Empty(Cow<'static, str>),
#[doc(hidden)]
#[error("Invalid IRI: '{0}'")]
InvalidIRI(Cow<'static, str>),
#[doc(hidden)]
#[error("Invalid URI: '{0}'")]
InvalidURI(Cow<'static, str>),
#[doc(hidden)]
#[error("Invalid IRL: <{0}>")]
InvalidIRL(Cow<'static, str>),
#[doc(hidden)]
#[error("Invalid URL: <{0}>")]
InvalidURL(url::ParseError),
#[doc(hidden)]
#[error("Not a Normalized IRI: \"{0}\"")]
NotNormalizedIRI(Cow<'static, str>),
#[doc(hidden)]
#[error("Not UTC timezone: \"{0}\"")]
NotUTC(Cow<'static, str>),
#[doc(hidden)]
#[error("Wrong 'objectType'. Expected {expected} but found {found}")]
WrongObjectType {
expected: ObjectType,
found: Cow<'static, str>,
},
#[doc(hidden)]
#[error("SHA-1 sum string contains non hex characters or has wrong characters count")]
InvalidSha1String,
#[doc(hidden)]
#[error("SHA-2 hash string contains non hex characters or has wrong characters count")]
InvalidSha2String,
#[doc(hidden)]
#[error("Empty anonymous group")]
EmptyAnonymousGroup,
#[doc(hidden)]
#[error("Invalid timestamp: {0}")]
InvalidDateTime(
#[doc(hidden)]
#[from]
chrono::format::ParseError,
),
#[doc(hidden)]
#[error("Invalid ISO-8601 duration: {0}")]
DurationParseError(speedate::ParseError),
#[doc(hidden)]
#[error("Invalid Language Tag: {0}")]
InvalidLanguageTag(Cow<'static, str>),
#[doc(hidden)]
#[error("{0} must have at least one IFI")]
MissingIFI(Cow<'static, str>),
#[doc(hidden)]
#[error("Missing '{0}'")]
MissingField(Cow<'static, str>),
#[doc(hidden)]
#[error("Invalid '{0}'")]
InvalidField(Cow<'static, str>),
#[doc(hidden)]
#[error("General constraint violation: {0}")]
ConstraintViolation(Cow<'static, str>),
}
pub(crate) fn validate_irl(val: &IriStr) -> Result<(), ValidationError> {
if val.is_empty() {
emit_error!(ValidationError::InvalidIRL(val.to_string().into()))
}
let uri = MappedToUri::from(val).to_dedicated_string();
let normalized_uri = uri.normalize().to_dedicated_string();
let s = normalized_uri.as_str();
match Url::parse(s) {
Ok(_) => Ok(()),
Err(x) => emit_error!(ValidationError::InvalidURL(x)),
}
}
pub(crate) fn validate_sha1sum(val: &str) -> Result<(), ValidationError> {
if val.chars().count() != 40 || !val.chars().all(|x| x.is_ascii_hexdigit()) {
emit_error!(ValidationError::InvalidSha1String)
} else {
Ok(())
}
}
pub(crate) fn validate_sha2(val: &str) -> Result<(), ValidationError> {
if !(32..65).contains(&val.chars().count()) || !val.chars().all(|x| x.is_ascii_hexdigit()) {
emit_error!(ValidationError::InvalidSha2String)
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tracing::info;
use tracing_test::traced_test;
use url::Url;
#[test]
fn test_validate_irl() {
const PASS: &str = "http://résumé.example.org/foo/../";
let r1 = IriStr::new(PASS);
assert!(r1.is_ok());
assert!(validate_irl(r1.unwrap()).is_ok());
const FAIL: &str = "résumé/bar";
let r2 = IriStr::new(FAIL);
assert!(r2.is_err());
}
#[test]
fn test_validate_sha1sum() {
assert!(validate_sha1sum("ebd31e95054c018b10727ccffd2ef2ec3a016ee9").is_ok());
const H1: &str = "ebd31e95054c018b10727ccffd2ef2ec3a016ee9ab";
let r1 = validate_sha1sum(H1);
assert!(r1.is_err_and(|x| matches!(x, ValidationError::InvalidSha1String)));
const H2: &str = "ebd31x95054c018b10727ccffd2ef2ec3a016ee9";
let r2 = validate_sha1sum(H2);
assert!(r2.is_err_and(|x| matches!(x, ValidationError::InvalidSha1String)));
}
#[test]
fn test_validate_sha2() {
assert!(
validate_sha2("495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a")
.is_ok()
);
const H1: &str = "1234567890123456789012345678901";
let r1 = validate_sha2(H1);
assert!(r1.is_err_and(|x| matches!(x, ValidationError::InvalidSha2String)));
const H2: &str = "x95395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a";
let r2 = validate_sha2(H2);
assert!(r2.is_err_and(|x| matches!(x, ValidationError::InvalidSha2String)));
}
#[traced_test]
#[test]
fn test_rfc3987_with_url_crate() {
const URIS: &[&str] = &[
"https://tools.ietf.org/html/rfc3987",
"https://datatracker.ietf.org/doc/html/rfc3987",
"http://xn--rsum-bpad.example.org",
"http://r%C3%A9sum%C3%A9.example.org",
"http://example.com/%F0%90%8C%80%F0%90%8C%81%F0%90%8C%82",
"http://www.example.org/r%C3%A9sum%C3%A9.html",
"http://www.example.org/r%E9sum%E9.html",
"http://www.example.org/D%C3%BCrst",
"http://www.example.org/D%FCrst",
"http://xn--99zt52a.example.org/%e2%80%ae",
"http://xn--99zt52a.example.org/%E2%80%AE",
"http://ab.CDEFGH.ij/kl/mn/op.html",
"http://ab.CDE.FGH/ij/kl/mn/op.html",
"http://AB.CD.ef/gh/IJ/KL.html",
"http://ab.cd.EF/GH/ij/kl.html",
"http://ab.CD.EF/GH/IJ/kl.html",
"http://ab.CDE123FGH.ij/kl/mn/op.html",
"http://ab.cd.ef/GH1/2IJ/KL.html",
"http://ab.cd.ef/GH%31/%32IJ/KL.html",
"http://ab.CDEFGH.123/kl/mn/op.html",
"eXAMPLE://a/./b/../b/%63/%7bfoo%7d/ros%C3%A9",
"HTTP://www.EXAMPLE.com/",
"http://www.example.com/",
"http://example.org/~user",
"http://example.org/%7euser",
"http://example.org/%7Euser",
"http://example.com",
"http://example.com/",
"http://example.com:/",
"http://example.com:80/",
"http://example.com/data",
"http://example.com/data/",
"http://www.example.org/red%09ros%C3%A9#red",
"http://AB.CD.EF/GH/IJ/KL?MN=OP;QR=ST#UV",
"http://r\u{E9}sum\u{E9}.example.org",
"http://example.com/\u{10300}\u{10301}\u{10302}",
"http://www.example.org/D\u{FC}rst",
"http://\u{7D0D}\u{8C46}.example.org/%E2%80%AE",
"http://example.org/ros\u{E9}",
"example://a/b/c/%7Bfoo%7D/ros\u{E9}",
"http://www.example.org/r\u{E9}sum\u{E9}.html",
"http://www.example.org/re\u{301}sume\u{301}.html",
"http://www.example.org/r%E9sum%E9.xml#r\u{E9}sum\u{E9}",
];
for data in URIS {
let uri = Url::parse(data);
match uri {
Ok(_) => {}
Err(x) => {
error!("Failed <{}>: {}", data, x);
let iri = IriStr::new(data);
match iri {
Ok(_) => info!("...but passed iri_string!"),
Err(x) => error!("...and iri_string: {}", x),
}
}
}
}
}
}