1use crate::constraints::ConstraintSet;
8use crate::domain::{DomainClass, DomainModel};
9use crate::score::Score;
10use crate::value::Value;
11use crate::SolverForgeResult;
12
13pub trait DomainStruct: Send + Sync {
34 fn domain_class() -> DomainClass;
36}
37
38pub trait PlanningEntity: Send + Sync + Clone {
65 fn domain_class() -> DomainClass;
70
71 fn planning_id(&self) -> Value;
76
77 fn to_value(&self) -> Value;
81
82 fn from_value(value: &Value) -> SolverForgeResult<Self>
86 where
87 Self: Sized;
88}
89
90pub trait PlanningSolution: Send + Sync + Clone {
121 type Score: Score;
123
124 fn domain_model() -> DomainModel;
129
130 fn constraints() -> ConstraintSet;
135
136 fn score(&self) -> Option<Self::Score>;
138
139 fn set_score(&mut self, score: Self::Score);
141
142 fn to_json(&self) -> SolverForgeResult<String>;
144
145 fn from_json(json: &str) -> SolverForgeResult<Self>
147 where
148 Self: Sized;
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::constraints::Constraint;
155 use crate::domain::{FieldDescriptor, FieldType, PlanningAnnotation, PrimitiveType, ScoreType};
156 use crate::{HardSoftScore, SolverForgeError};
157 use std::collections::HashMap;
158
159 #[derive(Clone, Debug, PartialEq)]
161 struct Room {
162 id: String,
163 name: String,
164 }
165
166 #[derive(Clone, Debug, PartialEq)]
168 struct Lesson {
169 id: String,
170 subject: String,
171 room: Option<String>, }
173
174 impl PlanningEntity for Lesson {
175 fn domain_class() -> DomainClass {
176 DomainClass::new("Lesson")
177 .with_annotation(PlanningAnnotation::PlanningEntity)
178 .with_field(
179 FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
180 .with_annotation(PlanningAnnotation::PlanningId),
181 )
182 .with_field(FieldDescriptor::new(
183 "subject",
184 FieldType::Primitive(PrimitiveType::String),
185 ))
186 .with_field(
187 FieldDescriptor::new("room", FieldType::object("Room")).with_annotation(
188 PlanningAnnotation::planning_variable(vec!["rooms".to_string()]),
189 ),
190 )
191 }
192
193 fn planning_id(&self) -> Value {
194 Value::String(self.id.clone())
195 }
196
197 fn to_value(&self) -> Value {
198 let mut map = HashMap::new();
199 map.insert("id".to_string(), Value::String(self.id.clone()));
200 map.insert("subject".to_string(), Value::String(self.subject.clone()));
201 map.insert(
202 "room".to_string(),
203 self.room.clone().map(Value::String).unwrap_or(Value::Null),
204 );
205 Value::Object(map)
206 }
207
208 fn from_value(value: &Value) -> SolverForgeResult<Self> {
209 match value {
210 Value::Object(map) => {
211 let id = map
212 .get("id")
213 .and_then(|v| v.as_str())
214 .ok_or_else(|| SolverForgeError::Serialization("Missing id".to_string()))?
215 .to_string();
216 let subject = map
217 .get("subject")
218 .and_then(|v| v.as_str())
219 .ok_or_else(|| {
220 SolverForgeError::Serialization("Missing subject".to_string())
221 })?
222 .to_string();
223 let room = map.get("room").and_then(|v| v.as_str()).map(String::from);
224 Ok(Lesson { id, subject, room })
225 }
226 _ => Err(SolverForgeError::Serialization(
227 "Expected object".to_string(),
228 )),
229 }
230 }
231 }
232
233 #[derive(Clone, Debug)]
235 struct Timetable {
236 rooms: Vec<Room>,
237 lessons: Vec<Lesson>,
238 score: Option<HardSoftScore>,
239 }
240
241 impl PlanningSolution for Timetable {
242 type Score = HardSoftScore;
243
244 fn domain_model() -> DomainModel {
245 DomainModel::builder()
246 .add_class(Lesson::domain_class())
247 .add_class(DomainClass::new("Room").with_field(FieldDescriptor::new(
248 "id",
249 FieldType::Primitive(PrimitiveType::String),
250 )))
251 .add_class(
252 DomainClass::new("Timetable")
253 .with_annotation(PlanningAnnotation::PlanningSolution)
254 .with_field(
255 FieldDescriptor::new(
256 "rooms",
257 FieldType::list(FieldType::object("Room")),
258 )
259 .with_annotation(PlanningAnnotation::ProblemFactCollectionProperty)
260 .with_annotation(
261 PlanningAnnotation::value_range_provider_with_id("rooms"),
262 ),
263 )
264 .with_field(
265 FieldDescriptor::new(
266 "lessons",
267 FieldType::list(FieldType::object("Lesson")),
268 )
269 .with_annotation(PlanningAnnotation::PlanningEntityCollectionProperty),
270 )
271 .with_field(
272 FieldDescriptor::new("score", FieldType::Score(ScoreType::HardSoft))
273 .with_annotation(PlanningAnnotation::planning_score()),
274 ),
275 )
276 .build()
277 }
278
279 fn constraints() -> ConstraintSet {
280 ConstraintSet::new().with_constraint(Constraint::new("Test constraint"))
282 }
283
284 fn score(&self) -> Option<Self::Score> {
285 self.score
286 }
287
288 fn set_score(&mut self, score: Self::Score) {
289 self.score = Some(score);
290 }
291
292 fn to_json(&self) -> SolverForgeResult<String> {
293 let mut map = HashMap::new();
295
296 let rooms: Vec<Value> = self
297 .rooms
298 .iter()
299 .map(|r| {
300 let mut m = HashMap::new();
301 m.insert("id".to_string(), Value::String(r.id.clone()));
302 m.insert("name".to_string(), Value::String(r.name.clone()));
303 Value::Object(m)
304 })
305 .collect();
306 map.insert("rooms".to_string(), Value::Array(rooms));
307
308 let lessons: Vec<Value> = self.lessons.iter().map(|l| l.to_value()).collect();
309 map.insert("lessons".to_string(), Value::Array(lessons));
310
311 if let Some(score) = &self.score {
312 map.insert("score".to_string(), Value::String(format!("{}", score)));
313 }
314
315 serde_json::to_string(&Value::Object(map))
316 .map_err(|e| SolverForgeError::Serialization(e.to_string()))
317 }
318
319 fn from_json(json: &str) -> SolverForgeResult<Self> {
320 let value: Value = serde_json::from_str(json)
321 .map_err(|e| SolverForgeError::Serialization(e.to_string()))?;
322
323 match value {
324 Value::Object(map) => {
325 let rooms = match map.get("rooms") {
326 Some(Value::Array(arr)) => arr
327 .iter()
328 .map(|v| match v {
329 Value::Object(m) => {
330 let id = m
331 .get("id")
332 .and_then(|v| v.as_str())
333 .unwrap_or("")
334 .to_string();
335 let name = m
336 .get("name")
337 .and_then(|v| v.as_str())
338 .unwrap_or("")
339 .to_string();
340 Room { id, name }
341 }
342 _ => Room {
343 id: String::new(),
344 name: String::new(),
345 },
346 })
347 .collect(),
348 _ => Vec::new(),
349 };
350
351 let lessons = match map.get("lessons") {
352 Some(Value::Array(arr)) => arr
353 .iter()
354 .filter_map(|v| Lesson::from_value(v).ok())
355 .collect(),
356 _ => Vec::new(),
357 };
358
359 Ok(Timetable {
360 rooms,
361 lessons,
362 score: None,
363 })
364 }
365 _ => Err(SolverForgeError::Serialization(
366 "Expected object".to_string(),
367 )),
368 }
369 }
370 }
371
372 #[test]
373 fn test_planning_entity_domain_class() {
374 let class = Lesson::domain_class();
375 assert_eq!(class.name, "Lesson");
376 assert!(class.is_planning_entity());
377 assert!(class.get_planning_id_field().is_some());
378 assert_eq!(class.get_planning_variables().count(), 1);
379 }
380
381 #[test]
382 fn test_planning_entity_planning_id() {
383 let lesson = Lesson {
384 id: "L1".to_string(),
385 subject: "Math".to_string(),
386 room: Some("R1".to_string()),
387 };
388 assert_eq!(lesson.planning_id(), Value::String("L1".to_string()));
389 }
390
391 #[test]
392 fn test_planning_entity_to_value() {
393 let lesson = Lesson {
394 id: "L1".to_string(),
395 subject: "Math".to_string(),
396 room: Some("R1".to_string()),
397 };
398 let value = lesson.to_value();
399
400 match value {
401 Value::Object(map) => {
402 assert_eq!(map.get("id"), Some(&Value::String("L1".to_string())));
403 assert_eq!(map.get("subject"), Some(&Value::String("Math".to_string())));
404 assert_eq!(map.get("room"), Some(&Value::String("R1".to_string())));
405 }
406 _ => panic!("Expected object"),
407 }
408 }
409
410 #[test]
411 fn test_planning_entity_from_value() {
412 let mut map = HashMap::new();
413 map.insert("id".to_string(), Value::String("L1".to_string()));
414 map.insert("subject".to_string(), Value::String("Math".to_string()));
415 map.insert("room".to_string(), Value::String("R1".to_string()));
416
417 let lesson = Lesson::from_value(&Value::Object(map)).unwrap();
418 assert_eq!(lesson.id, "L1");
419 assert_eq!(lesson.subject, "Math");
420 assert_eq!(lesson.room, Some("R1".to_string()));
421 }
422
423 #[test]
424 fn test_planning_entity_roundtrip() {
425 let original = Lesson {
426 id: "L1".to_string(),
427 subject: "Math".to_string(),
428 room: Some("R1".to_string()),
429 };
430
431 let value = original.to_value();
432 let restored = Lesson::from_value(&value).unwrap();
433 assert_eq!(original, restored);
434 }
435
436 #[test]
437 fn test_planning_solution_domain_model() {
438 let model = Timetable::domain_model();
439
440 assert!(model.get_solution_class().is_some());
441 assert_eq!(model.get_solution_class().unwrap().name, "Timetable");
442 assert!(model.get_class("Lesson").is_some());
443 assert!(model.get_class("Room").is_some());
444 }
445
446 #[test]
447 fn test_planning_solution_constraints() {
448 let constraints = Timetable::constraints();
449 assert!(!constraints.is_empty());
450 }
451
452 #[test]
453 fn test_planning_solution_score() {
454 let mut timetable = Timetable {
455 rooms: vec![],
456 lessons: vec![],
457 score: None,
458 };
459
460 assert!(timetable.score().is_none());
461
462 timetable.set_score(HardSoftScore::of(-1, -5));
463 assert_eq!(timetable.score(), Some(HardSoftScore::of(-1, -5)));
464 }
465
466 #[test]
467 fn test_planning_solution_json_roundtrip() {
468 let timetable = Timetable {
469 rooms: vec![Room {
470 id: "R1".to_string(),
471 name: "Room 1".to_string(),
472 }],
473 lessons: vec![Lesson {
474 id: "L1".to_string(),
475 subject: "Math".to_string(),
476 room: Some("R1".to_string()),
477 }],
478 score: None,
479 };
480
481 let json = timetable.to_json().unwrap();
482 let restored = Timetable::from_json(&json).unwrap();
483
484 assert_eq!(restored.rooms.len(), 1);
485 assert_eq!(restored.lessons.len(), 1);
486 assert_eq!(restored.lessons[0].id, "L1");
487 }
488}