solverforge_core/constraints/
constraint.rs

1use crate::constraints::{StreamComponent, WasmFunction};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5pub struct Constraint {
6    pub name: String,
7    #[serde(skip_serializing_if = "Option::is_none")]
8    pub package: Option<String>,
9    #[serde(skip_serializing_if = "Option::is_none")]
10    pub description: Option<String>,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub group: Option<String>,
13    pub components: Vec<StreamComponent>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub indictment: Option<WasmFunction>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub justification: Option<WasmFunction>,
18}
19
20impl Constraint {
21    pub fn new(name: impl Into<String>) -> Self {
22        Self {
23            name: name.into(),
24            package: None,
25            description: None,
26            group: None,
27            components: Vec::new(),
28            indictment: None,
29            justification: None,
30        }
31    }
32
33    pub fn with_package(mut self, package: impl Into<String>) -> Self {
34        self.package = Some(package.into());
35        self
36    }
37
38    pub fn with_description(mut self, description: impl Into<String>) -> Self {
39        self.description = Some(description.into());
40        self
41    }
42
43    pub fn with_group(mut self, group: impl Into<String>) -> Self {
44        self.group = Some(group.into());
45        self
46    }
47
48    pub fn with_component(mut self, component: StreamComponent) -> Self {
49        self.components.push(component);
50        self
51    }
52
53    pub fn with_components(mut self, components: Vec<StreamComponent>) -> Self {
54        self.components = components;
55        self
56    }
57
58    pub fn with_indictment(mut self, indictment: WasmFunction) -> Self {
59        self.indictment = Some(indictment);
60        self
61    }
62
63    pub fn with_justification(mut self, justification: WasmFunction) -> Self {
64        self.justification = Some(justification);
65        self
66    }
67
68    pub fn full_name(&self) -> String {
69        match &self.package {
70            Some(pkg) => format!("{}/{}", pkg, self.name),
71            None => self.name.clone(),
72        }
73    }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
77pub struct ConstraintSet {
78    pub constraints: Vec<Constraint>,
79}
80
81impl ConstraintSet {
82    pub fn new() -> Self {
83        Self {
84            constraints: Vec::new(),
85        }
86    }
87
88    pub fn with_constraint(mut self, constraint: Constraint) -> Self {
89        self.constraints.push(constraint);
90        self
91    }
92
93    pub fn add_constraint(&mut self, constraint: Constraint) {
94        self.constraints.push(constraint);
95    }
96
97    pub fn len(&self) -> usize {
98        self.constraints.len()
99    }
100
101    pub fn is_empty(&self) -> bool {
102        self.constraints.is_empty()
103    }
104
105    pub fn iter(&self) -> impl Iterator<Item = &Constraint> {
106        self.constraints.iter()
107    }
108
109    pub fn to_dto(&self) -> indexmap::IndexMap<String, Vec<StreamComponent>> {
110        self.constraints
111            .iter()
112            .map(|c| (c.full_name(), c.components.clone()))
113            .collect()
114    }
115}
116
117impl FromIterator<Constraint> for ConstraintSet {
118    fn from_iter<I: IntoIterator<Item = Constraint>>(iter: I) -> Self {
119        ConstraintSet {
120            constraints: iter.into_iter().collect(),
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::constraints::Joiner;
129
130    #[test]
131    fn test_constraint_new() {
132        let constraint = Constraint::new("Room conflict");
133        assert_eq!(constraint.name, "Room conflict");
134        assert!(constraint.package.is_none());
135        assert!(constraint.components.is_empty());
136    }
137
138    #[test]
139    fn test_constraint_with_package() {
140        let constraint = Constraint::new("Room conflict").with_package("timetabling");
141        assert_eq!(constraint.package, Some("timetabling".to_string()));
142    }
143
144    #[test]
145    fn test_constraint_with_description() {
146        let constraint =
147            Constraint::new("Room conflict").with_description("Two lessons in same room");
148        assert_eq!(
149            constraint.description,
150            Some("Two lessons in same room".to_string())
151        );
152    }
153
154    #[test]
155    fn test_constraint_with_group() {
156        let constraint = Constraint::new("Room conflict").with_group("Hard constraints");
157        assert_eq!(constraint.group, Some("Hard constraints".to_string()));
158    }
159
160    #[test]
161    fn test_constraint_with_component() {
162        let constraint = Constraint::new("Room conflict")
163            .with_component(StreamComponent::for_each("Lesson"))
164            .with_component(StreamComponent::penalize("1hard"));
165        assert_eq!(constraint.components.len(), 2);
166    }
167
168    #[test]
169    fn test_constraint_with_components() {
170        let components = vec![
171            StreamComponent::for_each("Lesson"),
172            StreamComponent::penalize("1hard"),
173        ];
174        let constraint = Constraint::new("Room conflict").with_components(components);
175        assert_eq!(constraint.components.len(), 2);
176    }
177
178    #[test]
179    fn test_constraint_with_indictment() {
180        let constraint =
181            Constraint::new("Room conflict").with_indictment(WasmFunction::new("get_room"));
182        assert!(constraint.indictment.is_some());
183    }
184
185    #[test]
186    fn test_constraint_with_justification() {
187        let constraint = Constraint::new("Room conflict")
188            .with_justification(WasmFunction::new("create_justification"));
189        assert!(constraint.justification.is_some());
190    }
191
192    #[test]
193    fn test_constraint_full_name() {
194        let constraint1 = Constraint::new("Room conflict");
195        assert_eq!(constraint1.full_name(), "Room conflict");
196
197        let constraint2 = Constraint::new("Room conflict").with_package("timetabling");
198        assert_eq!(constraint2.full_name(), "timetabling/Room conflict");
199    }
200
201    #[test]
202    fn test_constraint_set_new() {
203        let set = ConstraintSet::new();
204        assert!(set.is_empty());
205        assert_eq!(set.len(), 0);
206    }
207
208    #[test]
209    fn test_constraint_set_with_constraint() {
210        let set = ConstraintSet::new()
211            .with_constraint(Constraint::new("Constraint 1"))
212            .with_constraint(Constraint::new("Constraint 2"));
213        assert_eq!(set.len(), 2);
214    }
215
216    #[test]
217    fn test_constraint_set_add_constraint() {
218        let mut set = ConstraintSet::new();
219        set.add_constraint(Constraint::new("Constraint 1"));
220        set.add_constraint(Constraint::new("Constraint 2"));
221        assert_eq!(set.len(), 2);
222    }
223
224    #[test]
225    fn test_constraint_set_iter() {
226        let set = ConstraintSet::new()
227            .with_constraint(Constraint::new("C1"))
228            .with_constraint(Constraint::new("C2"));
229
230        let names: Vec<_> = set.iter().map(|c| c.name.as_str()).collect();
231        assert_eq!(names, vec!["C1", "C2"]);
232    }
233
234    #[test]
235    fn test_constraint_set_from_iter() {
236        let constraints = vec![Constraint::new("C1"), Constraint::new("C2")];
237        let set: ConstraintSet = constraints.into_iter().collect();
238        assert_eq!(set.len(), 2);
239    }
240
241    #[test]
242    fn test_constraint_json_serialization() {
243        let constraint = Constraint::new("Room conflict")
244            .with_package("timetabling")
245            .with_component(StreamComponent::for_each_unique_pair_with_joiners(
246                "Lesson",
247                vec![Joiner::equal(WasmFunction::new("get_timeslot"))],
248            ))
249            .with_component(StreamComponent::filter(WasmFunction::new("same_room")))
250            .with_component(StreamComponent::penalize("1hard"));
251
252        let json = serde_json::to_string(&constraint).unwrap();
253        assert!(json.contains("\"name\":\"Room conflict\""));
254        assert!(json.contains("\"package\":\"timetabling\""));
255        assert!(json.contains("\"components\""));
256
257        let parsed: Constraint = serde_json::from_str(&json).unwrap();
258        assert_eq!(parsed, constraint);
259    }
260
261    #[test]
262    fn test_constraint_set_json_serialization() {
263        let set = ConstraintSet::new()
264            .with_constraint(
265                Constraint::new("C1")
266                    .with_component(StreamComponent::for_each("Lesson"))
267                    .with_component(StreamComponent::penalize("1hard")),
268            )
269            .with_constraint(
270                Constraint::new("C2")
271                    .with_component(StreamComponent::for_each("Room"))
272                    .with_component(StreamComponent::reward("1soft")),
273            );
274
275        let json = serde_json::to_string(&set).unwrap();
276        let parsed: ConstraintSet = serde_json::from_str(&json).unwrap();
277        assert_eq!(parsed.len(), 2);
278    }
279
280    #[test]
281    fn test_realistic_room_conflict_constraint() {
282        let constraint = Constraint::new("Room conflict")
283            .with_package("school.timetabling")
284            .with_description("A room can accommodate at most one lesson at the same time.")
285            .with_group("Hard constraints")
286            .with_component(StreamComponent::for_each_unique_pair_with_joiners(
287                "Lesson",
288                vec![
289                    Joiner::equal(WasmFunction::new("get_timeslot")),
290                    Joiner::equal(WasmFunction::new("get_room")),
291                ],
292            ))
293            .with_component(StreamComponent::penalize("1hard"));
294
295        assert_eq!(constraint.components.len(), 2);
296        assert_eq!(constraint.full_name(), "school.timetabling/Room conflict");
297    }
298
299    #[test]
300    fn test_constraint_clone() {
301        let constraint = Constraint::new("Test")
302            .with_package("pkg")
303            .with_component(StreamComponent::for_each("Entity"));
304        let cloned = constraint.clone();
305        assert_eq!(constraint, cloned);
306    }
307
308    #[test]
309    fn test_constraint_debug() {
310        let constraint = Constraint::new("Test");
311        let debug = format!("{:?}", constraint);
312        assert!(debug.contains("Constraint"));
313        assert!(debug.contains("Test"));
314    }
315}