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 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, DateTime, }
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}