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