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#[doc(hidden)]
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum ChangeSource {
52    Unknown,
53    Static,
54    Descriptor(usize),
55}
56
57impl ChangeSource {
58    #[inline]
59    pub fn reacts_to(self, descriptor_index: usize) -> bool {
60        match self {
61            Self::Unknown => true,
62            Self::Static => false,
63            Self::Descriptor(index) => index == descriptor_index,
64        }
65    }
66
67    #[inline]
68    pub fn owns_descriptor(self, descriptor_index: usize) -> bool {
69        matches!(self, Self::Descriptor(index) if index == descriptor_index)
70    }
71
72    #[inline]
73    pub fn is_unknown(self) -> bool {
74        matches!(self, Self::Unknown)
75    }
76
77    #[inline]
78    pub fn same_index_domain(self, other: Self) -> bool {
79        matches!((self, other), (Self::Descriptor(left), Self::Descriptor(right)) if left == right)
80    }
81
82    #[inline]
83    pub fn assert_localizes(self, descriptor_index: usize, constraint_name: &str) -> bool {
84        if self.owns_descriptor(descriptor_index) {
85            return true;
86        }
87        if self.reacts_to(descriptor_index) {
88            panic!(
89                "constraint `{constraint_name}` received descriptor {descriptor_index}, but source {self:?} cannot localize entity indexes"
90            );
91        }
92        false
93    }
94}
95
96pub trait FlattenExtract<P>: Send + Sync {
97    type Item;
98
99    fn extract<'s>(&self, parent: &'s P) -> &'s [Self::Item];
100}
101
102impl<S, A, F> CollectionExtract<S> for F
103where
104    F: for<'a> Fn(&'a S) -> &'a [A] + Send + Sync,
105{
106    type Item = A;
107
108    #[inline]
109    fn extract<'s>(&self, s: &'s S) -> &'s [A] {
110        self(s)
111    }
112}
113
114impl<P, B, F> FlattenExtract<P> for F
115where
116    F: for<'a> Fn(&'a P) -> &'a [B] + Send + Sync,
117{
118    type Item = B;
119
120    #[inline]
121    fn extract<'s>(&self, parent: &'s P) -> &'s [B] {
122        self(parent)
123    }
124}
125
126#[derive(Clone, Copy)]
127pub struct FlattenVecExtract<F>(pub F);
128
129impl<P, B, F> FlattenExtract<P> for FlattenVecExtract<F>
130where
131    F: for<'a> Fn(&'a P) -> &'a Vec<B> + Send + Sync,
132{
133    type Item = B;
134
135    #[inline]
136    fn extract<'s>(&self, parent: &'s P) -> &'s [B] {
137        (self.0)(parent).as_slice()
138    }
139}
140
141#[doc(hidden)]
142#[derive(Clone, Copy)]
143pub struct SourceExtract<E> {
144    extractor: E,
145    change_source: ChangeSource,
146}
147
148impl<E> SourceExtract<E> {
149    pub fn new(extractor: E, change_source: ChangeSource) -> Self {
150        Self {
151            extractor,
152            change_source,
153        }
154    }
155
156    pub fn extractor(&self) -> &E {
157        &self.extractor
158    }
159}
160
161impl<S, E> CollectionExtract<S> for SourceExtract<E>
162where
163    E: CollectionExtract<S>,
164{
165    type Item = E::Item;
166
167    #[inline]
168    fn extract<'s>(&self, s: &'s S) -> &'s [Self::Item] {
169        self.extractor.extract(s)
170    }
171
172    #[inline]
173    fn contains(&self, s: &S, item: &Self::Item) -> bool {
174        self.extractor.contains(s, item)
175    }
176
177    fn change_source(&self) -> ChangeSource {
178        self.change_source
179    }
180}
181
182/* Wraps a `Fn(&S) -> &Vec<A>` closure so it satisfies `CollectionExtract<S>`.
183
184Construct via the [`vec`] free function.
185*/
186pub struct VecExtract<F>(pub F);
187
188impl<S, A, F> CollectionExtract<S> for VecExtract<F>
189where
190    F: for<'a> Fn(&'a S) -> &'a Vec<A> + Send + Sync,
191{
192    type Item = A;
193
194    #[inline]
195    fn extract<'s>(&self, s: &'s S) -> &'s [A] {
196        (self.0)(s).as_slice()
197    }
198}
199
200/* Wraps a `Fn(&S) -> &Vec<A>` closure into a [`VecExtract`] that satisfies
201[`CollectionExtract<S>`].
202
203Use this when your solution field is a `Vec<A>` and you want to write
204`|s| &s.field` instead of `|s| s.field.as_slice()`.
205
206# Example
207
208```
209use solverforge_scoring::stream::collection_extract::{CollectionExtract, vec};
210
211struct Schedule { employees: Vec<String> }
212
213let extractor = vec(|s: &Schedule| &s.employees);
214let schedule = Schedule { employees: vec!["Alice".into()] };
215assert_eq!(extractor.extract(&schedule), &["Alice".to_string()]);
216```
217*/
218pub fn vec<S, A, F>(f: F) -> VecExtract<F>
219where
220    F: for<'a> Fn(&'a S) -> &'a Vec<A> + Send + Sync,
221{
222    VecExtract(f)
223}
224
225#[doc(hidden)]
226pub fn source<E>(extractor: E, change_source: ChangeSource) -> SourceExtract<E> {
227    SourceExtract::new(extractor, change_source)
228}