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