solverforge_core/domain/
class.rs

1use super::PlanningAnnotation;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5pub struct DomainClass {
6    pub name: String,
7    #[serde(default)]
8    pub annotations: Vec<PlanningAnnotation>,
9    #[serde(default)]
10    pub fields: Vec<FieldDescriptor>,
11}
12
13impl DomainClass {
14    pub fn new(name: impl Into<String>) -> Self {
15        Self {
16            name: name.into(),
17            annotations: Vec::new(),
18            fields: Vec::new(),
19        }
20    }
21
22    pub fn with_annotation(mut self, annotation: PlanningAnnotation) -> Self {
23        self.annotations.push(annotation);
24        self
25    }
26
27    pub fn with_field(mut self, field: FieldDescriptor) -> Self {
28        self.fields.push(field);
29        self
30    }
31
32    pub fn is_planning_entity(&self) -> bool {
33        self.annotations
34            .iter()
35            .any(|a| matches!(a, PlanningAnnotation::PlanningEntity))
36    }
37
38    pub fn is_planning_solution(&self) -> bool {
39        self.annotations
40            .iter()
41            .any(|a| matches!(a, PlanningAnnotation::PlanningSolution))
42    }
43
44    pub fn get_planning_variables(&self) -> impl Iterator<Item = &FieldDescriptor> {
45        self.fields.iter().filter(|f| f.is_planning_variable())
46    }
47
48    pub fn get_planning_id_field(&self) -> Option<&FieldDescriptor> {
49        self.fields
50            .iter()
51            .find(|f| f.has_annotation(|a| matches!(a, PlanningAnnotation::PlanningId)))
52    }
53
54    pub fn get_score_field(&self) -> Option<&FieldDescriptor> {
55        self.fields
56            .iter()
57            .find(|f| f.has_annotation(|a| matches!(a, PlanningAnnotation::PlanningScore { .. })))
58    }
59}
60
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62pub struct FieldDescriptor {
63    pub name: String,
64    #[serde(rename = "type")]
65    pub field_type: FieldType,
66    #[serde(default)]
67    pub annotations: Vec<PlanningAnnotation>,
68    #[serde(default)]
69    pub accessor: Option<DomainAccessor>,
70}
71
72impl FieldDescriptor {
73    pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
74        Self {
75            name: name.into(),
76            field_type,
77            annotations: Vec::new(),
78            accessor: None,
79        }
80    }
81
82    pub fn with_annotation(mut self, annotation: PlanningAnnotation) -> Self {
83        self.annotations.push(annotation);
84        self
85    }
86
87    pub fn with_accessor(mut self, accessor: DomainAccessor) -> Self {
88        self.accessor = Some(accessor);
89        self
90    }
91
92    pub fn is_planning_variable(&self) -> bool {
93        self.annotations.iter().any(|a| a.is_any_variable())
94    }
95
96    pub fn is_shadow_variable(&self) -> bool {
97        self.annotations.iter().any(|a| a.is_shadow_variable())
98    }
99
100    pub fn has_annotation<F>(&self, predicate: F) -> bool
101    where
102        F: Fn(&PlanningAnnotation) -> bool,
103    {
104        self.annotations.iter().any(predicate)
105    }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct DomainAccessor {
110    pub getter: String,
111    pub setter: String,
112}
113
114impl DomainAccessor {
115    pub fn new(getter: impl Into<String>, setter: impl Into<String>) -> Self {
116        Self {
117            getter: getter.into(),
118            setter: setter.into(),
119        }
120    }
121
122    pub fn from_field_name(field_name: &str) -> Self {
123        let capitalized = capitalize_first(field_name);
124        Self {
125            getter: format!("get{}", capitalized),
126            setter: format!("set{}", capitalized),
127        }
128    }
129}
130
131fn capitalize_first(s: &str) -> String {
132    let mut chars = s.chars();
133    match chars.next() {
134        None => String::new(),
135        Some(first) => first.to_uppercase().chain(chars).collect(),
136    }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(tag = "kind")]
141pub enum FieldType {
142    Primitive(PrimitiveType),
143    Object {
144        class_name: String,
145    },
146    Array {
147        element_type: Box<FieldType>,
148    },
149    List {
150        element_type: Box<FieldType>,
151    },
152    Set {
153        element_type: Box<FieldType>,
154    },
155    Map {
156        key_type: Box<FieldType>,
157        value_type: Box<FieldType>,
158    },
159    Score(ScoreType),
160}
161
162impl FieldType {
163    pub fn object(class_name: impl Into<String>) -> Self {
164        FieldType::Object {
165            class_name: class_name.into(),
166        }
167    }
168
169    pub fn array(element_type: FieldType) -> Self {
170        FieldType::Array {
171            element_type: Box::new(element_type),
172        }
173    }
174
175    pub fn list(element_type: FieldType) -> Self {
176        FieldType::List {
177            element_type: Box::new(element_type),
178        }
179    }
180
181    pub fn set(element_type: FieldType) -> Self {
182        FieldType::Set {
183            element_type: Box::new(element_type),
184        }
185    }
186
187    pub fn map(key_type: FieldType, value_type: FieldType) -> Self {
188        FieldType::Map {
189            key_type: Box::new(key_type),
190            value_type: Box::new(value_type),
191        }
192    }
193
194    pub fn is_collection(&self) -> bool {
195        matches!(
196            self,
197            FieldType::Array { .. } | FieldType::List { .. } | FieldType::Set { .. }
198        )
199    }
200
201    /// Returns a Java-compatible type string for the field type
202    pub fn to_type_string(&self) -> String {
203        match self {
204            FieldType::Primitive(p) => match p {
205                PrimitiveType::Bool => "boolean".to_string(),
206                PrimitiveType::Int => "int".to_string(),
207                PrimitiveType::Long => "long".to_string(),
208                PrimitiveType::Float => "float".to_string(),
209                PrimitiveType::Double => "double".to_string(),
210                PrimitiveType::String => "String".to_string(),
211                PrimitiveType::Date => "LocalDate".to_string(),
212                PrimitiveType::DateTime => "LocalDateTime".to_string(),
213            },
214            FieldType::Object { class_name } => class_name.clone(),
215            FieldType::Array { element_type } => format!("{}[]", element_type.to_type_string()),
216            FieldType::List { element_type } => format!("{}[]", element_type.to_type_string()),
217            FieldType::Set { element_type } => format!("{}[]", element_type.to_type_string()),
218            FieldType::Map {
219                key_type,
220                value_type,
221            } => {
222                format!(
223                    "Map<{}, {}>",
224                    key_type.to_type_string(),
225                    value_type.to_type_string()
226                )
227            }
228            FieldType::Score(s) => match s {
229                ScoreType::Simple => "SimpleScore".to_string(),
230                ScoreType::HardSoft => "HardSoftScore".to_string(),
231                ScoreType::HardMediumSoft => "HardMediumSoftScore".to_string(),
232                ScoreType::SimpleDecimal => "SimpleBigDecimalScore".to_string(),
233                ScoreType::HardSoftDecimal => "HardSoftBigDecimalScore".to_string(),
234                ScoreType::HardMediumSoftDecimal => "HardMediumSoftBigDecimalScore".to_string(),
235                ScoreType::Bendable { .. } => "BendableScore".to_string(),
236                ScoreType::BendableDecimal { .. } => "BendableBigDecimalScore".to_string(),
237            },
238        }
239    }
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243pub enum PrimitiveType {
244    Bool,
245    Int,
246    Long,
247    Float,
248    Double,
249    String,
250    Date,     // LocalDate - stored as epoch day (i64)
251    DateTime, // LocalDateTime - stored as epoch second (i64)
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255pub enum ScoreType {
256    Simple,
257    HardSoft,
258    HardMediumSoft,
259    SimpleDecimal,
260    HardSoftDecimal,
261    HardMediumSoftDecimal,
262    Bendable {
263        hard_levels: usize,
264        soft_levels: usize,
265    },
266    BendableDecimal {
267        hard_levels: usize,
268        soft_levels: usize,
269    },
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_domain_class_new() {
278        let class = DomainClass::new("Lesson");
279        assert_eq!(class.name, "Lesson");
280        assert!(class.annotations.is_empty());
281        assert!(class.fields.is_empty());
282    }
283
284    #[test]
285    fn test_domain_class_builder() {
286        let class = DomainClass::new("Lesson")
287            .with_annotation(PlanningAnnotation::PlanningEntity)
288            .with_field(
289                FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
290                    .with_annotation(PlanningAnnotation::PlanningId),
291            )
292            .with_field(
293                FieldDescriptor::new("room", FieldType::object("Room")).with_annotation(
294                    PlanningAnnotation::planning_variable(vec!["rooms".to_string()]),
295                ),
296            );
297
298        assert!(class.is_planning_entity());
299        assert!(!class.is_planning_solution());
300        assert_eq!(class.get_planning_variables().count(), 1);
301        assert!(class.get_planning_id_field().is_some());
302    }
303
304    #[test]
305    fn test_field_descriptor() {
306        let field = FieldDescriptor::new("room", FieldType::object("Room"))
307            .with_annotation(PlanningAnnotation::planning_variable(vec![
308                "rooms".to_string()
309            ]))
310            .with_accessor(DomainAccessor::from_field_name("room"));
311
312        assert!(field.is_planning_variable());
313        assert!(!field.is_shadow_variable());
314        assert!(field.accessor.is_some());
315
316        let accessor = field.accessor.unwrap();
317        assert_eq!(accessor.getter, "getRoom");
318        assert_eq!(accessor.setter, "setRoom");
319    }
320
321    #[test]
322    fn test_shadow_field() {
323        let field = FieldDescriptor::new("vehicle", FieldType::object("Vehicle"))
324            .with_annotation(PlanningAnnotation::inverse_relation_shadow("visits"));
325
326        assert!(!field.is_planning_variable());
327        assert!(field.is_shadow_variable());
328    }
329
330    #[test]
331    fn test_field_type_object() {
332        let ft = FieldType::object("Room");
333        match ft {
334            FieldType::Object { class_name } => assert_eq!(class_name, "Room"),
335            _ => panic!("Expected Object"),
336        }
337    }
338
339    #[test]
340    fn test_field_type_collection() {
341        let list = FieldType::list(FieldType::object("Lesson"));
342        assert!(list.is_collection());
343
344        let obj = FieldType::object("Room");
345        assert!(!obj.is_collection());
346    }
347
348    #[test]
349    fn test_field_type_nested() {
350        let map = FieldType::map(
351            FieldType::Primitive(PrimitiveType::String),
352            FieldType::list(FieldType::object("Lesson")),
353        );
354
355        match map {
356            FieldType::Map {
357                key_type,
358                value_type,
359            } => {
360                assert!(matches!(
361                    *key_type,
362                    FieldType::Primitive(PrimitiveType::String)
363                ));
364                assert!(matches!(*value_type, FieldType::List { .. }));
365            }
366            _ => panic!("Expected Map"),
367        }
368    }
369
370    #[test]
371    fn test_score_type() {
372        let bendable = ScoreType::Bendable {
373            hard_levels: 2,
374            soft_levels: 3,
375        };
376        match bendable {
377            ScoreType::Bendable {
378                hard_levels,
379                soft_levels,
380            } => {
381                assert_eq!(hard_levels, 2);
382                assert_eq!(soft_levels, 3);
383            }
384            _ => panic!("Expected Bendable"),
385        }
386    }
387
388    #[test]
389    fn test_domain_accessor_from_field() {
390        let accessor = DomainAccessor::from_field_name("timeslot");
391        assert_eq!(accessor.getter, "getTimeslot");
392        assert_eq!(accessor.setter, "setTimeslot");
393    }
394
395    #[test]
396    fn test_solution_class() {
397        let solution = DomainClass::new("Timetable")
398            .with_annotation(PlanningAnnotation::PlanningSolution)
399            .with_field(
400                FieldDescriptor::new("score", FieldType::Score(ScoreType::HardSoft))
401                    .with_annotation(PlanningAnnotation::planning_score()),
402            );
403
404        assert!(solution.is_planning_solution());
405        assert!(solution.get_score_field().is_some());
406    }
407
408    #[test]
409    fn test_json_serialization() {
410        let class = DomainClass::new("Lesson")
411            .with_annotation(PlanningAnnotation::PlanningEntity)
412            .with_field(
413                FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
414                    .with_annotation(PlanningAnnotation::PlanningId),
415            );
416
417        let json = serde_json::to_string(&class).unwrap();
418        let parsed: DomainClass = serde_json::from_str(&json).unwrap();
419        assert_eq!(parsed.name, class.name);
420        assert_eq!(parsed.fields.len(), class.fields.len());
421    }
422}