solverforge_core/
traits.rs

1//! Core traits for domain model types in SolverForge.
2//!
3//! These traits define the interface for planning entities and solutions
4//! that can be solved by the constraint solver. They are typically implemented
5//! via derive macros from the `solverforge-derive` crate.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use solverforge_derive::{PlanningEntity, PlanningSolution};
11//! use solverforge_core::{PlanningEntity, PlanningSolution, HardSoftScore};
12//!
13//! #[derive(PlanningEntity)]
14//! struct Lesson {
15//!     #[planning_id]
16//!     id: String,
17//!     #[planning_variable(value_range_provider = "rooms")]
18//!     room: Option<Room>,
19//! }
20//!
21//! #[derive(PlanningSolution)]
22//! #[constraint_provider = "define_constraints"]
23//! struct Timetable {
24//!     #[problem_fact_collection]
25//!     #[value_range_provider(id = "rooms")]
26//!     rooms: Vec<Room>,
27//!     #[planning_entity_collection]
28//!     lessons: Vec<Lesson>,
29//!     #[planning_score]
30//!     score: Option<HardSoftScore>,
31//! }
32//! ```
33
34use crate::constraints::ConstraintSet;
35use crate::domain::{DomainClass, DomainModel};
36use crate::score::Score;
37use crate::value::Value;
38use crate::SolverForgeResult;
39
40/// Marker trait for types that can be used as planning entities.
41///
42/// A planning entity is an object that can be changed during solving.
43/// It contains one or more planning variables that the solver assigns
44/// values to from their respective value ranges.
45///
46/// # Requirements
47///
48/// - Must have a unique planning ID field
49/// - Must have at least one planning variable
50/// - Must be serializable to/from `Value`
51///
52/// # Derive Macro
53///
54/// This trait is typically implemented via `#[derive(PlanningEntity)]`:
55///
56/// ```ignore
57/// #[derive(PlanningEntity)]
58/// struct Lesson {
59///     #[planning_id]
60///     id: String,
61///     subject: String,
62///     #[planning_variable(value_range_provider = "timeslots")]
63///     timeslot: Option<Timeslot>,
64/// }
65/// ```
66pub trait PlanningEntity: Send + Sync + Clone {
67    /// Returns the domain class descriptor for this entity type.
68    ///
69    /// The domain class contains metadata about the entity's fields,
70    /// annotations, and planning variables.
71    fn domain_class() -> DomainClass;
72
73    /// Returns the planning ID for this instance.
74    ///
75    /// The planning ID uniquely identifies this entity instance and is
76    /// used for tracking during solving and for entity matching.
77    fn planning_id(&self) -> Value;
78
79    /// Serializes this entity to a language-agnostic `Value`.
80    ///
81    /// The resulting `Value::Object` contains all fields of the entity.
82    fn to_value(&self) -> Value;
83
84    /// Deserializes an entity from a `Value`.
85    ///
86    /// Returns an error if the value cannot be converted to this entity type.
87    fn from_value(value: &Value) -> SolverForgeResult<Self>
88    where
89        Self: Sized;
90}
91
92/// Marker trait for types that can be used as planning solutions.
93///
94/// A planning solution contains:
95/// - Problem facts: immutable data that constraints can reference
96/// - Planning entities: objects that can be changed during solving
97/// - A score: represents the quality of the solution
98///
99/// # Requirements
100///
101/// - Must have at least one planning entity collection
102/// - Must have a score field
103/// - Must provide a constraint provider function
104///
105/// # Derive Macro
106///
107/// This trait is typically implemented via `#[derive(PlanningSolution)]`:
108///
109/// ```ignore
110/// #[derive(PlanningSolution)]
111/// #[constraint_provider = "define_constraints"]
112/// struct Timetable {
113///     #[problem_fact_collection]
114///     #[value_range_provider(id = "timeslots")]
115///     timeslots: Vec<Timeslot>,
116///     #[planning_entity_collection]
117///     lessons: Vec<Lesson>,
118///     #[planning_score]
119///     score: Option<HardSoftScore>,
120/// }
121/// ```
122pub trait PlanningSolution: Send + Sync + Clone {
123    /// The score type used by this solution.
124    type Score: Score;
125
126    /// Returns the complete domain model for this solution.
127    ///
128    /// The domain model includes all entity classes, problem fact classes,
129    /// and the solution class itself with all their fields and annotations.
130    fn domain_model() -> DomainModel;
131
132    /// Returns the constraint set for this solution.
133    ///
134    /// The constraint set contains all constraints that will be evaluated
135    /// during solving to calculate the solution's score.
136    fn constraints() -> ConstraintSet;
137
138    /// Returns the current score of this solution, if set.
139    fn score(&self) -> Option<Self::Score>;
140
141    /// Sets the score of this solution.
142    fn set_score(&mut self, score: Self::Score);
143
144    /// Serializes this solution to JSON.
145    fn to_json(&self) -> SolverForgeResult<String>;
146
147    /// Deserializes a solution from JSON.
148    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    // Test entity: Room (problem fact, no planning variables)
162    #[derive(Clone, Debug, PartialEq)]
163    struct Room {
164        id: String,
165        name: String,
166    }
167
168    // Test entity: Lesson (planning entity with planning variable)
169    #[derive(Clone, Debug, PartialEq)]
170    struct Lesson {
171        id: String,
172        subject: String,
173        room: Option<String>, // References Room.id
174    }
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    // Test solution: Timetable
237    #[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            // Simple constraint set for testing
288            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            // Simplified JSON serialization for testing
301            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}