Skip to main content

durable_execution_sdk/
serdes.rs

1//! Serialization/Deserialization system for the AWS Durable Execution SDK.
2//!
3//! This module provides a trait-based abstraction for serializing and deserializing
4//! data in checkpoints, allowing users to customize encoding strategies.
5//!
6//! # Overview
7//!
8//! The [`SerDes`] trait defines the interface for serialization, while [`JsonSerDes`]
9//! provides a default JSON implementation using serde_json.
10//!
11//! # Sealed Trait
12//!
13//! The `SerDes` trait is sealed and cannot be implemented outside of this crate.
14//! This allows the SDK maintainers to evolve the serialization interface without
15//! breaking external code. If you need custom serialization behavior, use the
16//! provided factory functions.
17//!
18//! # Example
19//!
20//! ```rust
21//! use durable_execution_sdk::serdes::{SerDes, JsonSerDes, SerDesContext};
22//! use serde::{Serialize, Deserialize};
23//!
24//! #[derive(Serialize, Deserialize, PartialEq, Debug)]
25//! struct MyData {
26//!     value: i32,
27//! }
28//!
29//! let serdes = JsonSerDes::<MyData>::new();
30//! let context = SerDesContext::new("op-123", "arn:aws:lambda:...");
31//! let data = MyData { value: 42 };
32//!
33//! let serialized = serdes.serialize(&data, &context).unwrap();
34//! let deserialized = serdes.deserialize(&serialized, &context).unwrap();
35//! assert_eq!(data, deserialized);
36//! ```
37
38use std::fmt;
39use std::marker::PhantomData;
40
41use serde::{de::DeserializeOwned, Serialize};
42
43use crate::sealed::Sealed;
44
45/// Error type for serialization/deserialization failures.
46///
47/// This error captures both serialization and deserialization failures
48/// with descriptive messages.
49#[derive(Debug, Clone)]
50pub struct SerDesError {
51    /// The kind of error (serialization or deserialization)
52    pub kind: SerDesErrorKind,
53    /// Descriptive error message
54    pub message: String,
55}
56
57/// The kind of SerDes error.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum SerDesErrorKind {
60    /// Error during serialization
61    Serialization,
62    /// Error during deserialization
63    Deserialization,
64}
65
66impl SerDesError {
67    /// Creates a new serialization error.
68    pub fn serialization(message: impl Into<String>) -> Self {
69        Self {
70            kind: SerDesErrorKind::Serialization,
71            message: message.into(),
72        }
73    }
74
75    /// Creates a new deserialization error.
76    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/// Context provided to serializers during serialization/deserialization.
108///
109/// This context contains information about the current operation and execution,
110/// which can be used by custom serializers for logging, metrics, or custom encoding.
111#[derive(Debug, Clone)]
112pub struct SerDesContext {
113    /// The unique identifier for the current operation
114    pub operation_id: String,
115    /// The ARN of the durable execution
116    pub durable_execution_arn: String,
117}
118
119impl SerDesContext {
120    /// Creates a new SerDesContext.
121    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/// Trait for serialization and deserialization of checkpoint data.
130///
131/// Implement this trait to provide custom serialization strategies for
132/// checkpoint data. The SDK provides [`JsonSerDes`] as the default implementation.
133///
134/// # Sealed Trait
135///
136/// This trait is sealed and cannot be implemented outside of this crate.
137/// This allows the SDK maintainers to evolve the serialization interface without
138/// breaking external code. If you need custom serialization behavior, use the
139/// provided factory functions.
140///
141/// # Thread Safety
142///
143/// Implementations must be `Send + Sync` to support concurrent operations.
144///
145/// # Example
146///
147/// ```rust
148/// use durable_execution_sdk::serdes::{SerDes, JsonSerDes, SerDesContext};
149/// use serde::{Serialize, Deserialize};
150///
151/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
152/// struct MyData {
153///     value: i32,
154/// }
155///
156/// // Use the provided JsonSerDes implementation
157/// let serdes = JsonSerDes::<MyData>::new();
158/// let context = SerDesContext::new("op-123", "arn:aws:lambda:...");
159/// let data = MyData { value: 42 };
160///
161/// let serialized = serdes.serialize(&data, &context).unwrap();
162/// let deserialized = serdes.deserialize(&serialized, &context).unwrap();
163/// assert_eq!(data, deserialized);
164/// ```
165#[allow(private_bounds)]
166pub trait SerDes<T>: Sealed + Send + Sync {
167    /// Serializes a value to a string representation.
168    ///
169    /// # Arguments
170    ///
171    /// * `value` - The value to serialize
172    /// * `context` - Context containing operation and execution information
173    ///
174    /// # Returns
175    ///
176    /// A string representation of the value, or a [`SerDesError`] on failure.
177    fn serialize(&self, value: &T, context: &SerDesContext) -> Result<String, SerDesError>;
178
179    /// Deserializes a string representation back to a value.
180    ///
181    /// # Arguments
182    ///
183    /// * `data` - The string representation to deserialize
184    /// * `context` - Context containing operation and execution information
185    ///
186    /// # Returns
187    ///
188    /// The deserialized value, or a [`SerDesError`] on failure.
189    fn deserialize(&self, data: &str, context: &SerDesContext) -> Result<T, SerDesError>;
190}
191
192/// Default JSON serialization implementation using serde_json.
193///
194/// This is the default serializer used by the SDK when no custom serializer
195/// is provided. It uses serde_json for JSON encoding/decoding.
196///
197/// # Type Parameters
198///
199/// * `T` - The type to serialize/deserialize. Must implement `Serialize` and `DeserializeOwned`.
200///
201/// # Example
202///
203/// ```rust
204/// use durable_execution_sdk::serdes::{JsonSerDes, SerDes, SerDesContext};
205/// use serde::{Serialize, Deserialize};
206///
207/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
208/// struct MyData {
209///     name: String,
210///     count: u32,
211/// }
212///
213/// let serdes = JsonSerDes::<MyData>::new();
214/// let context = SerDesContext::new("op-1", "arn:aws:...");
215/// let data = MyData { name: "test".to_string(), count: 42 };
216///
217/// let json = serdes.serialize(&data, &context).unwrap();
218/// let restored: MyData = serdes.deserialize(&json, &context).unwrap();
219/// assert_eq!(data, restored);
220/// ```
221pub struct JsonSerDes<T> {
222    _marker: PhantomData<T>,
223}
224
225// Implement Sealed for JsonSerDes to allow it to implement SerDes
226impl<T> Sealed for JsonSerDes<T> {}
227
228impl<T> JsonSerDes<T> {
229    /// Creates a new JsonSerDes instance.
230    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
268// Ensure JsonSerDes is Send + Sync
269unsafe impl<T> Send for JsonSerDes<T> {}
270unsafe impl<T> Sync for JsonSerDes<T> {}
271
272/// A custom serializer/deserializer that delegates to user-provided closures.
273///
274/// This struct allows users to provide custom serialization behavior without
275/// implementing the sealed `SerDes` trait directly.
276///
277/// # Type Parameters
278///
279/// * `T` - The type to serialize/deserialize
280/// * `S` - The serialization closure type
281/// * `D` - The deserialization closure type
282///
283/// # Example
284///
285/// ```rust
286/// use durable_execution_sdk::serdes::{custom_serdes, SerDes, SerDesContext, SerDesError};
287///
288/// // Create a custom serializer for strings that adds a prefix
289/// let serdes = custom_serdes::<String, _, _>(
290///     |value, _ctx| Ok(format!("PREFIX:{}", value)),
291///     |data, _ctx| {
292///         data.strip_prefix("PREFIX:")
293///             .map(|s| s.to_string())
294///             .ok_or_else(|| SerDesError::deserialization("Missing PREFIX"))
295///     },
296/// );
297/// ```
298pub 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
309// Implement Sealed for CustomSerDes to allow it to implement SerDes
310impl<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
344/// Creates a custom serializer/deserializer with user-provided closures.
345///
346/// This factory function allows users to create custom serialization behavior
347/// without implementing the sealed `SerDes` trait directly.
348///
349/// # Type Parameters
350///
351/// * `T` - The type to serialize/deserialize
352///
353/// # Arguments
354///
355/// * `serialize_fn` - Closure that serializes a value to a string
356/// * `deserialize_fn` - Closure that deserializes a string to a value
357///
358/// # Example
359///
360/// ```rust
361/// use durable_execution_sdk::serdes::{custom_serdes, SerDes, SerDesContext, SerDesError};
362///
363/// // Create a custom serializer for i32 that uses a simple format
364/// let serdes = custom_serdes::<i32, _, _>(
365///     |value, _ctx| Ok(value.to_string()),
366///     |data, _ctx| data.parse().map_err(|e| SerDesError::deserialization(format!("{}", e))),
367/// );
368///
369/// let context = SerDesContext::new("op-1", "arn:test");
370/// let serialized = serdes.serialize(&42, &context).unwrap();
371/// assert_eq!(serialized, "42");
372///
373/// let deserialized = serdes.deserialize("42", &context).unwrap();
374/// assert_eq!(deserialized, 42);
375/// ```
376pub 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        // Test with String
510        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        // Test with i32
518        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        // Test with Vec
525        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    /// Tests for sealed SerDes trait implementations.
533    ///
534    /// The SerDes trait is sealed, meaning it cannot be implemented outside this crate.
535    /// This is enforced at compile time by requiring the private `Sealed` supertrait.
536    /// External crates attempting to implement SerDes will get a compile error:
537    /// "the trait bound `MyType: Sealed` is not satisfied"
538    ///
539    /// These tests verify that the internal implementations work correctly.
540    mod sealed_serdes_tests {
541        use super::*;
542
543        #[test]
544        fn test_json_serdes_implements_serdes() {
545            // JsonSerDes should implement SerDes (compile-time check)
546            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            // CustomSerDes should implement SerDes (compile-time check)
557            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    /// **Feature: durable-execution-rust-sdk, Property 2: SerDes Round-Trip**
687    /// **Validates: Requirements 11.2**
688    ///
689    /// For any value T that implements Serialize + DeserializeOwned,
690    /// serializing then deserializing with JsonSerDes SHALL produce
691    /// a value equal to the original.
692    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            /// Property test: String round-trip
746            /// For any String, serialize then deserialize produces the original value.
747            #[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            /// Property test: i64 round-trip
756            /// For any i64, serialize then deserialize produces the original value.
757            #[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            /// Property test: f64 round-trip with approximate equality
766            /// For any finite f64, serialize then deserialize produces a value
767            /// that is approximately equal (within floating-point precision limits).
768            /// Note: JSON serialization may lose some precision for extreme values.
769            #[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                // Use relative epsilon comparison for floating-point values
776                // JSON may lose some precision, so we check if values are "close enough"
777                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            /// Property test: bool round-trip
792            /// For any bool, serialize then deserialize produces the original value.
793            #[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            /// Property test: Vec<i32> round-trip
802            /// For any Vec<i32>, serialize then deserialize produces the original value.
803            #[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            /// Property test: Option<String> round-trip
812            /// For any Option<String>, serialize then deserialize produces the original value.
813            #[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            /// Property test: ComplexData round-trip
822            /// For any ComplexData struct, serialize then deserialize produces the original value.
823            #[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            /// Property test: Nested structures round-trip
835            /// For any HashMap<String, Vec<i32>>, serialize then deserialize produces the original value.
836            #[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    /// **Feature: rust-sdk-test-suite, Property 5: Operation JSON Round-Trip**
847    /// **Validates: Requirements 2.6, 9.2**
848    ///
849    /// For any Operation instance with valid fields, serializing to JSON then
850    /// deserializing SHALL produce an equivalent Operation.
851    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        /// Strategy for generating valid OperationType values.
859        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        /// Strategy for generating valid OperationStatus values.
871        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        /// Strategy for generating valid operation ID strings.
885        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        /// Strategy for generating optional strings.
890        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        /// Strategy for generating optional result payloads (JSON strings).
895        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        /// Strategy for generating optional timestamps (positive i64 values).
907        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        /// Strategy for generating valid Operation instances with type-specific details.
915        fn operation_strategy() -> impl Strategy<Value = Operation> {
916            (
917                operation_id_strategy(),
918                operation_type_strategy(),
919                operation_status_strategy(),
920                optional_string_strategy(),    // parent_id
921                optional_string_strategy(),    // name
922                optional_string_strategy(),    // sub_type
923                optional_timestamp_strategy(), // start_timestamp
924                optional_timestamp_strategy(), // end_timestamp
925            )
926                .prop_flat_map(
927                    |(id, op_type, status, parent_id, name, sub_type, start_ts, end_ts)| {
928                        // Generate type-specific details based on operation type
929                        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            /// Property test: Operation JSON round-trip
1039            /// **Feature: rust-sdk-test-suite, Property 5: Operation JSON Round-Trip**
1040            /// **Validates: Requirements 2.6, 9.2**
1041            ///
1042            /// For any Operation instance with valid fields, serializing to JSON
1043            /// then deserializing SHALL produce an equivalent Operation.
1044            #[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                // Verify key fields are preserved
1050                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                // Verify type-specific details are preserved
1060                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    /// **Feature: rust-sdk-test-suite, Property 17: Timestamp Format Equivalence**
1103    /// **Validates: Requirements 9.3**
1104    ///
1105    /// For any timestamp value (i64 milliseconds), deserializing from the integer
1106    /// format or from an equivalent ISO 8601 string SHALL produce the same result.
1107    mod timestamp_format_equivalence {
1108        use super::*;
1109        use crate::operation::Operation;
1110
1111        proptest! {
1112            #![proptest_config(ProptestConfig::with_cases(100))]
1113
1114            /// Property test: Timestamp integer format round-trip
1115            /// **Feature: rust-sdk-test-suite, Property 17: Timestamp Format Equivalence**
1116            /// **Validates: Requirements 9.3**
1117            ///
1118            /// For any timestamp value (i64 milliseconds), deserializing from integer
1119            /// format SHALL preserve the value.
1120            #[test]
1121            fn prop_timestamp_integer_round_trip(timestamp in 1000000000000i64..2000000000000i64) {
1122                // Create JSON with integer timestamp
1123                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            /// Property test: Timestamp ISO 8601 string format parsing
1133            /// **Feature: rust-sdk-test-suite, Property 17: Timestamp Format Equivalence**
1134            /// **Validates: Requirements 9.3**
1135            ///
1136            /// For any valid ISO 8601 datetime string, deserializing SHALL produce
1137            /// a valid timestamp in milliseconds.
1138            #[test]
1139            fn prop_timestamp_iso8601_parsing(
1140                year in 2020u32..2030u32,
1141                month in 1u32..=12u32,
1142                day in 1u32..=28u32,  // Use 28 to avoid month-end issues
1143                hour in 0u32..24u32,
1144                minute in 0u32..60u32,
1145                second in 0u32..60u32,
1146            ) {
1147                // Create ISO 8601 string
1148                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                // Verify the timestamp is reasonable (after year 2020)
1162                let ts = op.start_timestamp.unwrap();
1163                prop_assert!(ts > 1577836800000); // 2020-01-01 00:00:00 UTC in millis
1164            }
1165
1166            /// Property test: Floating point timestamp parsing
1167            /// **Feature: rust-sdk-test-suite, Property 17: Timestamp Format Equivalence**
1168            /// **Validates: Requirements 9.3**
1169            ///
1170            /// For any floating point timestamp (seconds with fractional milliseconds),
1171            /// deserializing SHALL convert to milliseconds correctly.
1172            ///
1173            /// Note: This test uses millis in 1..1000 range (not 0) because when millis=0,
1174            /// the float value (e.g., 1577836800.0) may be serialized without a decimal point
1175            /// and parsed as an integer by serde_json, which is correct behavior for integer
1176            /// timestamps (already in milliseconds).
1177            #[test]
1178            fn prop_timestamp_float_parsing(
1179                seconds in 1577836800i64..1893456000i64,  // 2020-2030 range
1180                millis in 1u32..1000u32,  // Start from 1 to ensure fractional part
1181            ) {
1182                // Create floating point timestamp (seconds.milliseconds)
1183                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                // The expected value in milliseconds
1194                let expected_millis = seconds * 1000 + millis as i64;
1195                let actual_millis = op.start_timestamp.unwrap();
1196
1197                // Allow for small rounding differences due to floating point
1198                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    /// **Feature: rust-sdk-test-suite, Property 16: OperationUpdate Serialization Validity**
1206    /// **Validates: Requirements 9.4**
1207    ///
1208    /// For any OperationUpdate instance, serialization SHALL produce valid JSON
1209    /// matching the API schema.
1210    mod operation_update_serialization {
1211        use super::*;
1212        use crate::error::ErrorObject;
1213        use crate::operation::{OperationAction, OperationType, OperationUpdate};
1214
1215        /// Strategy for generating valid OperationAction values.
1216        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        /// Strategy for generating valid OperationType values.
1227        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        /// Strategy for generating valid operation ID strings.
1239        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        /// Strategy for generating optional strings.
1244        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        /// Strategy for generating optional result payloads.
1249        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        /// Strategy for generating OperationUpdate instances.
1259        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(), // parent_id
1266                optional_string_strategy(), // name
1267            )
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            /// Property test: OperationUpdate serialization produces valid JSON
1299            /// **Feature: rust-sdk-test-suite, Property 16: OperationUpdate Serialization Validity**
1300            /// **Validates: Requirements 9.4**
1301            ///
1302            /// For any OperationUpdate instance, serialization SHALL produce valid JSON.
1303            #[test]
1304            fn prop_operation_update_serialization_valid(update in operation_update_strategy()) {
1305                // Serialization should succeed
1306                let json = serde_json::to_string(&update).unwrap();
1307
1308                // JSON should be parseable
1309                let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1310
1311                // Required fields should be present
1312                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                // Id should match
1317                prop_assert_eq!(
1318                    parsed.get("Id").unwrap().as_str().unwrap(),
1319                    &update.operation_id
1320                );
1321            }
1322
1323            /// Property test: OperationUpdate round-trip
1324            /// **Feature: rust-sdk-test-suite, Property 16: OperationUpdate Serialization Validity**
1325            /// **Validates: Requirements 9.4**
1326            ///
1327            /// For any OperationUpdate instance, serializing then deserializing
1328            /// SHALL produce an equivalent OperationUpdate.
1329            #[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                // Verify key fields are preserved
1335                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            /// Property test: OperationUpdate with WaitOptions
1344            /// **Feature: rust-sdk-test-suite, Property 16: OperationUpdate Serialization Validity**
1345            /// **Validates: Requirements 9.4**
1346            ///
1347            /// For any WAIT operation with WaitOptions, serialization SHALL include
1348            /// the WaitOptions field with correct structure.
1349            #[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                // WaitOptions should be present
1357                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}