use std::borrow::Cow;
use std::fmt;
use crate::error::{IssueCode, IssueParams};
#[derive(Clone, Debug, PartialEq)]
pub struct RefineRejection {
pub code: Option<IssueCode>,
pub message: Cow<'static, str>,
pub params: IssueParams,
}
pub trait Rule<T: ?Sized>: fmt::Debug + Send + Sync {
fn name(&self) -> &str;
fn validate(&self, value: &T) -> Option<RefineRejection>;
}
pub struct ClosureRule<T: ?Sized> {
f: Box<dyn Fn(&T) -> bool + Send + Sync>,
name: Cow<'static, str>,
}
impl<T: ?Sized> fmt::Debug for ClosureRule<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ClosureRule")
.field("name", &self.name)
.finish_non_exhaustive()
}
}
impl<T: ?Sized> ClosureRule<T> {
#[must_use]
pub fn new(f: impl Fn(&T) -> bool + Send + Sync + 'static) -> Self {
Self {
f: Box::new(f),
name: Cow::Borrowed("refine-0"),
}
}
#[must_use]
pub fn with_ordinal(ordinal: usize, f: impl Fn(&T) -> bool + Send + Sync + 'static) -> Self {
Self {
f: Box::new(f),
name: Cow::Owned(format!("refine-{ordinal}")),
}
}
}
impl<T: ?Sized> Rule<T> for ClosureRule<T> {
fn name(&self) -> &str {
&self.name
}
fn validate(&self, value: &T) -> Option<RefineRejection> {
if (self.f)(value) {
None
} else {
Some(RefineRejection {
code: Some(IssueCode::Refine),
message: Cow::Borrowed("failed refinement check"),
params: Vec::new(),
})
}
}
}
#[cfg(test)]
mod tests {
use crate::error::IssueCode;
use crate::rule::{ClosureRule, RefineRejection, Rule};
use std::borrow::Cow;
#[test]
fn rf3_closure_rule_signature_is_sync() {
fn assert_sync<T: Sync>() {}
fn assert_send<T: Send>() {}
assert_sync::<ClosureRule<fn(&str) -> bool>>();
assert_send::<ClosureRule<fn(&str) -> bool>>();
}
#[test]
fn closure_rule_pass_yields_none() {
let r = ClosureRule::new(|_s: &str| true);
assert!(r.validate("abc").is_none());
}
#[test]
fn closure_rule_fail_yields_default_rejection() {
let r = ClosureRule::new(|_s: &str| false);
let rej: RefineRejection = r.validate("abc").unwrap();
assert_eq!(rej.code, Some(IssueCode::Refine));
assert_eq!(rej.message, Cow::Borrowed("failed refinement check"));
}
#[test]
fn closure_rule_name_is_refine_0_by_default() {
let r = ClosureRule::new(|_s: &str| false);
assert_eq!(r.name(), "refine-0");
}
#[test]
fn closure_rule_with_ordinal_names_stably() {
let r = ClosureRule::with_ordinal(2, |_s: &str| false);
assert_eq!(r.name(), "refine-2");
}
#[test]
fn closure_rule_ordinal_name_owned_not_leaked() {
let r = ClosureRule::with_ordinal(4096, |_s: &str| true);
assert_eq!(r.name(), "refine-4096");
}
#[test]
fn custom_rule_implementor_participates() {
#[derive(Debug)]
struct StartsWith(char);
impl Rule<str> for StartsWith {
fn name(&self) -> &'static str {
"starts_with"
}
fn validate(&self, value: &str) -> Option<RefineRejection> {
if value.starts_with(self.0) {
None
} else {
Some(RefineRejection {
code: None,
message: format!("must start with '{}'", self.0).into(),
params: Vec::new(),
})
}
}
}
let r = StartsWith('a');
assert!(r.validate("apple").is_none());
let rej = r.validate("pear").unwrap();
assert_eq!(rej.code, None, "None means IssueCode::Refine at the kernel");
assert_eq!(r.name(), "starts_with");
}
}