solverforge_core/constraints/
constraint.rs1use 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}