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