1use std::fmt;
39use std::marker::PhantomData;
40
41use serde::{de::DeserializeOwned, Serialize};
42
43use crate::sealed::Sealed;
44
45#[derive(Debug, Clone)]
50pub struct SerDesError {
51 pub kind: SerDesErrorKind,
53 pub message: String,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum SerDesErrorKind {
60 Serialization,
62 Deserialization,
64}
65
66impl SerDesError {
67 pub fn serialization(message: impl Into<String>) -> Self {
69 Self {
70 kind: SerDesErrorKind::Serialization,
71 message: message.into(),
72 }
73 }
74
75 pub fn deserialization(message: impl Into<String>) -> Self {
77 Self {
78 kind: SerDesErrorKind::Deserialization,
79 message: message.into(),
80 }
81 }
82}
83
84impl fmt::Display for SerDesError {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self.kind {
87 SerDesErrorKind::Serialization => write!(f, "Serialization error: {}", self.message),
88 SerDesErrorKind::Deserialization => {
89 write!(f, "Deserialization error: {}", self.message)
90 }
91 }
92 }
93}
94
95impl std::error::Error for SerDesError {}
96
97impl From<serde_json::Error> for SerDesError {
98 fn from(error: serde_json::Error) -> Self {
99 if error.is_io() || error.is_syntax() || error.is_data() {
100 Self::deserialization(error.to_string())
101 } else {
102 Self::serialization(error.to_string())
103 }
104 }
105}
106
107#[derive(Debug, Clone)]
112pub struct SerDesContext {
113 pub operation_id: String,
115 pub durable_execution_arn: String,
117}
118
119impl SerDesContext {
120 pub fn new(operation_id: impl Into<String>, durable_execution_arn: impl Into<String>) -> Self {
122 Self {
123 operation_id: operation_id.into(),
124 durable_execution_arn: durable_execution_arn.into(),
125 }
126 }
127}
128
129#[allow(private_bounds)]
166pub trait SerDes<T>: Sealed + Send + Sync {
167 fn serialize(&self, value: &T, context: &SerDesContext) -> Result<String, SerDesError>;
178
179 fn deserialize(&self, data: &str, context: &SerDesContext) -> Result<T, SerDesError>;
190}
191
192pub struct JsonSerDes<T> {
222 _marker: PhantomData<T>,
223}
224
225impl<T> Sealed for JsonSerDes<T> {}
227
228impl<T> JsonSerDes<T> {
229 pub fn new() -> Self {
231 Self {
232 _marker: PhantomData,
233 }
234 }
235}
236
237impl<T> Default for JsonSerDes<T> {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243impl<T> Clone for JsonSerDes<T> {
244 fn clone(&self) -> Self {
245 Self::new()
246 }
247}
248
249impl<T> fmt::Debug for JsonSerDes<T> {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 f.debug_struct("JsonSerDes").finish()
252 }
253}
254
255impl<T> SerDes<T> for JsonSerDes<T>
256where
257 T: Serialize + DeserializeOwned,
258{
259 fn serialize(&self, value: &T, _context: &SerDesContext) -> Result<String, SerDesError> {
260 serde_json::to_string(value).map_err(|e| SerDesError::serialization(e.to_string()))
261 }
262
263 fn deserialize(&self, data: &str, _context: &SerDesContext) -> Result<T, SerDesError> {
264 serde_json::from_str(data).map_err(|e| SerDesError::deserialization(e.to_string()))
265 }
266}
267
268unsafe impl<T> Send for JsonSerDes<T> {}
270unsafe impl<T> Sync for JsonSerDes<T> {}
271
272pub struct CustomSerDes<T, S, D>
299where
300 T: Send + Sync,
301 S: Fn(&T, &SerDesContext) -> Result<String, SerDesError> + Send + Sync,
302 D: Fn(&str, &SerDesContext) -> Result<T, SerDesError> + Send + Sync,
303{
304 serialize_fn: S,
305 deserialize_fn: D,
306 _marker: PhantomData<T>,
307}
308
309impl<T, S, D> Sealed for CustomSerDes<T, S, D>
311where
312 T: Send + Sync,
313 S: Fn(&T, &SerDesContext) -> Result<String, SerDesError> + Send + Sync,
314 D: Fn(&str, &SerDesContext) -> Result<T, SerDesError> + Send + Sync,
315{
316}
317
318impl<T, S, D> SerDes<T> for CustomSerDes<T, S, D>
319where
320 T: Send + Sync,
321 S: Fn(&T, &SerDesContext) -> Result<String, SerDesError> + Send + Sync,
322 D: Fn(&str, &SerDesContext) -> Result<T, SerDesError> + Send + Sync,
323{
324 fn serialize(&self, value: &T, context: &SerDesContext) -> Result<String, SerDesError> {
325 (self.serialize_fn)(value, context)
326 }
327
328 fn deserialize(&self, data: &str, context: &SerDesContext) -> Result<T, SerDesError> {
329 (self.deserialize_fn)(data, context)
330 }
331}
332
333impl<T, S, D> fmt::Debug for CustomSerDes<T, S, D>
334where
335 T: Send + Sync,
336 S: Fn(&T, &SerDesContext) -> Result<String, SerDesError> + Send + Sync,
337 D: Fn(&str, &SerDesContext) -> Result<T, SerDesError> + Send + Sync,
338{
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 f.debug_struct("CustomSerDes").finish()
341 }
342}
343
344pub fn custom_serdes<T, S, D>(serialize_fn: S, deserialize_fn: D) -> CustomSerDes<T, S, D>
377where
378 T: Send + Sync,
379 S: Fn(&T, &SerDesContext) -> Result<String, SerDesError> + Send + Sync,
380 D: Fn(&str, &SerDesContext) -> Result<T, SerDesError> + Send + Sync,
381{
382 CustomSerDes {
383 serialize_fn,
384 deserialize_fn,
385 _marker: PhantomData,
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use serde::{Deserialize, Serialize};
393
394 #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
395 struct TestData {
396 name: String,
397 value: i32,
398 }
399
400 fn create_test_context() -> SerDesContext {
401 SerDesContext::new(
402 "test-op-123",
403 "arn:aws:lambda:us-east-1:123456789:function:test",
404 )
405 }
406
407 #[test]
408 fn test_serdes_context_creation() {
409 let ctx = SerDesContext::new("op-1", "arn:test");
410 assert_eq!(ctx.operation_id, "op-1");
411 assert_eq!(ctx.durable_execution_arn, "arn:test");
412 }
413
414 #[test]
415 fn test_serdes_error_serialization() {
416 let error = SerDesError::serialization("failed to serialize");
417 assert_eq!(error.kind, SerDesErrorKind::Serialization);
418 assert!(error.to_string().contains("Serialization error"));
419 }
420
421 #[test]
422 fn test_serdes_error_deserialization() {
423 let error = SerDesError::deserialization("failed to deserialize");
424 assert_eq!(error.kind, SerDesErrorKind::Deserialization);
425 assert!(error.to_string().contains("Deserialization error"));
426 }
427
428 #[test]
429 fn test_json_serdes_serialize() {
430 let serdes = JsonSerDes::<TestData>::new();
431 let context = create_test_context();
432 let data = TestData {
433 name: "test".to_string(),
434 value: 42,
435 };
436
437 let result = serdes.serialize(&data, &context).unwrap();
438 assert!(result.contains("\"name\":\"test\""));
439 assert!(result.contains("\"value\":42"));
440 }
441
442 #[test]
443 fn test_json_serdes_deserialize() {
444 let serdes = JsonSerDes::<TestData>::new();
445 let context = create_test_context();
446 let json = r#"{"name":"test","value":42}"#;
447
448 let result = serdes.deserialize(json, &context).unwrap();
449 assert_eq!(result.name, "test");
450 assert_eq!(result.value, 42);
451 }
452
453 #[test]
454 fn test_json_serdes_round_trip() {
455 let serdes = JsonSerDes::<TestData>::new();
456 let context = create_test_context();
457 let original = TestData {
458 name: "round-trip".to_string(),
459 value: 123,
460 };
461
462 let serialized = serdes.serialize(&original, &context).unwrap();
463 let deserialized = serdes.deserialize(&serialized, &context).unwrap();
464
465 assert_eq!(original, deserialized);
466 }
467
468 #[test]
469 fn test_json_serdes_deserialize_invalid() {
470 let serdes = JsonSerDes::<TestData>::new();
471 let context = create_test_context();
472 let invalid_json = "not valid json";
473
474 let result = serdes.deserialize(invalid_json, &context);
475 assert!(result.is_err());
476 assert_eq!(result.unwrap_err().kind, SerDesErrorKind::Deserialization);
477 }
478
479 #[test]
480 fn test_json_serdes_default() {
481 let serdes: JsonSerDes<TestData> = JsonSerDes::default();
482 let context = create_test_context();
483 let data = TestData {
484 name: "default".to_string(),
485 value: 1,
486 };
487
488 let result = serdes.serialize(&data, &context);
489 assert!(result.is_ok());
490 }
491
492 #[test]
493 fn test_json_serdes_clone() {
494 let serdes = JsonSerDes::<TestData>::new();
495 let cloned = serdes.clone();
496 let context = create_test_context();
497 let data = TestData {
498 name: "clone".to_string(),
499 value: 2,
500 };
501
502 let result1 = serdes.serialize(&data, &context).unwrap();
503 let result2 = cloned.serialize(&data, &context).unwrap();
504 assert_eq!(result1, result2);
505 }
506
507 #[test]
508 fn test_json_serdes_primitive_types() {
509 let string_serdes = JsonSerDes::<String>::new();
511 let context = create_test_context();
512 let original = "hello world".to_string();
513 let serialized = string_serdes.serialize(&original, &context).unwrap();
514 let deserialized: String = string_serdes.deserialize(&serialized, &context).unwrap();
515 assert_eq!(original, deserialized);
516
517 let int_serdes = JsonSerDes::<i32>::new();
519 let original = 42i32;
520 let serialized = int_serdes.serialize(&original, &context).unwrap();
521 let deserialized: i32 = int_serdes.deserialize(&serialized, &context).unwrap();
522 assert_eq!(original, deserialized);
523
524 let vec_serdes = JsonSerDes::<Vec<i32>>::new();
526 let original = vec![1, 2, 3, 4, 5];
527 let serialized = vec_serdes.serialize(&original, &context).unwrap();
528 let deserialized: Vec<i32> = vec_serdes.deserialize(&serialized, &context).unwrap();
529 assert_eq!(original, deserialized);
530 }
531
532 mod sealed_serdes_tests {
541 use super::*;
542
543 #[test]
544 fn test_json_serdes_implements_serdes() {
545 let serdes: &dyn SerDes<String> = &JsonSerDes::<String>::new();
547 let context = create_test_context();
548
549 let serialized = serdes.serialize(&"test".to_string(), &context).unwrap();
550 let deserialized = serdes.deserialize(&serialized, &context).unwrap();
551 assert_eq!(deserialized, "test");
552 }
553
554 #[test]
555 fn test_custom_serdes_implements_serdes() {
556 let serdes = custom_serdes::<i32, _, _>(
558 |value, _ctx| Ok(value.to_string()),
559 |data, _ctx| {
560 data.parse()
561 .map_err(|e| SerDesError::deserialization(format!("{}", e)))
562 },
563 );
564
565 let serdes_ref: &dyn SerDes<i32> = &serdes;
566 let context = create_test_context();
567
568 let serialized = serdes_ref.serialize(&42, &context).unwrap();
569 assert_eq!(serialized, "42");
570
571 let deserialized = serdes_ref.deserialize("42", &context).unwrap();
572 assert_eq!(deserialized, 42);
573 }
574
575 #[test]
576 fn test_custom_serdes_round_trip() {
577 let serdes = custom_serdes::<String, _, _>(
578 |value, _ctx| Ok(format!("PREFIX:{}", value)),
579 |data, _ctx| {
580 data.strip_prefix("PREFIX:")
581 .map(|s| s.to_string())
582 .ok_or_else(|| SerDesError::deserialization("Missing PREFIX"))
583 },
584 );
585
586 let context = create_test_context();
587 let original = "hello world".to_string();
588
589 let serialized = serdes.serialize(&original, &context).unwrap();
590 assert_eq!(serialized, "PREFIX:hello world");
591
592 let deserialized = serdes.deserialize(&serialized, &context).unwrap();
593 assert_eq!(deserialized, original);
594 }
595
596 #[test]
597 fn test_custom_serdes_error_handling() {
598 let serdes = custom_serdes::<i32, _, _>(
599 |_value, _ctx| Err(SerDesError::serialization("intentional error")),
600 |_data, _ctx| Err(SerDesError::deserialization("intentional error")),
601 );
602
603 let context = create_test_context();
604
605 let serialize_result = serdes.serialize(&42, &context);
606 assert!(serialize_result.is_err());
607 assert_eq!(
608 serialize_result.unwrap_err().kind,
609 SerDesErrorKind::Serialization
610 );
611
612 let deserialize_result = serdes.deserialize("42", &context);
613 assert!(deserialize_result.is_err());
614 assert_eq!(
615 deserialize_result.unwrap_err().kind,
616 SerDesErrorKind::Deserialization
617 );
618 }
619
620 #[test]
621 fn test_custom_serdes_receives_context() {
622 use std::sync::atomic::{AtomicBool, Ordering};
623
624 let context_received = std::sync::Arc::new(AtomicBool::new(false));
625 let context_clone = context_received.clone();
626
627 let serdes = custom_serdes::<String, _, _>(
628 move |value, ctx| {
629 assert_eq!(ctx.operation_id, "test-op-123");
630 assert!(ctx.durable_execution_arn.contains("lambda"));
631 context_clone.store(true, Ordering::SeqCst);
632 Ok(value.clone())
633 },
634 |data, _ctx| Ok(data.to_string()),
635 );
636
637 let context = create_test_context();
638 let _ = serdes.serialize(&"test".to_string(), &context);
639
640 assert!(context_received.load(Ordering::SeqCst));
641 }
642
643 #[test]
644 fn test_custom_serdes_with_complex_type() {
645 #[derive(Debug, Clone, PartialEq)]
646 struct Point {
647 x: i32,
648 y: i32,
649 }
650
651 let serdes = custom_serdes::<Point, _, _>(
652 |point, _ctx| Ok(format!("{},{}", point.x, point.y)),
653 |data, _ctx| {
654 let parts: Vec<&str> = data.split(',').collect();
655 if parts.len() != 2 {
656 return Err(SerDesError::deserialization("Invalid format"));
657 }
658 let x = parts[0]
659 .parse()
660 .map_err(|_| SerDesError::deserialization("Invalid x"))?;
661 let y = parts[1]
662 .parse()
663 .map_err(|_| SerDesError::deserialization("Invalid y"))?;
664 Ok(Point { x, y })
665 },
666 );
667
668 let context = create_test_context();
669 let original = Point { x: 10, y: 20 };
670
671 let serialized = serdes.serialize(&original, &context).unwrap();
672 assert_eq!(serialized, "10,20");
673
674 let deserialized = serdes.deserialize(&serialized, &context).unwrap();
675 assert_eq!(deserialized, original);
676 }
677 }
678}
679
680#[cfg(test)]
681mod property_tests {
682 use super::*;
683 use proptest::prelude::*;
684 use serde::{Deserialize, Serialize};
685
686 mod serdes_round_trip {
693 use super::*;
694
695 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
696 struct ComplexData {
697 string_field: String,
698 int_field: i64,
699 bool_field: bool,
700 optional_field: Option<String>,
701 vec_field: Vec<i32>,
702 }
703
704 fn arbitrary_context() -> impl Strategy<Value = SerDesContext> {
705 (any::<String>(), any::<String>()).prop_map(|(op_id, arn)| {
706 SerDesContext::new(
707 if op_id.is_empty() {
708 "default-op".to_string()
709 } else {
710 op_id
711 },
712 if arn.is_empty() {
713 "arn:default".to_string()
714 } else {
715 arn
716 },
717 )
718 })
719 }
720
721 fn arbitrary_complex_data() -> impl Strategy<Value = ComplexData> {
722 (
723 any::<String>(),
724 any::<i64>(),
725 any::<bool>(),
726 any::<Option<String>>(),
727 any::<Vec<i32>>(),
728 )
729 .prop_map(
730 |(string_field, int_field, bool_field, optional_field, vec_field)| {
731 ComplexData {
732 string_field,
733 int_field,
734 bool_field,
735 optional_field,
736 vec_field,
737 }
738 },
739 )
740 }
741
742 proptest! {
743 #![proptest_config(ProptestConfig::with_cases(100))]
744
745 #[test]
748 fn prop_string_round_trip(value: String, context in arbitrary_context()) {
749 let serdes = JsonSerDes::<String>::new();
750 let serialized = serdes.serialize(&value, &context).unwrap();
751 let deserialized = serdes.deserialize(&serialized, &context).unwrap();
752 prop_assert_eq!(value, deserialized);
753 }
754
755 #[test]
758 fn prop_i64_round_trip(value: i64, context in arbitrary_context()) {
759 let serdes = JsonSerDes::<i64>::new();
760 let serialized = serdes.serialize(&value, &context).unwrap();
761 let deserialized = serdes.deserialize(&serialized, &context).unwrap();
762 prop_assert_eq!(value, deserialized);
763 }
764
765 #[test]
770 fn prop_f64_round_trip(value in any::<f64>().prop_filter("finite", |v| v.is_finite()), context in arbitrary_context()) {
771 let serdes = JsonSerDes::<f64>::new();
772 let serialized = serdes.serialize(&value, &context).unwrap();
773 let deserialized: f64 = serdes.deserialize(&serialized, &context).unwrap();
774
775 let epsilon = 1e-10;
778 let diff = (value - deserialized).abs();
779 let relative_diff = if value.abs() > epsilon {
780 diff / value.abs()
781 } else {
782 diff
783 };
784 prop_assert!(
785 relative_diff < epsilon,
786 "f64 round-trip failed: original={}, deserialized={}, relative_diff={}",
787 value, deserialized, relative_diff
788 );
789 }
790
791 #[test]
794 fn prop_bool_round_trip(value: bool, context in arbitrary_context()) {
795 let serdes = JsonSerDes::<bool>::new();
796 let serialized = serdes.serialize(&value, &context).unwrap();
797 let deserialized = serdes.deserialize(&serialized, &context).unwrap();
798 prop_assert_eq!(value, deserialized);
799 }
800
801 #[test]
804 fn prop_vec_round_trip(value: Vec<i32>, context in arbitrary_context()) {
805 let serdes = JsonSerDes::<Vec<i32>>::new();
806 let serialized = serdes.serialize(&value, &context).unwrap();
807 let deserialized = serdes.deserialize(&serialized, &context).unwrap();
808 prop_assert_eq!(value, deserialized);
809 }
810
811 #[test]
814 fn prop_option_round_trip(value: Option<String>, context in arbitrary_context()) {
815 let serdes = JsonSerDes::<Option<String>>::new();
816 let serialized = serdes.serialize(&value, &context).unwrap();
817 let deserialized = serdes.deserialize(&serialized, &context).unwrap();
818 prop_assert_eq!(value, deserialized);
819 }
820
821 #[test]
824 fn prop_complex_data_round_trip(
825 value in arbitrary_complex_data(),
826 context in arbitrary_context()
827 ) {
828 let serdes = JsonSerDes::<ComplexData>::new();
829 let serialized = serdes.serialize(&value, &context).unwrap();
830 let deserialized = serdes.deserialize(&serialized, &context).unwrap();
831 prop_assert_eq!(value, deserialized);
832 }
833
834 #[test]
837 fn prop_nested_round_trip(value: std::collections::HashMap<String, Vec<i32>>, context in arbitrary_context()) {
838 let serdes = JsonSerDes::<std::collections::HashMap<String, Vec<i32>>>::new();
839 let serialized = serdes.serialize(&value, &context).unwrap();
840 let deserialized = serdes.deserialize(&serialized, &context).unwrap();
841 prop_assert_eq!(value, deserialized);
842 }
843 }
844 }
845
846 mod operation_round_trip {
852 use super::*;
853 use crate::operation::{
854 CallbackDetails, ChainedInvokeDetails, ContextDetails, ExecutionDetails, Operation,
855 OperationStatus, OperationType, StepDetails, WaitDetails,
856 };
857
858 fn operation_type_strategy() -> impl Strategy<Value = OperationType> {
860 prop_oneof![
861 Just(OperationType::Execution),
862 Just(OperationType::Step),
863 Just(OperationType::Wait),
864 Just(OperationType::Callback),
865 Just(OperationType::Invoke),
866 Just(OperationType::Context),
867 ]
868 }
869
870 fn operation_status_strategy() -> impl Strategy<Value = OperationStatus> {
872 prop_oneof![
873 Just(OperationStatus::Started),
874 Just(OperationStatus::Pending),
875 Just(OperationStatus::Ready),
876 Just(OperationStatus::Succeeded),
877 Just(OperationStatus::Failed),
878 Just(OperationStatus::Cancelled),
879 Just(OperationStatus::TimedOut),
880 Just(OperationStatus::Stopped),
881 ]
882 }
883
884 fn operation_id_strategy() -> impl Strategy<Value = String> {
886 "[a-zA-Z][a-zA-Z0-9_-]{0,63}".prop_map(|s| s)
887 }
888
889 fn optional_string_strategy() -> impl Strategy<Value = Option<String>> {
891 prop_oneof![Just(None), "[a-zA-Z0-9_-]{1,32}".prop_map(|s| Some(s)),]
892 }
893
894 fn optional_result_strategy() -> impl Strategy<Value = Option<String>> {
896 prop_oneof![
897 Just(None),
898 Just(Some("null".to_string())),
899 Just(Some("42".to_string())),
900 Just(Some("\"test-result\"".to_string())),
901 Just(Some("{\"key\":\"value\"}".to_string())),
902 Just(Some("[1,2,3]".to_string())),
903 ]
904 }
905
906 fn optional_timestamp_strategy() -> impl Strategy<Value = Option<i64>> {
908 prop_oneof![
909 Just(None),
910 (1000000000000i64..2000000000000i64).prop_map(Some),
911 ]
912 }
913
914 fn operation_strategy() -> impl Strategy<Value = Operation> {
916 (
917 operation_id_strategy(),
918 operation_type_strategy(),
919 operation_status_strategy(),
920 optional_string_strategy(), optional_string_strategy(), optional_string_strategy(), optional_timestamp_strategy(), optional_timestamp_strategy(), )
926 .prop_flat_map(
927 |(id, op_type, status, parent_id, name, sub_type, start_ts, end_ts)| {
928 let details_strategy = match op_type {
930 OperationType::Step => optional_result_strategy()
931 .prop_map(move |result| {
932 let mut op = Operation::new(id.clone(), op_type);
933 op.status = status;
934 op.parent_id = parent_id.clone();
935 op.name = name.clone();
936 op.sub_type = sub_type.clone();
937 op.start_timestamp = start_ts;
938 op.end_timestamp = end_ts;
939 op.step_details = Some(StepDetails {
940 result,
941 attempt: Some(0),
942 next_attempt_timestamp: None,
943 error: None,
944 payload: None,
945 });
946 op
947 })
948 .boxed(),
949 OperationType::Wait => Just(())
950 .prop_map(move |_| {
951 let mut op = Operation::new(id.clone(), op_type);
952 op.status = status;
953 op.parent_id = parent_id.clone();
954 op.name = name.clone();
955 op.sub_type = sub_type.clone();
956 op.start_timestamp = start_ts;
957 op.end_timestamp = end_ts;
958 op.wait_details = Some(WaitDetails {
959 scheduled_end_timestamp: Some(1234567890000),
960 });
961 op
962 })
963 .boxed(),
964 OperationType::Callback => optional_result_strategy()
965 .prop_map(move |result| {
966 let mut op = Operation::new(id.clone(), op_type);
967 op.status = status;
968 op.parent_id = parent_id.clone();
969 op.name = name.clone();
970 op.sub_type = sub_type.clone();
971 op.start_timestamp = start_ts;
972 op.end_timestamp = end_ts;
973 op.callback_details = Some(CallbackDetails {
974 callback_id: Some(format!("cb-{}", id.clone())),
975 result,
976 error: None,
977 });
978 op
979 })
980 .boxed(),
981 OperationType::Invoke => optional_result_strategy()
982 .prop_map(move |result| {
983 let mut op = Operation::new(id.clone(), op_type);
984 op.status = status;
985 op.parent_id = parent_id.clone();
986 op.name = name.clone();
987 op.sub_type = sub_type.clone();
988 op.start_timestamp = start_ts;
989 op.end_timestamp = end_ts;
990 op.chained_invoke_details = Some(ChainedInvokeDetails {
991 result,
992 error: None,
993 });
994 op
995 })
996 .boxed(),
997 OperationType::Context => optional_result_strategy()
998 .prop_map(move |result| {
999 let mut op = Operation::new(id.clone(), op_type);
1000 op.status = status;
1001 op.parent_id = parent_id.clone();
1002 op.name = name.clone();
1003 op.sub_type = sub_type.clone();
1004 op.start_timestamp = start_ts;
1005 op.end_timestamp = end_ts;
1006 op.context_details = Some(ContextDetails {
1007 result,
1008 replay_children: Some(true),
1009 error: None,
1010 });
1011 op
1012 })
1013 .boxed(),
1014 OperationType::Execution => optional_result_strategy()
1015 .prop_map(move |input| {
1016 let mut op = Operation::new(id.clone(), op_type);
1017 op.status = status;
1018 op.parent_id = parent_id.clone();
1019 op.name = name.clone();
1020 op.sub_type = sub_type.clone();
1021 op.start_timestamp = start_ts;
1022 op.end_timestamp = end_ts;
1023 op.execution_details = Some(ExecutionDetails {
1024 input_payload: input,
1025 });
1026 op
1027 })
1028 .boxed(),
1029 };
1030 details_strategy
1031 },
1032 )
1033 }
1034
1035 proptest! {
1036 #![proptest_config(ProptestConfig::with_cases(100))]
1037
1038 #[test]
1045 fn prop_operation_json_round_trip(op in operation_strategy()) {
1046 let json = serde_json::to_string(&op).unwrap();
1047 let deserialized: Operation = serde_json::from_str(&json).unwrap();
1048
1049 prop_assert_eq!(&op.operation_id, &deserialized.operation_id);
1051 prop_assert_eq!(op.operation_type, deserialized.operation_type);
1052 prop_assert_eq!(op.status, deserialized.status);
1053 prop_assert_eq!(&op.parent_id, &deserialized.parent_id);
1054 prop_assert_eq!(&op.name, &deserialized.name);
1055 prop_assert_eq!(&op.sub_type, &deserialized.sub_type);
1056 prop_assert_eq!(op.start_timestamp, deserialized.start_timestamp);
1057 prop_assert_eq!(op.end_timestamp, deserialized.end_timestamp);
1058
1059 match op.operation_type {
1061 OperationType::Step => {
1062 prop_assert!(deserialized.step_details.is_some());
1063 let orig = op.step_details.as_ref().unwrap();
1064 let deser = deserialized.step_details.as_ref().unwrap();
1065 prop_assert_eq!(&orig.result, &deser.result);
1066 prop_assert_eq!(orig.attempt, deser.attempt);
1067 }
1068 OperationType::Wait => {
1069 prop_assert!(deserialized.wait_details.is_some());
1070 }
1071 OperationType::Callback => {
1072 prop_assert!(deserialized.callback_details.is_some());
1073 let orig = op.callback_details.as_ref().unwrap();
1074 let deser = deserialized.callback_details.as_ref().unwrap();
1075 prop_assert_eq!(&orig.callback_id, &deser.callback_id);
1076 prop_assert_eq!(&orig.result, &deser.result);
1077 }
1078 OperationType::Invoke => {
1079 prop_assert!(deserialized.chained_invoke_details.is_some());
1080 let orig = op.chained_invoke_details.as_ref().unwrap();
1081 let deser = deserialized.chained_invoke_details.as_ref().unwrap();
1082 prop_assert_eq!(&orig.result, &deser.result);
1083 }
1084 OperationType::Context => {
1085 prop_assert!(deserialized.context_details.is_some());
1086 let orig = op.context_details.as_ref().unwrap();
1087 let deser = deserialized.context_details.as_ref().unwrap();
1088 prop_assert_eq!(&orig.result, &deser.result);
1089 prop_assert_eq!(orig.replay_children, deser.replay_children);
1090 }
1091 OperationType::Execution => {
1092 prop_assert!(deserialized.execution_details.is_some());
1093 let orig = op.execution_details.as_ref().unwrap();
1094 let deser = deserialized.execution_details.as_ref().unwrap();
1095 prop_assert_eq!(&orig.input_payload, &deser.input_payload);
1096 }
1097 }
1098 }
1099 }
1100 }
1101
1102 mod timestamp_format_equivalence {
1108 use super::*;
1109 use crate::operation::Operation;
1110
1111 proptest! {
1112 #![proptest_config(ProptestConfig::with_cases(100))]
1113
1114 #[test]
1121 fn prop_timestamp_integer_round_trip(timestamp in 1000000000000i64..2000000000000i64) {
1122 let json = format!(
1124 r#"{{"Id":"test-op","Type":"STEP","Status":"STARTED","StartTimestamp":{}}}"#,
1125 timestamp
1126 );
1127
1128 let op: Operation = serde_json::from_str(&json).unwrap();
1129 prop_assert_eq!(op.start_timestamp, Some(timestamp));
1130 }
1131
1132 #[test]
1139 fn prop_timestamp_iso8601_parsing(
1140 year in 2020u32..2030u32,
1141 month in 1u32..=12u32,
1142 day in 1u32..=28u32, hour in 0u32..24u32,
1144 minute in 0u32..60u32,
1145 second in 0u32..60u32,
1146 ) {
1147 let iso_string = format!(
1149 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}+00:00",
1150 year, month, day, hour, minute, second
1151 );
1152
1153 let json = format!(
1154 r#"{{"Id":"test-op","Type":"STEP","Status":"STARTED","StartTimestamp":"{}"}}"#,
1155 iso_string
1156 );
1157
1158 let op: Operation = serde_json::from_str(&json).unwrap();
1159 prop_assert!(op.start_timestamp.is_some());
1160
1161 let ts = op.start_timestamp.unwrap();
1163 prop_assert!(ts > 1577836800000); }
1165
1166 #[test]
1178 fn prop_timestamp_float_parsing(
1179 seconds in 1577836800i64..1893456000i64, millis in 1u32..1000u32, ) {
1182 let float_ts = seconds as f64 + (millis as f64 / 1000.0);
1184
1185 let json = format!(
1186 r#"{{"Id":"test-op","Type":"STEP","Status":"STARTED","StartTimestamp":{}}}"#,
1187 float_ts
1188 );
1189
1190 let op: Operation = serde_json::from_str(&json).unwrap();
1191 prop_assert!(op.start_timestamp.is_some());
1192
1193 let expected_millis = seconds * 1000 + millis as i64;
1195 let actual_millis = op.start_timestamp.unwrap();
1196
1197 let diff = (expected_millis - actual_millis).abs();
1199 prop_assert!(diff <= 1, "Timestamp difference too large: expected {}, got {}, diff {}",
1200 expected_millis, actual_millis, diff);
1201 }
1202 }
1203 }
1204
1205 mod operation_update_serialization {
1211 use super::*;
1212 use crate::error::ErrorObject;
1213 use crate::operation::{OperationAction, OperationType, OperationUpdate};
1214
1215 fn operation_action_strategy() -> impl Strategy<Value = OperationAction> {
1217 prop_oneof![
1218 Just(OperationAction::Start),
1219 Just(OperationAction::Succeed),
1220 Just(OperationAction::Fail),
1221 Just(OperationAction::Cancel),
1222 Just(OperationAction::Retry),
1223 ]
1224 }
1225
1226 fn operation_type_strategy() -> impl Strategy<Value = OperationType> {
1228 prop_oneof![
1229 Just(OperationType::Execution),
1230 Just(OperationType::Step),
1231 Just(OperationType::Wait),
1232 Just(OperationType::Callback),
1233 Just(OperationType::Invoke),
1234 Just(OperationType::Context),
1235 ]
1236 }
1237
1238 fn operation_id_strategy() -> impl Strategy<Value = String> {
1240 "[a-zA-Z][a-zA-Z0-9_-]{0,63}".prop_map(|s| s)
1241 }
1242
1243 fn optional_string_strategy() -> impl Strategy<Value = Option<String>> {
1245 prop_oneof![Just(None), "[a-zA-Z0-9_-]{1,32}".prop_map(|s| Some(s)),]
1246 }
1247
1248 fn optional_result_strategy() -> impl Strategy<Value = Option<String>> {
1250 prop_oneof![
1251 Just(None),
1252 Just(Some("null".to_string())),
1253 Just(Some("42".to_string())),
1254 Just(Some("\"test\"".to_string())),
1255 ]
1256 }
1257
1258 fn operation_update_strategy() -> impl Strategy<Value = OperationUpdate> {
1260 (
1261 operation_id_strategy(),
1262 operation_action_strategy(),
1263 operation_type_strategy(),
1264 optional_result_strategy(),
1265 optional_string_strategy(), optional_string_strategy(), )
1268 .prop_map(|(id, action, op_type, result, parent_id, name)| {
1269 let mut update = match action {
1270 OperationAction::Start => {
1271 if op_type == OperationType::Wait {
1272 OperationUpdate::start_wait(&id, 60)
1273 } else {
1274 OperationUpdate::start(&id, op_type)
1275 }
1276 }
1277 OperationAction::Succeed => {
1278 OperationUpdate::succeed(&id, op_type, result.clone())
1279 }
1280 OperationAction::Fail => {
1281 let err = ErrorObject::new("TestError", "Test error message");
1282 OperationUpdate::fail(&id, op_type, err)
1283 }
1284 OperationAction::Cancel => OperationUpdate::cancel(&id, op_type),
1285 OperationAction::Retry => {
1286 OperationUpdate::retry(&id, op_type, result.clone(), None)
1287 }
1288 };
1289 update.parent_id = parent_id;
1290 update.name = name;
1291 update
1292 })
1293 }
1294
1295 proptest! {
1296 #![proptest_config(ProptestConfig::with_cases(100))]
1297
1298 #[test]
1304 fn prop_operation_update_serialization_valid(update in operation_update_strategy()) {
1305 let json = serde_json::to_string(&update).unwrap();
1307
1308 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1310
1311 prop_assert!(parsed.get("Id").is_some(), "Missing Id field");
1313 prop_assert!(parsed.get("Action").is_some(), "Missing Action field");
1314 prop_assert!(parsed.get("Type").is_some(), "Missing Type field");
1315
1316 prop_assert_eq!(
1318 parsed.get("Id").unwrap().as_str().unwrap(),
1319 &update.operation_id
1320 );
1321 }
1322
1323 #[test]
1330 fn prop_operation_update_round_trip(update in operation_update_strategy()) {
1331 let json = serde_json::to_string(&update).unwrap();
1332 let deserialized: OperationUpdate = serde_json::from_str(&json).unwrap();
1333
1334 prop_assert_eq!(&update.operation_id, &deserialized.operation_id);
1336 prop_assert_eq!(update.action, deserialized.action);
1337 prop_assert_eq!(update.operation_type, deserialized.operation_type);
1338 prop_assert_eq!(&update.result, &deserialized.result);
1339 prop_assert_eq!(&update.parent_id, &deserialized.parent_id);
1340 prop_assert_eq!(&update.name, &deserialized.name);
1341 }
1342
1343 #[test]
1350 fn prop_wait_options_serialization(wait_seconds in 1u64..86400u64) {
1351 let update = OperationUpdate::start_wait("test-wait", wait_seconds);
1352
1353 let json = serde_json::to_string(&update).unwrap();
1354 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1355
1356 prop_assert!(parsed.get("WaitOptions").is_some(), "Missing WaitOptions field");
1358
1359 let wait_opts = parsed.get("WaitOptions").unwrap();
1360 prop_assert_eq!(
1361 wait_opts.get("WaitSeconds").unwrap().as_u64().unwrap(),
1362 wait_seconds
1363 );
1364 }
1365 }
1366 }
1367}