use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
#[async_trait]
pub trait DatabaseValidator: Send + Sync {
async fn exists(&self, table: &str, column: &str, value: &str) -> Result<bool, String>;
async fn is_unique(&self, table: &str, column: &str, value: &str) -> Result<bool, String>;
async fn is_unique_except(
&self,
table: &str,
column: &str,
value: &str,
except_id: &str,
) -> Result<bool, String>;
}
#[async_trait]
pub trait HttpValidator: Send + Sync {
async fn validate(&self, endpoint: &str, value: &str) -> Result<bool, String>;
}
#[async_trait]
pub trait CustomValidator: Send + Sync {
async fn validate(&self, value: &str) -> Result<bool, String>;
}
#[derive(Clone, Default)]
pub struct ValidationContext {
database: Option<Arc<dyn DatabaseValidator>>,
http: Option<Arc<dyn HttpValidator>>,
custom: HashMap<String, Arc<dyn CustomValidator>>,
exclude_id: Option<String>,
locale: Option<String>,
}
impl ValidationContext {
pub fn new() -> Self {
Self::default()
}
pub fn database(&self) -> Option<&Arc<dyn DatabaseValidator>> {
self.database.as_ref()
}
pub fn http(&self) -> Option<&Arc<dyn HttpValidator>> {
self.http.as_ref()
}
pub fn custom(&self, name: &str) -> Option<&Arc<dyn CustomValidator>> {
self.custom.get(name)
}
pub fn locale(&self) -> Option<&str> {
self.locale.as_deref()
}
pub fn exclude_id(&self) -> Option<&str> {
self.exclude_id.as_deref()
}
pub fn builder() -> ValidationContextBuilder {
ValidationContextBuilder::new()
}
}
impl std::fmt::Debug for ValidationContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ValidationContext")
.field("has_database", &self.database.is_some())
.field("has_http", &self.http.is_some())
.field("custom_validators", &self.custom.keys().collect::<Vec<_>>())
.field("exclude_id", &self.exclude_id)
.field("locale", &self.locale)
.finish()
}
}
#[derive(Clone, Default)]
pub struct ValidationContextBuilder {
database: Option<Arc<dyn DatabaseValidator>>,
http: Option<Arc<dyn HttpValidator>>,
custom: HashMap<String, Arc<dyn CustomValidator>>,
exclude_id: Option<String>,
locale: Option<String>,
}
impl ValidationContextBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn database(mut self, validator: impl DatabaseValidator + 'static) -> Self {
self.database = Some(Arc::new(validator));
self
}
pub fn database_arc(mut self, validator: Arc<dyn DatabaseValidator>) -> Self {
self.database = Some(validator);
self
}
pub fn http(mut self, validator: impl HttpValidator + 'static) -> Self {
self.http = Some(Arc::new(validator));
self
}
pub fn http_arc(mut self, validator: Arc<dyn HttpValidator>) -> Self {
self.http = Some(validator);
self
}
pub fn custom(
mut self,
name: impl Into<String>,
validator: impl CustomValidator + 'static,
) -> Self {
self.custom.insert(name.into(), Arc::new(validator));
self
}
pub fn custom_arc(
mut self,
name: impl Into<String>,
validator: Arc<dyn CustomValidator>,
) -> Self {
self.custom.insert(name.into(), validator);
self
}
pub fn exclude_id(mut self, id: impl Into<String>) -> Self {
self.exclude_id = Some(id.into());
self
}
pub fn locale(mut self, locale: impl Into<String>) -> Self {
self.locale = Some(locale.into());
self
}
pub fn build(self) -> ValidationContext {
ValidationContext {
database: self.database,
http: self.http,
custom: self.custom,
exclude_id: self.exclude_id,
locale: self.locale,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockDbValidator;
#[async_trait]
impl DatabaseValidator for MockDbValidator {
async fn exists(&self, _table: &str, _column: &str, _value: &str) -> Result<bool, String> {
Ok(true)
}
async fn is_unique(
&self,
_table: &str,
_column: &str,
_value: &str,
) -> Result<bool, String> {
Ok(true)
}
async fn is_unique_except(
&self,
_table: &str,
_column: &str,
_value: &str,
_except_id: &str,
) -> Result<bool, String> {
Ok(true)
}
}
#[test]
fn context_builder() {
let ctx = ValidationContextBuilder::new()
.database(MockDbValidator)
.exclude_id("123")
.build();
assert!(ctx.database().is_some());
assert!(ctx.http().is_none());
assert_eq!(ctx.exclude_id(), Some("123"));
}
#[test]
fn empty_context() {
let ctx = ValidationContext::new();
assert!(ctx.database().is_none());
assert!(ctx.http().is_none());
assert!(ctx.exclude_id().is_none());
}
}