From be5e16c31548ae180eb0e78549f482e42a8d5652 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 17:43:14 +0800 Subject: [PATCH] feat: add map combinator to project input before evaluation Add MapPredicate and PredicateMapExt::map so predicates on a field type (e.g. TaskId) can be reused when filtering a larger type (e.g. Task), including in boxed boolean combinations (#142). --- src/lib.rs | 1 + src/map.rs | 190 +++++++++++++++++++++++++++++++++++++++++++++++++ src/prelude.rs | 1 + 3 files changed, 192 insertions(+) create mode 100644 src/map.rs diff --git a/src/lib.rs b/src/lib.rs index 5e254de..20d2dd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,6 +213,7 @@ pub use crate::boxed::*; pub mod constant; pub mod function; pub mod iter; +pub mod map; pub mod name; pub mod ord; diff --git a/src/map.rs b/src/map.rs new file mode 100644 index 0000000..c6a96e3 --- /dev/null +++ b/src/map.rs @@ -0,0 +1,190 @@ +// Copyright (c) 2018 The predicates-rs Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Map input values before evaluating an inner predicate. + +use std::fmt; +use std::marker::PhantomData; + +use crate::Predicate; +use crate::reflection; + +/// Predicate adapter that maps the input value before evaluation. +/// +/// This is created by [`PredicateMapExt::map`]. +pub struct MapPredicate +where + Inner: Predicate, + Item: ?Sized, +{ + inner: Inner, + map: F, + _phantom: PhantomData Mapped>, +} + +impl MapPredicate +where + Inner: Predicate, + Item: ?Sized, +{ + /// Create a new `MapPredicate` from an inner predicate and a mapping function. + pub fn new(inner: Inner, map: F) -> Self { + Self { + inner, + map, + _phantom: PhantomData, + } + } +} + +unsafe impl Send for MapPredicate +where + Inner: Predicate + Send, + F: Send, + Item: ?Sized, +{ +} + +unsafe impl Sync for MapPredicate +where + Inner: Predicate + Sync, + F: Sync, + Item: ?Sized, +{ +} + +impl Predicate for MapPredicate +where + Inner: Predicate, + F: Fn(&Item) -> Mapped, + Item: ?Sized, +{ + fn eval(&self, variable: &Item) -> bool { + let mapped = (self.map)(variable); + self.inner.eval(&mapped) + } + + fn find_case<'a>(&'a self, expected: bool, variable: &Item) -> Option> { + let mapped = (self.map)(variable); + self.inner + .find_case(expected, &mapped) + .map(|child_case| reflection::Case::new(Some(self), expected).add_child(child_case)) + } +} + +impl reflection::PredicateReflection for MapPredicate +where + Inner: Predicate, + Item: ?Sized, +{ + fn children<'a>(&'a self) -> Box> + 'a> { + let params = vec![reflection::Child::new("predicate", &self.inner)]; + Box::new(params.into_iter()) + } +} + +impl fmt::Display for MapPredicate +where + Inner: Predicate + fmt::Display, + Item: ?Sized, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +} + +impl fmt::Debug for MapPredicate +where + Inner: Predicate + fmt::Debug, + Item: ?Sized, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MapPredicate") + .field("inner", &self.inner) + .finish() + } +} + +/// `Predicate` extension that maps the input value before evaluation. +pub trait PredicateMapExt { + /// Map the input value before passing it to this predicate. + /// + /// This is useful when a predicate is defined on a field or projection of a + /// larger type, for example filtering `Task` values using `predicate::in_hash` + /// on `TaskId`. + /// + /// # Examples + /// + /// ``` + /// use predicates::prelude::*; + /// use std::collections::HashSet; + /// + /// #[derive(Debug)] + /// struct Task { + /// id: u32, + /// } + /// + /// let ids: HashSet = [1, 2].into_iter().collect(); + /// let in_ids = predicate::in_hash(ids); + /// let has_id = in_ids.map(|task: &Task| task.id); + /// + /// assert!(has_id.eval(&Task { id: 1 })); + /// assert!(!has_id.eval(&Task { id: 3 })); + /// ``` + fn map(self, map: F) -> MapPredicate + where + Self: Sized + Predicate, + Item: ?Sized, + F: Fn(&Item) -> Mapped; +} + +impl PredicateMapExt for Inner { + fn map(self, map: F) -> MapPredicate + where + Self: Sized + Predicate, + Item: ?Sized, + F: Fn(&Item) -> Mapped, + { + MapPredicate::new(self, map) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use crate::prelude::*; + + #[derive(Debug)] + struct Task { + id: u32, + } + + #[test] + fn map_in_hash() { + let ids: HashSet = [1, 2].into_iter().collect(); + let has_id = predicate::in_hash(ids).map(|task: &Task| task.id); + + assert!(has_id.eval(&Task { id: 1 })); + assert!(!has_id.eval(&Task { id: 3 })); + } + + #[test] + fn map_combines_with_boxed() { + let ids: HashSet = [1].into_iter().collect(); + let has_id = predicate::in_hash(ids).map(|task: &Task| task.id); + + let predicate = predicate::always() + .boxed() + .and(has_id.not()) + .boxed(); + + assert!(predicate.eval(&Task { id: 2 })); + assert!(!predicate.eval(&Task { id: 1 })); + } +} diff --git a/src/prelude.rs b/src/prelude.rs index ebd58b3..afd8ab5 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -11,6 +11,7 @@ pub use crate::Predicate; pub use crate::boolean::PredicateBooleanExt; pub use crate::boxed::PredicateBoxExt; +pub use crate::map::PredicateMapExt; pub use crate::name::PredicateNameExt; pub use crate::path::PredicateFileContentExt; pub use crate::str::PredicateStrExt;