Skip to main content

solverforge_scoring/stream/
collection_extract.rs

1/* CollectionExtract trait for ergonomic entity collection extraction.
2
3Allows extractor closures to return either `&[A]` or `&Vec<A>`,
4so users can write `vec(|s| &s.employees)` without `.as_slice()`.
5
6# Usage
7
8```
9use solverforge_scoring::stream::collection_extract::{CollectionExtract, VecExtract, vec};
10
11struct Schedule { employees: Vec<String> }
12
13// Direct slice closure — works out of the box:
14let e1 = |s: &Schedule| s.employees.as_slice();
15let _: &[String] = e1.extract(&Schedule { employees: vec![] });
16
17// Vec reference closure — wrap with `vec(...)`:
18let e2 = vec(|s: &Schedule| &s.employees);
19let _: &[String] = e2.extract(&Schedule { employees: vec![] });
20```
21*/
22
23/* Extracts a slice of entities from the solution.
24
25The associated type `Item` names the entity type, allowing callers to
26write `E: CollectionExtract<S, Item = A>` when `A` must be inferred from `E`
27rather than stated as a separate generic parameter.
28*/
29pub trait CollectionExtract<S>: Send + Sync {
30    // The entity type yielded by this extractor.
31    type Item;
32
33    // Extracts the entity slice from the solution.
34    fn extract<'s>(&self, s: &'s S) -> &'s [Self::Item];
35
36    // Reports whether this extractor's own source-level predicate accepts an item.
37    fn contains(&self, _s: &S, _item: &Self::Item) -> bool {
38        true
39    }
40
41    // Identifies whether the solution source owns descriptor-scoped localized updates.
42    // Plain extractors are non-localized. Macro-generated solution source methods
43    // attach descriptor/static metadata through hidden internal support.
44    fn change_source(&self) -> ChangeSource {
45        ChangeSource::Unknown
46    }
47}
48
49/* Preserves a unary stream's membership at the source boundary, before
50indexed joins build keys or materialize pairs. The original slice and index
51mapping remain unchanged so descriptor-local mutations keep their identity. */
52#[doc(hidden)]
53pub struct FilteredExtract<E, F> {
54    extractor: E,
55    filter: F,
56}
57
58impl<E, F> FilteredExtract<E, F> {
59    pub fn new(extractor: E, filter: F) -> Self {
60        Self { extractor, filter }
61    }
62}
63
64impl<S, E, F> CollectionExtract<S> for FilteredExtract<E, F>
65where
66    E: CollectionExtract<S>,
67    F: super::filter::UniFilter<S, E::Item>,
68{
69    type Item = E::Item;
70
71    fn extract<'s>(&self, s: &'s S) -> &'s [Self::Item] {
72        self.extractor.extract(s)
73    }
74
75    fn contains(&self, s: &S, item: &Self::Item) -> bool {
76        self.extractor.contains(s, item) && self.filter.test(s, item)
77    }
78
79    fn change_source(&self) -> ChangeSource {
80        self.extractor.change_source()
81    }
82}
83
84#[doc(hidden)]
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum ChangeSource {
87    Unknown,
88    Static,
89    Descriptor(usize),
90}
91
92impl ChangeSource {
93    #[inline]
94    pub fn reacts_to(self, descriptor_index: usize) -> bool {
95        match self {
96            Self::Unknown => true,
97            Self::Static => false,
98            Self::Descriptor(index) => index == descriptor_index,
99        }
100    }
101
102    #[inline]
103    pub fn owns_descriptor(self, descriptor_index: usize) -> bool {
104        matches!(self, Self::Descriptor(index) if index == descriptor_index)
105    }
106
107    #[inline]
108    pub fn is_unknown(self) -> bool {
109        matches!(self, Self::Unknown)
110    }
111
112    #[inline]
113    pub fn same_index_domain(self, other: Self) -> bool {
114        matches!((self, other), (Self::Descriptor(left), Self::Descriptor(right)) if left == right)
115    }
116
117    #[inline]
118    pub fn assert_localizes(self, descriptor_index: usize, constraint_name: &str) -> bool {
119        if self.owns_descriptor(descriptor_index) {
120            return true;
121        }
122        if self.reacts_to(descriptor_index) {
123            panic!(
124                "constraint `{constraint_name}` received descriptor {descriptor_index}, but source {self:?} cannot localize entity indexes"
125            );
126        }
127        false
128    }
129}
130
131pub trait FlattenExtract<P>: Send + Sync {
132    type Item;
133
134    fn extract<'s>(&self, parent: &'s P) -> &'s [Self::Item];
135}
136
137impl<S, A, F> CollectionExtract<S> for F
138where
139    F: for<'a> Fn(&'a S) -> &'a [A] + Send + Sync,
140{
141    type Item = A;
142
143    #[inline]
144    fn extract<'s>(&self, s: &'s S) -> &'s [A] {
145        self(s)
146    }
147}
148
149impl<P, B, F> FlattenExtract<P> for F
150where
151    F: for<'a> Fn(&'a P) -> &'a [B] + Send + Sync,
152{
153    type Item = B;
154
155    #[inline]
156    fn extract<'s>(&self, parent: &'s P) -> &'s [B] {
157        self(parent)
158    }
159}
160
161#[derive(Clone, Copy)]
162pub struct FlattenVecExtract<F>(pub F);
163
164impl<P, B, F> FlattenExtract<P> for FlattenVecExtract<F>
165where
166    F: for<'a> Fn(&'a P) -> &'a Vec<B> + Send + Sync,
167{
168    type Item = B;
169
170    #[inline]
171    fn extract<'s>(&self, parent: &'s P) -> &'s [B] {
172        (self.0)(parent).as_slice()
173    }
174}
175
176#[doc(hidden)]
177#[derive(Clone, Copy)]
178pub struct SourceExtract<E> {
179    extractor: E,
180    change_source: ChangeSource,
181}
182
183impl<E> SourceExtract<E> {
184    pub fn new(extractor: E, change_source: ChangeSource) -> Self {
185        Self {
186            extractor,
187            change_source,
188        }
189    }
190
191    pub fn extractor(&self) -> &E {
192        &self.extractor
193    }
194}
195
196impl<S, E> CollectionExtract<S> for SourceExtract<E>
197where
198    E: CollectionExtract<S>,
199{
200    type Item = E::Item;
201
202    #[inline]
203    fn extract<'s>(&self, s: &'s S) -> &'s [Self::Item] {
204        self.extractor.extract(s)
205    }
206
207    #[inline]
208    fn contains(&self, s: &S, item: &Self::Item) -> bool {
209        self.extractor.contains(s, item)
210    }
211
212    fn change_source(&self) -> ChangeSource {
213        self.change_source
214    }
215}
216
217/* Wraps a `Fn(&S) -> &Vec<A>` closure so it satisfies `CollectionExtract<S>`.
218
219Construct via the [`vec`] free function.
220*/
221pub struct VecExtract<F>(pub F);
222
223impl<S, A, F> CollectionExtract<S> for VecExtract<F>
224where
225    F: for<'a> Fn(&'a S) -> &'a Vec<A> + Send + Sync,
226{
227    type Item = A;
228
229    #[inline]
230    fn extract<'s>(&self, s: &'s S) -> &'s [A] {
231        (self.0)(s).as_slice()
232    }
233}
234
235/* Wraps a `Fn(&S) -> &Vec<A>` closure into a [`VecExtract`] that satisfies
236[`CollectionExtract<S>`].
237
238Use this when your solution field is a `Vec<A>` and you want to write
239`|s| &s.field` instead of `|s| s.field.as_slice()`.
240
241# Example
242
243```
244use solverforge_scoring::stream::collection_extract::{CollectionExtract, vec};
245
246struct Schedule { employees: Vec<String> }
247
248let extractor = vec(|s: &Schedule| &s.employees);
249let schedule = Schedule { employees: vec!["Alice".into()] };
250assert_eq!(extractor.extract(&schedule), &["Alice".to_string()]);
251```
252*/
253pub fn vec<S, A, F>(f: F) -> VecExtract<F>
254where
255    F: for<'a> Fn(&'a S) -> &'a Vec<A> + Send + Sync,
256{
257    VecExtract(f)
258}
259
260#[doc(hidden)]
261pub fn source<E>(extractor: E, change_source: ChangeSource) -> SourceExtract<E> {
262    SourceExtract::new(extractor, change_source)
263}