1use crate::constraints::StreamComponent;
2use crate::domain::PlanningAnnotation;
3use crate::solver::TerminationConfig;
4use indexmap::IndexMap;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct SolveRequest {
12 pub domain: IndexMap<String, DomainObjectDto>,
13 pub constraints: IndexMap<String, Vec<StreamComponent>>,
14 pub wasm: String,
15 pub allocator: String,
16 pub deallocator: String,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub solution_deallocator: Option<String>,
19 pub list_accessor: ListAccessorDto,
20 pub problem: String,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub environment_mode: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub termination: Option<TerminationConfig>,
25 #[serde(skip_serializing_if = "Option::is_none")]
29 pub precomputed: Option<IndexMap<i32, IndexMap<String, i32>>>,
30}
31
32impl SolveRequest {
33 pub fn new(
34 domain: IndexMap<String, DomainObjectDto>,
35 constraints: IndexMap<String, Vec<StreamComponent>>,
36 wasm: String,
37 allocator: String,
38 deallocator: String,
39 list_accessor: ListAccessorDto,
40 problem: String,
41 ) -> Self {
42 Self {
43 domain,
44 constraints,
45 wasm,
46 allocator,
47 deallocator,
48 solution_deallocator: None,
49 list_accessor,
50 problem,
51 environment_mode: None,
52 termination: None,
53 precomputed: None,
54 }
55 }
56
57 pub fn with_solution_deallocator(mut self, deallocator: impl Into<String>) -> Self {
58 self.solution_deallocator = Some(deallocator.into());
59 self
60 }
61
62 pub fn with_environment_mode(mut self, mode: impl Into<String>) -> Self {
63 self.environment_mode = Some(mode.into());
64 self
65 }
66
67 pub fn with_termination(mut self, termination: TerminationConfig) -> Self {
68 self.termination = Some(termination);
69 self
70 }
71
72 pub fn with_precomputed(mut self, precomputed: IndexMap<i32, IndexMap<String, i32>>) -> Self {
73 self.precomputed = Some(precomputed);
74 self
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct DomainObjectDto {
83 pub fields: IndexMap<String, FieldDescriptor>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub mapper: Option<DomainObjectMapper>,
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 pub annotations: Vec<ClassAnnotation>,
89}
90
91impl DomainObjectDto {
92 pub fn new() -> Self {
93 Self {
94 fields: IndexMap::new(),
95 mapper: None,
96 annotations: Vec::new(),
97 }
98 }
99
100 pub fn with_annotation(mut self, annotation: ClassAnnotation) -> Self {
101 self.annotations.push(annotation);
102 self
103 }
104
105 pub fn with_field(mut self, name: impl Into<String>, field: FieldDescriptor) -> Self {
106 self.fields.insert(name.into(), field);
107 self
108 }
109
110 pub fn with_mapper(mut self, mapper: DomainObjectMapper) -> Self {
111 self.mapper = Some(mapper);
112 self
113 }
114}
115
116impl Default for DomainObjectDto {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124pub struct FieldDescriptor {
125 #[serde(rename = "type")]
126 pub field_type: String,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub accessor: Option<DomainAccessor>,
129 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub annotations: Vec<PlanningAnnotation>,
131}
132
133impl FieldDescriptor {
134 pub fn new(field_type: impl Into<String>) -> Self {
135 Self {
136 field_type: field_type.into(),
137 accessor: None,
138 annotations: Vec::new(),
139 }
140 }
141
142 pub fn with_accessor(mut self, accessor: DomainAccessor) -> Self {
143 self.accessor = Some(accessor);
144 self
145 }
146
147 pub fn with_annotation(mut self, annotation: PlanningAnnotation) -> Self {
148 self.annotations.push(annotation);
149 self
150 }
151
152 pub fn with_annotations(mut self, annotations: Vec<PlanningAnnotation>) -> Self {
153 self.annotations = annotations;
154 self
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct DomainAccessor {
161 pub getter: String,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub setter: Option<String>,
164}
165
166impl DomainAccessor {
167 pub fn new(getter: impl Into<String>) -> Self {
168 Self {
169 getter: getter.into(),
170 setter: None,
171 }
172 }
173
174 pub fn with_setter(mut self, setter: impl Into<String>) -> Self {
175 self.setter = Some(setter.into());
176 self
177 }
178
179 pub fn getter_setter(getter: impl Into<String>, setter: impl Into<String>) -> Self {
180 Self {
181 getter: getter.into(),
182 setter: Some(setter.into()),
183 }
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct DomainObjectMapper {
190 #[serde(rename = "fromString")]
191 pub from_string: String,
192 #[serde(rename = "toString")]
193 pub to_string: String,
194}
195
196impl DomainObjectMapper {
197 pub fn new(from_string: impl Into<String>, to_string: impl Into<String>) -> Self {
198 Self {
199 from_string: from_string.into(),
200 to_string: to_string.into(),
201 }
202 }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207#[serde(tag = "annotation")]
208pub enum ClassAnnotation {
209 PlanningEntity,
210 PlanningSolution,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216pub struct ListAccessorDto {
217 #[serde(rename = "new")]
218 pub create: String,
219 #[serde(rename = "get")]
220 pub get_item: String,
221 #[serde(rename = "set")]
222 pub set_item: String,
223 #[serde(rename = "length")]
224 pub get_size: String,
225 pub append: String,
226 pub insert: String,
227 pub remove: String,
228 pub deallocator: String,
229}
230
231impl ListAccessorDto {
232 #[allow(clippy::too_many_arguments)]
233 pub fn new(
234 create: impl Into<String>,
235 get_item: impl Into<String>,
236 set_item: impl Into<String>,
237 get_size: impl Into<String>,
238 append: impl Into<String>,
239 insert: impl Into<String>,
240 remove: impl Into<String>,
241 deallocator: impl Into<String>,
242 ) -> Self {
243 Self {
244 create: create.into(),
245 get_item: get_item.into(),
246 set_item: set_item.into(),
247 get_size: get_size.into(),
248 append: append.into(),
249 insert: insert.into(),
250 remove: remove.into(),
251 deallocator: deallocator.into(),
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_solve_request_new() {
262 let request = SolveRequest::new(
263 IndexMap::new(),
264 IndexMap::new(),
265 "AGFzbQ==".to_string(),
266 "allocate".to_string(),
267 "deallocate".to_string(),
268 ListAccessorDto::new(
269 "newList", "getItem", "setItem", "size", "append", "insert", "remove", "dealloc",
270 ),
271 "{}".to_string(),
272 );
273
274 assert_eq!(request.wasm, "AGFzbQ==");
275 assert_eq!(request.allocator, "allocate");
276 assert!(request.environment_mode.is_none());
277 }
278
279 #[test]
280 fn test_solve_request_with_options() {
281 let termination = TerminationConfig::new().with_spent_limit("PT5M");
282 let request = SolveRequest::new(
283 IndexMap::new(),
284 IndexMap::new(),
285 "AGFzbQ==".to_string(),
286 "allocate".to_string(),
287 "deallocate".to_string(),
288 ListAccessorDto::new(
289 "newList", "getItem", "setItem", "size", "append", "insert", "remove", "dealloc",
290 ),
291 "{}".to_string(),
292 )
293 .with_environment_mode("FULL_ASSERT")
294 .with_termination(termination);
295
296 assert_eq!(request.environment_mode, Some("FULL_ASSERT".to_string()));
297 assert!(request.termination.is_some());
298 }
299
300 #[test]
301 fn test_domain_object_dto_with_fields() {
302 let dto = DomainObjectDto::new()
303 .with_field(
304 "id",
305 FieldDescriptor::new("int")
306 .with_accessor(DomainAccessor::new("getId"))
307 .with_annotation(PlanningAnnotation::planning_id()),
308 )
309 .with_field(
310 "employee",
311 FieldDescriptor::new("Employee")
312 .with_accessor(DomainAccessor::getter_setter("getEmployee", "setEmployee"))
313 .with_annotation(PlanningAnnotation::planning_variable(vec![])),
314 );
315
316 assert_eq!(dto.fields.len(), 2);
317 assert!(dto.fields.contains_key("id"));
318 assert!(dto.fields.contains_key("employee"));
319 }
320
321 #[test]
322 fn test_domain_object_dto_with_mapper() {
323 let dto = DomainObjectDto::new()
324 .with_mapper(DomainObjectMapper::new("parseSchedule", "scheduleString"));
325
326 assert!(dto.mapper.is_some());
327 let mapper = dto.mapper.unwrap();
328 assert_eq!(mapper.from_string, "parseSchedule");
329 assert_eq!(mapper.to_string, "scheduleString");
330 }
331
332 #[test]
333 fn test_field_descriptor() {
334 let field = FieldDescriptor::new("Employee")
335 .with_accessor(DomainAccessor::getter_setter("getEmployee", "setEmployee"))
336 .with_annotation(PlanningAnnotation::PlanningVariable {
337 value_range_provider_refs: vec![],
338 allows_unassigned: false,
339 });
340
341 assert_eq!(field.field_type, "Employee");
342 assert!(field.accessor.is_some());
343 assert_eq!(field.annotations.len(), 1);
344 }
345
346 #[test]
347 fn test_domain_accessor() {
348 let accessor = DomainAccessor::new("getRoom").with_setter("setRoom");
349 assert_eq!(accessor.getter, "getRoom");
350 assert_eq!(accessor.setter, Some("setRoom".to_string()));
351
352 let accessor2 = DomainAccessor::getter_setter("getRoom", "setRoom");
353 assert_eq!(accessor2.getter, "getRoom");
354 assert_eq!(accessor2.setter, Some("setRoom".to_string()));
355 }
356
357 #[test]
358 fn test_planning_annotation_constructors() {
359 assert!(matches!(
360 PlanningAnnotation::planning_variable(vec![]),
361 PlanningAnnotation::PlanningVariable {
362 allows_unassigned: false,
363 ..
364 }
365 ));
366 assert!(matches!(
367 PlanningAnnotation::planning_variable_unassigned(vec![]),
368 PlanningAnnotation::PlanningVariable {
369 allows_unassigned: true,
370 ..
371 }
372 ));
373 assert!(matches!(
374 PlanningAnnotation::planning_id(),
375 PlanningAnnotation::PlanningId
376 ));
377 assert!(matches!(
378 PlanningAnnotation::planning_score(),
379 PlanningAnnotation::PlanningScore { .. }
380 ));
381 assert!(matches!(
382 PlanningAnnotation::value_range_provider(),
383 PlanningAnnotation::ValueRangeProvider { .. }
384 ));
385 }
386
387 #[test]
388 fn test_list_accessor_dto() {
389 let accessor = ListAccessorDto::new(
390 "newList", "getItem", "setItem", "size", "append", "insert", "remove", "dealloc",
391 );
392
393 assert_eq!(accessor.create, "newList");
394 assert_eq!(accessor.get_item, "getItem");
395 assert_eq!(accessor.set_item, "setItem");
396 assert_eq!(accessor.get_size, "size");
397 assert_eq!(accessor.deallocator, "dealloc");
398 }
399
400 #[test]
401 fn test_solve_request_json_serialization() {
402 let mut domain = IndexMap::new();
403 domain.insert(
404 "Employee".to_string(),
405 DomainObjectDto::new().with_field(
406 "id",
407 FieldDescriptor::new("int")
408 .with_accessor(DomainAccessor::new("getEmployeeId"))
409 .with_annotation(PlanningAnnotation::planning_id()),
410 ),
411 );
412
413 let request = SolveRequest::new(
414 domain,
415 IndexMap::new(),
416 "AGFzbQ==".to_string(),
417 "allocate".to_string(),
418 "deallocate".to_string(),
419 ListAccessorDto::new(
420 "newList", "getItem", "setItem", "size", "append", "insert", "remove", "dealloc",
421 ),
422 r#"{"employees": []}"#.to_string(),
423 );
424
425 let json = serde_json::to_string(&request).unwrap();
426 assert!(json.contains("\"domain\""));
427 assert!(json.contains("\"listAccessor\""));
428 assert!(json.contains("\"type\":\"int\""));
429 assert!(json.contains("\"annotation\":\"PlanningId\""));
430
431 let parsed: SolveRequest = serde_json::from_str(&json).unwrap();
432 assert_eq!(parsed, request);
433 }
434
435 #[test]
436 fn test_solve_request_omits_none_fields() {
437 let request = SolveRequest::new(
438 IndexMap::new(),
439 IndexMap::new(),
440 "AGFzbQ==".to_string(),
441 "allocate".to_string(),
442 "deallocate".to_string(),
443 ListAccessorDto::new(
444 "newList", "getItem", "setItem", "size", "append", "insert", "remove", "dealloc",
445 ),
446 "{}".to_string(),
447 );
448
449 let json = serde_json::to_string(&request).unwrap();
450 assert!(!json.contains("environmentMode"));
451 assert!(!json.contains("termination"));
452 assert!(!json.contains("solutionDeallocator"));
453 }
454
455 #[test]
456 fn test_planning_annotation_json_serialization() {
457 let variable = PlanningAnnotation::PlanningVariable {
458 value_range_provider_refs: vec![],
459 allows_unassigned: false,
460 };
461 let json = serde_json::to_string(&variable).unwrap();
462 assert!(json.contains("\"annotation\":\"PlanningVariable\""));
463 assert!(!json.contains("allowsUnassigned"));
465
466 let variable_unassigned = PlanningAnnotation::PlanningVariable {
467 value_range_provider_refs: vec![],
468 allows_unassigned: true,
469 };
470 let json2 = serde_json::to_string(&variable_unassigned).unwrap();
471 assert!(json2.contains("\"allowsUnassigned\":true"));
472
473 let planning_id = PlanningAnnotation::PlanningId;
474 let json3 = serde_json::to_string(&planning_id).unwrap();
475 assert!(json3.contains("\"annotation\":\"PlanningId\""));
476 }
477
478 #[test]
479 fn test_list_accessor_json_field_names() {
480 let accessor = ListAccessorDto::new(
481 "newList", "getItem", "setItem", "size", "append", "insert", "remove", "dealloc",
482 );
483
484 let json = serde_json::to_string(&accessor).unwrap();
485 assert!(json.contains("\"new\":\"newList\""));
487 assert!(json.contains("\"get\":\"getItem\""));
488 assert!(json.contains("\"set\":\"setItem\""));
489 assert!(json.contains("\"length\":\"size\""));
490 assert!(json.contains("\"append\":\"append\""));
491 }
492
493 #[test]
494 fn test_domain_object_mapper_json_serialization() {
495 let mapper = DomainObjectMapper::new("parseSchedule", "scheduleString");
496 let json = serde_json::to_string(&mapper).unwrap();
497 assert!(json.contains("\"fromString\":\"parseSchedule\""));
498 assert!(json.contains("\"toString\":\"scheduleString\""));
499 }
500
501 #[test]
502 fn test_field_descriptor_json_serialization() {
503 let field = FieldDescriptor::new("Employee")
504 .with_accessor(DomainAccessor::getter_setter("getEmployee", "setEmployee"))
505 .with_annotation(PlanningAnnotation::planning_variable(vec![]));
506
507 let json = serde_json::to_string(&field).unwrap();
508 assert!(json.contains("\"type\":\"Employee\""));
509 assert!(json.contains("\"getter\":\"getEmployee\""));
510 assert!(json.contains("\"setter\":\"setEmployee\""));
511 assert!(json.contains("\"annotation\":\"PlanningVariable\""));
512
513 let parsed: FieldDescriptor = serde_json::from_str(&json).unwrap();
514 assert_eq!(parsed, field);
515 }
516
517 #[test]
518 fn test_full_domain_json_structure() {
519 let mut domain = IndexMap::new();
521
522 domain.insert(
524 "Employee".to_string(),
525 DomainObjectDto::new().with_field(
526 "id",
527 FieldDescriptor::new("int")
528 .with_accessor(DomainAccessor::new("getEmployeeId"))
529 .with_annotation(PlanningAnnotation::planning_id()),
530 ),
531 );
532
533 domain.insert(
535 "Shift".to_string(),
536 DomainObjectDto::new().with_field(
537 "employee",
538 FieldDescriptor::new("Employee")
539 .with_accessor(DomainAccessor::getter_setter("getEmployee", "setEmployee"))
540 .with_annotation(PlanningAnnotation::planning_variable(vec![])),
541 ),
542 );
543
544 domain.insert(
546 "Schedule".to_string(),
547 DomainObjectDto::new()
548 .with_field(
549 "employees",
550 FieldDescriptor::new("Employee[]")
551 .with_accessor(DomainAccessor::getter_setter(
552 "getEmployees",
553 "setEmployees",
554 ))
555 .with_annotation(PlanningAnnotation::problem_fact_collection_property())
556 .with_annotation(PlanningAnnotation::value_range_provider()),
557 )
558 .with_field(
559 "shifts",
560 FieldDescriptor::new("Shift[]")
561 .with_accessor(DomainAccessor::getter_setter("getShifts", "setShifts"))
562 .with_annotation(PlanningAnnotation::planning_entity_collection_property()),
563 )
564 .with_field(
565 "score",
566 FieldDescriptor::new("SimpleScore")
567 .with_annotation(PlanningAnnotation::planning_score()),
568 )
569 .with_mapper(DomainObjectMapper::new("parseSchedule", "scheduleString")),
570 );
571
572 let json = serde_json::to_string_pretty(&domain).unwrap();
573
574 assert!(json.contains("\"Employee\""));
576 assert!(json.contains("\"Shift\""));
577 assert!(json.contains("\"Schedule\""));
578 assert!(json.contains("\"type\": \"int\"") || json.contains("\"type\":\"int\""));
580 assert!(json.contains("\"Employee\""));
581 assert!(json.contains("\"Employee[]\""));
582 assert!(json.contains("\"SimpleScore\""));
583 assert!(
585 json.contains("\"annotation\": \"PlanningId\"")
586 || json.contains("\"annotation\":\"PlanningId\"")
587 );
588 assert!(
589 json.contains("\"annotation\": \"PlanningVariable\"")
590 || json.contains("\"annotation\":\"PlanningVariable\"")
591 );
592 assert!(
593 json.contains("\"annotation\": \"PlanningScore\"")
594 || json.contains("\"annotation\":\"PlanningScore\"")
595 );
596 assert!(
597 json.contains("\"annotation\": \"ValueRangeProvider\"")
598 || json.contains("\"annotation\":\"ValueRangeProvider\"")
599 );
600 assert!(
601 json.contains("\"fromString\": \"parseSchedule\"")
602 || json.contains("\"fromString\":\"parseSchedule\"")
603 );
604 }
605
606 #[test]
607 fn test_domain_object_dto_clone() {
608 let dto = DomainObjectDto::new().with_field(
609 "id",
610 FieldDescriptor::new("int").with_annotation(PlanningAnnotation::planning_id()),
611 );
612 let cloned = dto.clone();
613 assert_eq!(dto, cloned);
614 }
615
616 #[test]
617 fn test_domain_object_dto_debug() {
618 let dto = DomainObjectDto::new();
619 let debug = format!("{:?}", dto);
620 assert!(debug.contains("DomainObjectDto"));
621 }
622}