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