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