1use serde::{Deserialize, Serialize};
37use std::fmt::Debug;
38
39pub trait ProblemChange: Send + Sync + Debug {
47 fn do_change(&self, solution: &mut serde_json::Value, director: &mut dyn ProblemChangeDirector);
54
55 fn to_dto(&self) -> ProblemChangeDto;
57}
58
59pub type ChangeConsumer = Box<dyn FnOnce(&mut serde_json::Value) + Send>;
61
62pub trait ProblemChangeDirector: Send {
67 fn add_entity(&mut self, entity_id: &str, entity: serde_json::Value, consumer: ChangeConsumer);
75
76 fn remove_entity(&mut self, entity_id: &str, consumer: ChangeConsumer);
85
86 fn change_variable(&mut self, entity_id: &str, variable_name: &str, consumer: ChangeConsumer);
94
95 fn add_problem_fact(
103 &mut self,
104 fact_id: &str,
105 fact: serde_json::Value,
106 consumer: ChangeConsumer,
107 );
108
109 fn remove_problem_fact(&mut self, fact_id: &str, consumer: ChangeConsumer);
116
117 fn change_problem_property(&mut self, object_id: &str, consumer: ChangeConsumer);
124
125 fn look_up_working_object_or_fail(
133 &self,
134 external_id: &str,
135 ) -> Result<serde_json::Value, ProblemChangeError>;
136
137 fn look_up_working_object(&self, external_id: &str) -> Option<serde_json::Value>;
145
146 fn update_shadow_variables(&mut self);
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub enum ProblemChangeError {
156 ObjectNotFound { id: String },
158 UnsupportedType { type_name: String },
160 ValidationError { message: String },
162 ApplicationError { message: String },
164}
165
166impl std::fmt::Display for ProblemChangeError {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 match self {
169 ProblemChangeError::ObjectNotFound { id } => {
170 write!(f, "Object not found: {}", id)
171 }
172 ProblemChangeError::UnsupportedType { type_name } => {
173 write!(f, "Unsupported type for lookup: {}", type_name)
174 }
175 ProblemChangeError::ValidationError { message } => {
176 write!(f, "Validation error: {}", message)
177 }
178 ProblemChangeError::ApplicationError { message } => {
179 write!(f, "Application error: {}", message)
180 }
181 }
182 }
183}
184
185impl std::error::Error for ProblemChangeError {}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(tag = "type")]
190pub enum ProblemChangeDto {
191 #[serde(rename = "addEntity")]
193 AddEntity {
194 entity_class: String,
196 entity_id: String,
198 entity: serde_json::Value,
200 collection_path: String,
202 },
203
204 #[serde(rename = "removeEntity")]
206 RemoveEntity {
207 entity_class: String,
209 entity_id: String,
211 collection_path: String,
213 },
214
215 #[serde(rename = "changeVariable")]
217 ChangeVariable {
218 entity_class: String,
220 entity_id: String,
222 variable_name: String,
224 new_value: serde_json::Value,
226 },
227
228 #[serde(rename = "addProblemFact")]
230 AddProblemFact {
231 fact_class: String,
233 fact_id: String,
235 fact: serde_json::Value,
237 collection_path: String,
239 },
240
241 #[serde(rename = "removeProblemFact")]
243 RemoveProblemFact {
244 fact_class: String,
246 fact_id: String,
248 collection_path: String,
250 },
251
252 #[serde(rename = "changeProblemProperty")]
254 ChangeProblemProperty {
255 object_class: String,
257 object_id: String,
259 property_name: String,
261 new_value: serde_json::Value,
263 },
264
265 #[serde(rename = "batch")]
267 Batch {
268 changes: Vec<ProblemChangeDto>,
270 },
271}
272
273impl ProblemChangeDto {
274 pub fn add_entity(
276 entity_class: impl Into<String>,
277 entity_id: impl Into<String>,
278 entity: serde_json::Value,
279 collection_path: impl Into<String>,
280 ) -> Self {
281 ProblemChangeDto::AddEntity {
282 entity_class: entity_class.into(),
283 entity_id: entity_id.into(),
284 entity,
285 collection_path: collection_path.into(),
286 }
287 }
288
289 pub fn remove_entity(
291 entity_class: impl Into<String>,
292 entity_id: impl Into<String>,
293 collection_path: impl Into<String>,
294 ) -> Self {
295 ProblemChangeDto::RemoveEntity {
296 entity_class: entity_class.into(),
297 entity_id: entity_id.into(),
298 collection_path: collection_path.into(),
299 }
300 }
301
302 pub fn change_variable(
304 entity_class: impl Into<String>,
305 entity_id: impl Into<String>,
306 variable_name: impl Into<String>,
307 new_value: serde_json::Value,
308 ) -> Self {
309 ProblemChangeDto::ChangeVariable {
310 entity_class: entity_class.into(),
311 entity_id: entity_id.into(),
312 variable_name: variable_name.into(),
313 new_value,
314 }
315 }
316
317 pub fn add_problem_fact(
319 fact_class: impl Into<String>,
320 fact_id: impl Into<String>,
321 fact: serde_json::Value,
322 collection_path: impl Into<String>,
323 ) -> Self {
324 ProblemChangeDto::AddProblemFact {
325 fact_class: fact_class.into(),
326 fact_id: fact_id.into(),
327 fact,
328 collection_path: collection_path.into(),
329 }
330 }
331
332 pub fn remove_problem_fact(
334 fact_class: impl Into<String>,
335 fact_id: impl Into<String>,
336 collection_path: impl Into<String>,
337 ) -> Self {
338 ProblemChangeDto::RemoveProblemFact {
339 fact_class: fact_class.into(),
340 fact_id: fact_id.into(),
341 collection_path: collection_path.into(),
342 }
343 }
344
345 pub fn change_problem_property(
347 object_class: impl Into<String>,
348 object_id: impl Into<String>,
349 property_name: impl Into<String>,
350 new_value: serde_json::Value,
351 ) -> Self {
352 ProblemChangeDto::ChangeProblemProperty {
353 object_class: object_class.into(),
354 object_id: object_id.into(),
355 property_name: property_name.into(),
356 new_value,
357 }
358 }
359
360 pub fn batch(changes: Vec<ProblemChangeDto>) -> Self {
362 ProblemChangeDto::Batch { changes }
363 }
364}
365
366#[derive(Debug)]
371pub struct DefaultProblemChangeDirector {
372 object_index: std::collections::HashMap<String, serde_json::Value>,
374 changes: Vec<ChangeRecord>,
376 shadow_variables_dirty: bool,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq)]
382pub enum ChangeRecord {
383 EntityAdded {
385 id: String,
387 },
388 EntityRemoved {
390 id: String,
392 },
393 VariableChanged {
395 entity_id: String,
397 variable: String,
399 },
400 FactAdded {
402 id: String,
404 },
405 FactRemoved {
407 id: String,
409 },
410 PropertyChanged {
412 object_id: String,
414 },
415}
416
417impl DefaultProblemChangeDirector {
418 pub fn new(solution: &serde_json::Value, id_field: &str) -> Self {
425 let mut object_index = std::collections::HashMap::new();
426 Self::index_objects(solution, id_field, &mut object_index);
427
428 Self {
429 object_index,
430 changes: Vec::new(),
431 shadow_variables_dirty: false,
432 }
433 }
434
435 fn index_objects(
437 value: &serde_json::Value,
438 id_field: &str,
439 index: &mut std::collections::HashMap<String, serde_json::Value>,
440 ) {
441 match value {
442 serde_json::Value::Object(map) => {
443 if let Some(serde_json::Value::String(id)) = map.get(id_field) {
444 index.insert(id.clone(), value.clone());
445 }
446 for v in map.values() {
447 Self::index_objects(v, id_field, index);
448 }
449 }
450 serde_json::Value::Array(arr) => {
451 for v in arr {
452 Self::index_objects(v, id_field, index);
453 }
454 }
455 _ => {}
456 }
457 }
458
459 pub fn changes(&self) -> &[ChangeRecord] {
461 &self.changes
462 }
463
464 pub fn has_changes(&self) -> bool {
466 !self.changes.is_empty()
467 }
468
469 pub fn update_index(&mut self, id: String, value: serde_json::Value) {
471 self.object_index.insert(id, value);
472 }
473
474 pub fn remove_from_index(&mut self, id: &str) {
476 self.object_index.remove(id);
477 }
478}
479
480impl ProblemChangeDirector for DefaultProblemChangeDirector {
481 fn add_entity(&mut self, entity_id: &str, entity: serde_json::Value, consumer: ChangeConsumer) {
482 self.object_index
483 .insert(entity_id.to_string(), entity.clone());
484 self.changes.push(ChangeRecord::EntityAdded {
485 id: entity_id.to_string(),
486 });
487 self.shadow_variables_dirty = true;
488
489 let mut entity_copy = entity;
492 consumer(&mut entity_copy);
493 }
494
495 fn remove_entity(&mut self, entity_id: &str, consumer: ChangeConsumer) {
496 if let Some(mut entity) = self.object_index.remove(entity_id) {
497 consumer(&mut entity);
498 self.changes.push(ChangeRecord::EntityRemoved {
499 id: entity_id.to_string(),
500 });
501 self.shadow_variables_dirty = true;
502 }
503 }
504
505 fn change_variable(&mut self, entity_id: &str, variable_name: &str, consumer: ChangeConsumer) {
506 if let Some(entity) = self.object_index.get_mut(entity_id) {
507 consumer(entity);
508 self.changes.push(ChangeRecord::VariableChanged {
509 entity_id: entity_id.to_string(),
510 variable: variable_name.to_string(),
511 });
512 self.shadow_variables_dirty = true;
513 }
514 }
515
516 fn add_problem_fact(
517 &mut self,
518 fact_id: &str,
519 fact: serde_json::Value,
520 consumer: ChangeConsumer,
521 ) {
522 self.object_index.insert(fact_id.to_string(), fact.clone());
523 self.changes.push(ChangeRecord::FactAdded {
524 id: fact_id.to_string(),
525 });
526
527 let mut fact_copy = fact;
528 consumer(&mut fact_copy);
529 }
530
531 fn remove_problem_fact(&mut self, fact_id: &str, consumer: ChangeConsumer) {
532 if let Some(mut fact) = self.object_index.remove(fact_id) {
533 consumer(&mut fact);
534 self.changes.push(ChangeRecord::FactRemoved {
535 id: fact_id.to_string(),
536 });
537 }
538 }
539
540 fn change_problem_property(&mut self, object_id: &str, consumer: ChangeConsumer) {
541 if let Some(object) = self.object_index.get_mut(object_id) {
542 consumer(object);
543 self.changes.push(ChangeRecord::PropertyChanged {
544 object_id: object_id.to_string(),
545 });
546 }
547 }
548
549 fn look_up_working_object_or_fail(
550 &self,
551 external_id: &str,
552 ) -> Result<serde_json::Value, ProblemChangeError> {
553 self.object_index.get(external_id).cloned().ok_or_else(|| {
554 ProblemChangeError::ObjectNotFound {
555 id: external_id.to_string(),
556 }
557 })
558 }
559
560 fn look_up_working_object(&self, external_id: &str) -> Option<serde_json::Value> {
561 self.object_index.get(external_id).cloned()
562 }
563
564 fn update_shadow_variables(&mut self) {
565 self.shadow_variables_dirty = false;
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574 use serde_json::json;
575
576 #[test]
578 fn test_error_display_object_not_found() {
579 let err = ProblemChangeError::ObjectNotFound {
580 id: "entity1".to_string(),
581 };
582 assert_eq!(format!("{}", err), "Object not found: entity1");
583 }
584
585 #[test]
586 fn test_error_display_unsupported_type() {
587 let err = ProblemChangeError::UnsupportedType {
588 type_name: "UnknownType".to_string(),
589 };
590 assert_eq!(
591 format!("{}", err),
592 "Unsupported type for lookup: UnknownType"
593 );
594 }
595
596 #[test]
597 fn test_error_display_validation() {
598 let err = ProblemChangeError::ValidationError {
599 message: "ID cannot be empty".to_string(),
600 };
601 assert_eq!(format!("{}", err), "Validation error: ID cannot be empty");
602 }
603
604 #[test]
605 fn test_error_display_application() {
606 let err = ProblemChangeError::ApplicationError {
607 message: "Failed to apply change".to_string(),
608 };
609 assert_eq!(
610 format!("{}", err),
611 "Application error: Failed to apply change"
612 );
613 }
614
615 #[test]
617 fn test_add_entity_dto() {
618 let dto = ProblemChangeDto::add_entity(
619 "Lesson",
620 "lesson1",
621 json!({"id": "lesson1", "subject": "Math"}),
622 "lessons",
623 );
624
625 match dto {
626 ProblemChangeDto::AddEntity {
627 entity_class,
628 entity_id,
629 entity,
630 collection_path,
631 } => {
632 assert_eq!(entity_class, "Lesson");
633 assert_eq!(entity_id, "lesson1");
634 assert_eq!(entity["subject"], "Math");
635 assert_eq!(collection_path, "lessons");
636 }
637 _ => panic!("Expected AddEntity"),
638 }
639 }
640
641 #[test]
642 fn test_remove_entity_dto() {
643 let dto = ProblemChangeDto::remove_entity("Lesson", "lesson1", "lessons");
644
645 match dto {
646 ProblemChangeDto::RemoveEntity {
647 entity_class,
648 entity_id,
649 collection_path,
650 } => {
651 assert_eq!(entity_class, "Lesson");
652 assert_eq!(entity_id, "lesson1");
653 assert_eq!(collection_path, "lessons");
654 }
655 _ => panic!("Expected RemoveEntity"),
656 }
657 }
658
659 #[test]
660 fn test_change_variable_dto() {
661 let dto = ProblemChangeDto::change_variable("Lesson", "lesson1", "room", json!("Room101"));
662
663 match dto {
664 ProblemChangeDto::ChangeVariable {
665 entity_class,
666 entity_id,
667 variable_name,
668 new_value,
669 } => {
670 assert_eq!(entity_class, "Lesson");
671 assert_eq!(entity_id, "lesson1");
672 assert_eq!(variable_name, "room");
673 assert_eq!(new_value, json!("Room101"));
674 }
675 _ => panic!("Expected ChangeVariable"),
676 }
677 }
678
679 #[test]
680 fn test_change_variable_dto_null() {
681 let dto =
682 ProblemChangeDto::change_variable("Lesson", "lesson1", "room", serde_json::Value::Null);
683
684 match dto {
685 ProblemChangeDto::ChangeVariable { new_value, .. } => {
686 assert!(new_value.is_null());
687 }
688 _ => panic!("Expected ChangeVariable"),
689 }
690 }
691
692 #[test]
693 fn test_add_problem_fact_dto() {
694 let dto = ProblemChangeDto::add_problem_fact(
695 "Room",
696 "room1",
697 json!({"id": "room1", "capacity": 30}),
698 "rooms",
699 );
700
701 match dto {
702 ProblemChangeDto::AddProblemFact {
703 fact_class,
704 fact_id,
705 fact,
706 collection_path,
707 } => {
708 assert_eq!(fact_class, "Room");
709 assert_eq!(fact_id, "room1");
710 assert_eq!(fact["capacity"], 30);
711 assert_eq!(collection_path, "rooms");
712 }
713 _ => panic!("Expected AddProblemFact"),
714 }
715 }
716
717 #[test]
718 fn test_remove_problem_fact_dto() {
719 let dto = ProblemChangeDto::remove_problem_fact("Room", "room1", "rooms");
720
721 match dto {
722 ProblemChangeDto::RemoveProblemFact {
723 fact_class,
724 fact_id,
725 collection_path,
726 } => {
727 assert_eq!(fact_class, "Room");
728 assert_eq!(fact_id, "room1");
729 assert_eq!(collection_path, "rooms");
730 }
731 _ => panic!("Expected RemoveProblemFact"),
732 }
733 }
734
735 #[test]
736 fn test_change_problem_property_dto() {
737 let dto = ProblemChangeDto::change_problem_property("Room", "room1", "capacity", json!(50));
738
739 match dto {
740 ProblemChangeDto::ChangeProblemProperty {
741 object_class,
742 object_id,
743 property_name,
744 new_value,
745 } => {
746 assert_eq!(object_class, "Room");
747 assert_eq!(object_id, "room1");
748 assert_eq!(property_name, "capacity");
749 assert_eq!(new_value, json!(50));
750 }
751 _ => panic!("Expected ChangeProblemProperty"),
752 }
753 }
754
755 #[test]
756 fn test_batch_dto() {
757 let changes = vec![
758 ProblemChangeDto::add_entity("Lesson", "lesson1", json!({"id": "lesson1"}), "lessons"),
759 ProblemChangeDto::change_variable("Lesson", "lesson1", "room", json!("Room101")),
760 ];
761
762 let dto = ProblemChangeDto::batch(changes);
763
764 match dto {
765 ProblemChangeDto::Batch { changes } => {
766 assert_eq!(changes.len(), 2);
767 }
768 _ => panic!("Expected Batch"),
769 }
770 }
771
772 #[test]
774 fn test_add_entity_dto_serialization() {
775 let dto = ProblemChangeDto::add_entity(
776 "Lesson",
777 "lesson1",
778 json!({"id": "lesson1", "subject": "Math"}),
779 "lessons",
780 );
781
782 let json = serde_json::to_string(&dto).unwrap();
783 assert!(json.contains(r#""type":"addEntity""#));
784 assert!(json.contains(r#""entity_class":"Lesson""#));
785 assert!(json.contains(r#""entity_id":"lesson1""#));
786
787 let parsed: ProblemChangeDto = serde_json::from_str(&json).unwrap();
788 assert_eq!(dto, parsed);
789 }
790
791 #[test]
792 fn test_remove_entity_dto_serialization() {
793 let dto = ProblemChangeDto::remove_entity("Lesson", "lesson1", "lessons");
794
795 let json = serde_json::to_string(&dto).unwrap();
796 assert!(json.contains(r#""type":"removeEntity""#));
797
798 let parsed: ProblemChangeDto = serde_json::from_str(&json).unwrap();
799 assert_eq!(dto, parsed);
800 }
801
802 #[test]
803 fn test_change_variable_dto_serialization() {
804 let dto = ProblemChangeDto::change_variable("Lesson", "lesson1", "room", json!("Room101"));
805
806 let json = serde_json::to_string(&dto).unwrap();
807 assert!(json.contains(r#""type":"changeVariable""#));
808 assert!(json.contains(r#""variable_name":"room""#));
809
810 let parsed: ProblemChangeDto = serde_json::from_str(&json).unwrap();
811 assert_eq!(dto, parsed);
812 }
813
814 #[test]
815 fn test_batch_dto_serialization() {
816 let dto = ProblemChangeDto::batch(vec![
817 ProblemChangeDto::add_entity("Lesson", "l1", json!({"id": "l1"}), "lessons"),
818 ProblemChangeDto::remove_entity("Lesson", "l2", "lessons"),
819 ]);
820
821 let json = serde_json::to_string(&dto).unwrap();
822 assert!(json.contains(r#""type":"batch""#));
823 assert!(json.contains(r#""changes""#));
824
825 let parsed: ProblemChangeDto = serde_json::from_str(&json).unwrap();
826 assert_eq!(dto, parsed);
827 }
828
829 #[test]
831 fn test_director_new_indexes_objects() {
832 let solution = json!({
833 "lessons": [
834 {"id": "lesson1", "subject": "Math"},
835 {"id": "lesson2", "subject": "English"}
836 ],
837 "rooms": [
838 {"id": "room1", "capacity": 30}
839 ]
840 });
841
842 let director = DefaultProblemChangeDirector::new(&solution, "id");
843
844 assert!(director.look_up_working_object("lesson1").is_some());
845 assert!(director.look_up_working_object("lesson2").is_some());
846 assert!(director.look_up_working_object("room1").is_some());
847 assert!(director.look_up_working_object("nonexistent").is_none());
848 }
849
850 #[test]
851 fn test_director_look_up_or_fail() {
852 let solution = json!({
853 "entities": [{"id": "e1", "value": 10}]
854 });
855
856 let director = DefaultProblemChangeDirector::new(&solution, "id");
857
858 let result = director.look_up_working_object_or_fail("e1");
859 assert!(result.is_ok());
860 assert_eq!(result.unwrap()["value"], 10);
861
862 let result = director.look_up_working_object_or_fail("nonexistent");
863 assert!(result.is_err());
864 match result.unwrap_err() {
865 ProblemChangeError::ObjectNotFound { id } => {
866 assert_eq!(id, "nonexistent");
867 }
868 _ => panic!("Expected ObjectNotFound error"),
869 }
870 }
871
872 #[test]
873 fn test_director_add_entity() {
874 let solution = json!({"entities": []});
875 let mut director = DefaultProblemChangeDirector::new(&solution, "id");
876
877 let new_entity = json!({"id": "e1", "value": 42});
878 director.add_entity("e1", new_entity.clone(), Box::new(|_| {}));
879
880 assert!(director.look_up_working_object("e1").is_some());
881 assert!(director.has_changes());
882
883 let changes = director.changes();
884 assert_eq!(changes.len(), 1);
885 matches!(&changes[0], ChangeRecord::EntityAdded { id } if id == "e1");
886 }
887
888 #[test]
889 fn test_director_remove_entity() {
890 let solution = json!({
891 "entities": [{"id": "e1", "value": 42}]
892 });
893 let mut director = DefaultProblemChangeDirector::new(&solution, "id");
894
895 assert!(director.look_up_working_object("e1").is_some());
896
897 director.remove_entity("e1", Box::new(|_| {}));
898
899 assert!(director.look_up_working_object("e1").is_none());
900 assert!(director.has_changes());
901 }
902
903 #[test]
904 fn test_director_change_variable() {
905 let solution = json!({
906 "entities": [{"id": "e1", "room": null}]
907 });
908 let mut director = DefaultProblemChangeDirector::new(&solution, "id");
909
910 director.change_variable(
911 "e1",
912 "room",
913 Box::new(|entity| {
914 entity["room"] = json!("Room101");
915 }),
916 );
917
918 let entity = director.look_up_working_object("e1").unwrap();
919 assert_eq!(entity["room"], json!("Room101"));
920 assert!(director.has_changes());
921 }
922
923 #[test]
924 fn test_director_add_problem_fact() {
925 let solution = json!({"facts": []});
926 let mut director = DefaultProblemChangeDirector::new(&solution, "id");
927
928 let fact = json!({"id": "f1", "name": "Fact1"});
929 director.add_problem_fact("f1", fact, Box::new(|_| {}));
930
931 assert!(director.look_up_working_object("f1").is_some());
932 assert!(director.has_changes());
933 }
934
935 #[test]
936 fn test_director_remove_problem_fact() {
937 let solution = json!({
938 "facts": [{"id": "f1", "name": "Fact1"}]
939 });
940 let mut director = DefaultProblemChangeDirector::new(&solution, "id");
941
942 director.remove_problem_fact("f1", Box::new(|_| {}));
943
944 assert!(director.look_up_working_object("f1").is_none());
945 assert!(director.has_changes());
946 }
947
948 #[test]
949 fn test_director_change_problem_property() {
950 let solution = json!({
951 "facts": [{"id": "f1", "capacity": 30}]
952 });
953 let mut director = DefaultProblemChangeDirector::new(&solution, "id");
954
955 director.change_problem_property(
956 "f1",
957 Box::new(|obj| {
958 obj["capacity"] = json!(50);
959 }),
960 );
961
962 let fact = director.look_up_working_object("f1").unwrap();
963 assert_eq!(fact["capacity"], json!(50));
964 assert!(director.has_changes());
965 }
966
967 #[test]
968 fn test_director_update_shadow_variables() {
969 let solution = json!({"entities": []});
970 let mut director = DefaultProblemChangeDirector::new(&solution, "id");
971
972 director.add_entity("e1", json!({"id": "e1"}), Box::new(|_| {}));
973 assert!(director.shadow_variables_dirty);
974
975 director.update_shadow_variables();
976 assert!(!director.shadow_variables_dirty);
977 }
978
979 #[test]
980 fn test_director_no_changes_initially() {
981 let solution = json!({"entities": []});
982 let director = DefaultProblemChangeDirector::new(&solution, "id");
983
984 assert!(!director.has_changes());
985 assert!(director.changes().is_empty());
986 }
987
988 #[test]
989 fn test_director_multiple_changes() {
990 let solution = json!({
991 "entities": [{"id": "e1", "room": null}],
992 "rooms": [{"id": "r1", "capacity": 30}]
993 });
994 let mut director = DefaultProblemChangeDirector::new(&solution, "id");
995
996 director.add_entity("e2", json!({"id": "e2", "room": null}), Box::new(|_| {}));
997 director.change_variable(
998 "e1",
999 "room",
1000 Box::new(|e| {
1001 e["room"] = json!("r1");
1002 }),
1003 );
1004 director.remove_problem_fact("r1", Box::new(|_| {}));
1005
1006 assert_eq!(director.changes().len(), 3);
1007 }
1008
1009 #[test]
1010 fn test_director_nested_objects() {
1011 let solution = json!({
1012 "schedule": {
1013 "lessons": [
1014 {
1015 "id": "l1",
1016 "teacher": {"id": "t1", "name": "Alice"}
1017 }
1018 ]
1019 }
1020 });
1021
1022 let director = DefaultProblemChangeDirector::new(&solution, "id");
1023
1024 assert!(director.look_up_working_object("l1").is_some());
1026 assert!(director.look_up_working_object("t1").is_some());
1027 }
1028
1029 #[test]
1030 fn test_director_update_and_remove_from_index() {
1031 let solution = json!({"entities": []});
1032 let mut director = DefaultProblemChangeDirector::new(&solution, "id");
1033
1034 director.update_index("manual1".to_string(), json!({"id": "manual1", "value": 1}));
1035 assert!(director.look_up_working_object("manual1").is_some());
1036
1037 director.remove_from_index("manual1");
1038 assert!(director.look_up_working_object("manual1").is_none());
1039 }
1040
1041 #[test]
1043 fn test_error_serialization() {
1044 let err = ProblemChangeError::ObjectNotFound {
1045 id: "test".to_string(),
1046 };
1047 let json = serde_json::to_string(&err).unwrap();
1048 let parsed: ProblemChangeError = serde_json::from_str(&json).unwrap();
1049 assert_eq!(err, parsed);
1050 }
1051
1052 #[test]
1054 fn test_director_remove_nonexistent_entity() {
1055 let solution = json!({"entities": []});
1056 let mut director = DefaultProblemChangeDirector::new(&solution, "id");
1057
1058 director.remove_entity("nonexistent", Box::new(|_| {}));
1060 assert!(!director.has_changes());
1061 }
1062
1063 #[test]
1064 fn test_director_change_variable_nonexistent() {
1065 let solution = json!({"entities": []});
1066 let mut director = DefaultProblemChangeDirector::new(&solution, "id");
1067
1068 director.change_variable("nonexistent", "room", Box::new(|_| {}));
1070 assert!(!director.has_changes());
1071 }
1072
1073 #[test]
1074 fn test_dto_with_complex_entity() {
1075 let complex_entity = json!({
1076 "id": "lesson1",
1077 "subject": "Advanced Mathematics",
1078 "teacher": {"id": "t1", "name": "Dr. Smith"},
1079 "students": [
1080 {"id": "s1", "name": "Alice"},
1081 {"id": "s2", "name": "Bob"}
1082 ],
1083 "schedule": {
1084 "day": "Monday",
1085 "period": 3
1086 }
1087 });
1088
1089 let dto =
1090 ProblemChangeDto::add_entity("Lesson", "lesson1", complex_entity.clone(), "lessons");
1091
1092 let json = serde_json::to_string(&dto).unwrap();
1093 let parsed: ProblemChangeDto = serde_json::from_str(&json).unwrap();
1094
1095 match parsed {
1096 ProblemChangeDto::AddEntity { entity, .. } => {
1097 assert_eq!(entity, complex_entity);
1098 }
1099 _ => panic!("Expected AddEntity"),
1100 }
1101 }
1102
1103 #[test]
1104 fn test_director_empty_solution() {
1105 let solution = json!({});
1106 let director = DefaultProblemChangeDirector::new(&solution, "id");
1107
1108 assert!(director.look_up_working_object("any").is_none());
1109 }
1110
1111 #[test]
1112 fn test_director_array_at_root() {
1113 let solution = json!([
1114 {"id": "e1", "value": 1},
1115 {"id": "e2", "value": 2}
1116 ]);
1117
1118 let director = DefaultProblemChangeDirector::new(&solution, "id");
1119
1120 assert!(director.look_up_working_object("e1").is_some());
1121 assert!(director.look_up_working_object("e2").is_some());
1122 }
1123
1124 #[test]
1125 fn test_director_different_id_field() {
1126 let solution = json!({
1127 "entities": [
1128 {"uuid": "abc-123", "name": "Entity1"},
1129 {"uuid": "def-456", "name": "Entity2"}
1130 ]
1131 });
1132
1133 let director = DefaultProblemChangeDirector::new(&solution, "uuid");
1134
1135 assert!(director.look_up_working_object("abc-123").is_some());
1136 assert!(director.look_up_working_object("def-456").is_some());
1137 assert!(director.look_up_working_object("Entity1").is_none());
1138 }
1139}