You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

161 lines
3.7 KiB
Rust

use std::fmt;
use crate::core::*;
/// Sanitized user input
#[repr(transparent)]
pub struct Sane<T>(T);
/// Unsanitized user input
#[repr(transparent)]
pub struct Insane<T>(T);
/// Anything that can be validated (used for transforming [`Insane`] to [`Sane`])
pub trait Validate: Sized {
fn validate(&self, builder: ResultBuilder) -> ResultBuilder;
}
/// A field in a struct that can be validated and that is reused in multiple places.
/// Primarily useful for newtype structs.
pub trait ValidateField<'a>: Sized {
fn validate_field(builder: FieldResultBuilder<'a, Self>) -> FieldResultBuilder<'a, Self>;
}
/// Validation error
#[derive(Debug)]
pub struct Error {
pub errors: Vec<(String, String)>,
}
/// Little helper to make tracking validation errors more convenient
pub struct ResultBuilder {
errors: Vec<(String, String)>,
}
pub struct FieldResultBuilder<'a, T> {
val: &'a T,
errors: Vec<String>,
}
impl<T> Sane<T> {
pub fn inner(self) -> T {
self.0
}
}
impl<T> From<T> for Insane<T> {
fn from(t: T) -> Insane<T> {
Insane(t)
}
}
impl<T> Insane<T> {
pub fn inner(self) -> T {
self.0
}
}
impl<T> TryFrom<Insane<T>> for Sane<T>
where
T: Validate,
{
type Error = crate::core::Error;
fn try_from(t: Insane<T>) -> Result<Sane<T>> {
Ok(Sane(
(&t.0).validate(ResultBuilder::new()).result(t.inner())?,
))
}
}
impl ResultBuilder {
pub fn new() -> ResultBuilder {
ResultBuilder {
errors: Vec::with_capacity(0),
}
}
pub fn check<T, N, M, F>(mut self, val: &T, field_name: N, error_msg: M, check: F) -> Self
where
N: Into<String>,
M: Into<String>,
F: FnOnce(&T) -> bool,
{
if !check(val) {
self.errors.push((field_name.into(), error_msg.into()));
}
self
}
/// Begin sanitization on a single field
pub fn field<'a, T, N, F>(mut self, val: &'a T, name: N, check: F) -> Self
where
N: Into<&'a str>,
F: FnOnce(FieldResultBuilder<T>) -> FieldResultBuilder<T>,
{
let builder = check(FieldResultBuilder {
val,
errors: Vec::with_capacity(0),
});
builder.append_errors(name.into(), &mut self.errors);
self
}
/// Validate a field that implements [`ValidateField`]
pub fn field_auto<'a, T, N>(mut self, val: &'a T, name: N) -> Self
where
T: ValidateField<'a>,
N: Into<&'a str>,
{
let builder = T::validate_field(FieldResultBuilder {
val,
errors: Vec::with_capacity(0),
});
builder.append_errors(name.into(), &mut self.errors);
self
}
/// Obtain the final result of the validation
fn result<T>(self, success_val: T) -> Result<T> {
if self.errors.is_empty() {
Ok(success_val)
} else {
Err(Error {
errors: self.errors,
}
.into())
}
}
}
impl<'a, T> FieldResultBuilder<'a, T> {
pub fn check<M, F>(mut self, err_msg: M, check: F) -> Self
where
M: Into<String>,
F: FnOnce(&T) -> bool,
{
let err_msg = err_msg.into();
if !check(self.val) {
self.errors.push(err_msg);
}
self
}
fn append_errors(self, field_name: &str, errors: &mut Vec<(String, String)>) {
errors.extend(
self.errors
.into_iter()
.map(|msg| (String::from(field_name), msg)),
);
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for (field, msg) in &self.errors {
writeln!(f, "in field \"{}\": {}", field, msg)?;
}
Ok(())
}
}