use std::fmt;
use std::marker::PhantomData;
use serde::{de::DeserializeOwned, Serialize};
use crate::sealed::Sealed;
#[derive(Debug, Clone)]
pub struct SerDesError {
pub kind: SerDesErrorKind,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SerDesErrorKind {
Serialization,
Deserialization,
}
impl SerDesError {
pub fn serialization(message: impl Into<String>) -> Self {
Self {
kind: SerDesErrorKind::Serialization,
message: message.into(),
}
}
pub fn deserialization(message: impl Into<String>) -> Self {
Self {
kind: SerDesErrorKind::Deserialization,
message: message.into(),
}
}
}
impl fmt::Display for SerDesError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
SerDesErrorKind::Serialization => write!(f, "Serialization error: {}", self.message),
SerDesErrorKind::Deserialization => {
write!(f, "Deserialization error: {}", self.message)
}
}
}
}
impl std::error::Error for SerDesError {}
impl From<serde_json::Error> for SerDesError {
fn from(error: serde_json::Error) -> Self {
if error.is_io() || error.is_syntax() || error.is_data() {
Self::deserialization(error.to_string())
} else {
Self::serialization(error.to_string())
}
}
}
#[derive(Debug, Clone)]
pub struct SerDesContext {
pub operation_id: String,
pub durable_execution_arn: String,
}
impl SerDesContext {
pub fn new(operation_id: impl Into<String>, durable_execution_arn: impl Into<String>) -> Self {
Self {
operation_id: operation_id.into(),
durable_execution_arn: durable_execution_arn.into(),
}
}
}
#[allow(private_bounds)]
pub trait SerDes<T>: Sealed + Send + Sync {
fn serialize(&self, value: &T, context: &SerDesContext) -> Result<String, SerDesError>;
fn deserialize(&self, data: &str, context: &SerDesContext) -> Result<T, SerDesError>;
}
pub struct JsonSerDes<T> {
_marker: PhantomData<T>,
}
impl<T> Sealed for JsonSerDes<T> {}
impl<T> JsonSerDes<T> {
pub fn new() -> Self {
Self {
_marker: PhantomData,
}
}
}
impl<T> Default for JsonSerDes<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Clone for JsonSerDes<T> {
fn clone(&self) -> Self {
Self::new()
}
}
impl<T> fmt::Debug for JsonSerDes<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JsonSerDes").finish()
}
}
impl<T> SerDes<T> for JsonSerDes<T>
where
T: Serialize + DeserializeOwned,
{
fn serialize(&self, value: &T, _context: &SerDesContext) -> Result<String, SerDesError> {
serde_json::to_string(value).map_err(|e| SerDesError::serialization(e.to_string()))
}
fn deserialize(&self, data: &str, _context: &SerDesContext) -> Result<T, SerDesError> {
serde_json::from_str(data).map_err(|e| SerDesError::deserialization(e.to_string()))
}
}
unsafe impl<T> Send for JsonSerDes<T> {}
unsafe impl<T> Sync for JsonSerDes<T> {}
pub struct CustomSerDes<T, S, D>
where
T: Send + Sync,
S: Fn(&T, &SerDesContext) -> Result<String, SerDesError> + Send + Sync,
D: Fn(&str, &SerDesContext) -> Result<T, SerDesError> + Send + Sync,
{
serialize_fn: S,
deserialize_fn: D,
_marker: PhantomData<T>,
}
impl<T, S, D> Sealed for CustomSerDes<T, S, D>
where
T: Send + Sync,
S: Fn(&T, &SerDesContext) -> Result<String, SerDesError> + Send + Sync,
D: Fn(&str, &SerDesContext) -> Result<T, SerDesError> + Send + Sync,
{
}
impl<T, S, D> SerDes<T> for CustomSerDes<T, S, D>
where
T: Send + Sync,
S: Fn(&T, &SerDesContext) -> Result<String, SerDesError> + Send + Sync,
D: Fn(&str, &SerDesContext) -> Result<T, SerDesError> + Send + Sync,
{
fn serialize(&self, value: &T, context: &SerDesContext) -> Result<String, SerDesError> {
(self.serialize_fn)(value, context)
}
fn deserialize(&self, data: &str, context: &SerDesContext) -> Result<T, SerDesError> {
(self.deserialize_fn)(data, context)
}
}
impl<T, S, D> fmt::Debug for CustomSerDes<T, S, D>
where
T: Send + Sync,
S: Fn(&T, &SerDesContext) -> Result<String, SerDesError> + Send + Sync,
D: Fn(&str, &SerDesContext) -> Result<T, SerDesError> + Send + Sync,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CustomSerDes").finish()
}
}
pub fn custom_serdes<T, S, D>(serialize_fn: S, deserialize_fn: D) -> CustomSerDes<T, S, D>
where
T: Send + Sync,
S: Fn(&T, &SerDesContext) -> Result<String, SerDesError> + Send + Sync,
D: Fn(&str, &SerDesContext) -> Result<T, SerDesError> + Send + Sync,
{
CustomSerDes {
serialize_fn,
deserialize_fn,
_marker: PhantomData,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct TestData {
name: String,
value: i32,
}
fn create_test_context() -> SerDesContext {
SerDesContext::new(
"test-op-123",
"arn:aws:lambda:us-east-1:123456789:function:test",
)
}
#[test]
fn test_serdes_context_creation() {
let ctx = SerDesContext::new("op-1", "arn:test");
assert_eq!(ctx.operation_id, "op-1");
assert_eq!(ctx.durable_execution_arn, "arn:test");
}
#[test]
fn test_serdes_error_serialization() {
let error = SerDesError::serialization("failed to serialize");
assert_eq!(error.kind, SerDesErrorKind::Serialization);
assert!(error.to_string().contains("Serialization error"));
}
#[test]
fn test_serdes_error_deserialization() {
let error = SerDesError::deserialization("failed to deserialize");
assert_eq!(error.kind, SerDesErrorKind::Deserialization);
assert!(error.to_string().contains("Deserialization error"));
}
#[test]
fn test_json_serdes_serialize() {
let serdes = JsonSerDes::<TestData>::new();
let context = create_test_context();
let data = TestData {
name: "test".to_string(),
value: 42,
};
let result = serdes.serialize(&data, &context).unwrap();
assert!(result.contains("\"name\":\"test\""));
assert!(result.contains("\"value\":42"));
}
#[test]
fn test_json_serdes_deserialize() {
let serdes = JsonSerDes::<TestData>::new();
let context = create_test_context();
let json = r#"{"name":"test","value":42}"#;
let result = serdes.deserialize(json, &context).unwrap();
assert_eq!(result.name, "test");
assert_eq!(result.value, 42);
}
#[test]
fn test_json_serdes_round_trip() {
let serdes = JsonSerDes::<TestData>::new();
let context = create_test_context();
let original = TestData {
name: "round-trip".to_string(),
value: 123,
};
let serialized = serdes.serialize(&original, &context).unwrap();
let deserialized = serdes.deserialize(&serialized, &context).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_json_serdes_deserialize_invalid() {
let serdes = JsonSerDes::<TestData>::new();
let context = create_test_context();
let invalid_json = "not valid json";
let result = serdes.deserialize(invalid_json, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind, SerDesErrorKind::Deserialization);
}
#[test]
fn test_json_serdes_default() {
let serdes: JsonSerDes<TestData> = JsonSerDes::default();
let context = create_test_context();
let data = TestData {
name: "default".to_string(),
value: 1,
};
let result = serdes.serialize(&data, &context);
assert!(result.is_ok());
}
#[test]
fn test_json_serdes_clone() {
let serdes = JsonSerDes::<TestData>::new();
let cloned = serdes.clone();
let context = create_test_context();
let data = TestData {
name: "clone".to_string(),
value: 2,
};
let result1 = serdes.serialize(&data, &context).unwrap();
let result2 = cloned.serialize(&data, &context).unwrap();
assert_eq!(result1, result2);
}
#[test]
fn test_json_serdes_primitive_types() {
let string_serdes = JsonSerDes::<String>::new();
let context = create_test_context();
let original = "hello world".to_string();
let serialized = string_serdes.serialize(&original, &context).unwrap();
let deserialized: String = string_serdes.deserialize(&serialized, &context).unwrap();
assert_eq!(original, deserialized);
let int_serdes = JsonSerDes::<i32>::new();
let original = 42i32;
let serialized = int_serdes.serialize(&original, &context).unwrap();
let deserialized: i32 = int_serdes.deserialize(&serialized, &context).unwrap();
assert_eq!(original, deserialized);
let vec_serdes = JsonSerDes::<Vec<i32>>::new();
let original = vec![1, 2, 3, 4, 5];
let serialized = vec_serdes.serialize(&original, &context).unwrap();
let deserialized: Vec<i32> = vec_serdes.deserialize(&serialized, &context).unwrap();
assert_eq!(original, deserialized);
}
mod sealed_serdes_tests {
use super::*;
#[test]
fn test_json_serdes_implements_serdes() {
let serdes: &dyn SerDes<String> = &JsonSerDes::<String>::new();
let context = create_test_context();
let serialized = serdes.serialize(&"test".to_string(), &context).unwrap();
let deserialized = serdes.deserialize(&serialized, &context).unwrap();
assert_eq!(deserialized, "test");
}
#[test]
fn test_custom_serdes_implements_serdes() {
let serdes = custom_serdes::<i32, _, _>(
|value, _ctx| Ok(value.to_string()),
|data, _ctx| {
data.parse()
.map_err(|e| SerDesError::deserialization(format!("{}", e)))
},
);
let serdes_ref: &dyn SerDes<i32> = &serdes;
let context = create_test_context();
let serialized = serdes_ref.serialize(&42, &context).unwrap();
assert_eq!(serialized, "42");
let deserialized = serdes_ref.deserialize("42", &context).unwrap();
assert_eq!(deserialized, 42);
}
#[test]
fn test_custom_serdes_round_trip() {
let serdes = custom_serdes::<String, _, _>(
|value, _ctx| Ok(format!("PREFIX:{}", value)),
|data, _ctx| {
data.strip_prefix("PREFIX:")
.map(|s| s.to_string())
.ok_or_else(|| SerDesError::deserialization("Missing PREFIX"))
},
);
let context = create_test_context();
let original = "hello world".to_string();
let serialized = serdes.serialize(&original, &context).unwrap();
assert_eq!(serialized, "PREFIX:hello world");
let deserialized = serdes.deserialize(&serialized, &context).unwrap();
assert_eq!(deserialized, original);
}
#[test]
fn test_custom_serdes_error_handling() {
let serdes = custom_serdes::<i32, _, _>(
|_value, _ctx| Err(SerDesError::serialization("intentional error")),
|_data, _ctx| Err(SerDesError::deserialization("intentional error")),
);
let context = create_test_context();
let serialize_result = serdes.serialize(&42, &context);
assert!(serialize_result.is_err());
assert_eq!(
serialize_result.unwrap_err().kind,
SerDesErrorKind::Serialization
);
let deserialize_result = serdes.deserialize("42", &context);
assert!(deserialize_result.is_err());
assert_eq!(
deserialize_result.unwrap_err().kind,
SerDesErrorKind::Deserialization
);
}
#[test]
fn test_custom_serdes_receives_context() {
use std::sync::atomic::{AtomicBool, Ordering};
let context_received = std::sync::Arc::new(AtomicBool::new(false));
let context_clone = context_received.clone();
let serdes = custom_serdes::<String, _, _>(
move |value, ctx| {
assert_eq!(ctx.operation_id, "test-op-123");
assert!(ctx.durable_execution_arn.contains("lambda"));
context_clone.store(true, Ordering::SeqCst);
Ok(value.clone())
},
|data, _ctx| Ok(data.to_string()),
);
let context = create_test_context();
let _ = serdes.serialize(&"test".to_string(), &context);
assert!(context_received.load(Ordering::SeqCst));
}
#[test]
fn test_custom_serdes_with_complex_type() {
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
let serdes = custom_serdes::<Point, _, _>(
|point, _ctx| Ok(format!("{},{}", point.x, point.y)),
|data, _ctx| {
let parts: Vec<&str> = data.split(',').collect();
if parts.len() != 2 {
return Err(SerDesError::deserialization("Invalid format"));
}
let x = parts[0]
.parse()
.map_err(|_| SerDesError::deserialization("Invalid x"))?;
let y = parts[1]
.parse()
.map_err(|_| SerDesError::deserialization("Invalid y"))?;
Ok(Point { x, y })
},
);
let context = create_test_context();
let original = Point { x: 10, y: 20 };
let serialized = serdes.serialize(&original, &context).unwrap();
assert_eq!(serialized, "10,20");
let deserialized = serdes.deserialize(&serialized, &context).unwrap();
assert_eq!(deserialized, original);
}
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
use serde::{Deserialize, Serialize};
mod serdes_round_trip {
use super::*;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct ComplexData {
string_field: String,
int_field: i64,
bool_field: bool,
optional_field: Option<String>,
vec_field: Vec<i32>,
}
fn arbitrary_context() -> impl Strategy<Value = SerDesContext> {
(any::<String>(), any::<String>()).prop_map(|(op_id, arn)| {
SerDesContext::new(
if op_id.is_empty() {
"default-op".to_string()
} else {
op_id
},
if arn.is_empty() {
"arn:default".to_string()
} else {
arn
},
)
})
}
fn arbitrary_complex_data() -> impl Strategy<Value = ComplexData> {
(
any::<String>(),
any::<i64>(),
any::<bool>(),
any::<Option<String>>(),
any::<Vec<i32>>(),
)
.prop_map(
|(string_field, int_field, bool_field, optional_field, vec_field)| {
ComplexData {
string_field,
int_field,
bool_field,
optional_field,
vec_field,
}
},
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_string_round_trip(value: String, context in arbitrary_context()) {
let serdes = JsonSerDes::<String>::new();
let serialized = serdes.serialize(&value, &context).unwrap();
let deserialized = serdes.deserialize(&serialized, &context).unwrap();
prop_assert_eq!(value, deserialized);
}
#[test]
fn prop_i64_round_trip(value: i64, context in arbitrary_context()) {
let serdes = JsonSerDes::<i64>::new();
let serialized = serdes.serialize(&value, &context).unwrap();
let deserialized = serdes.deserialize(&serialized, &context).unwrap();
prop_assert_eq!(value, deserialized);
}
#[test]
fn prop_f64_round_trip(value in any::<f64>().prop_filter("finite", |v| v.is_finite()), context in arbitrary_context()) {
let serdes = JsonSerDes::<f64>::new();
let serialized = serdes.serialize(&value, &context).unwrap();
let deserialized: f64 = serdes.deserialize(&serialized, &context).unwrap();
let epsilon = 1e-10;
let diff = (value - deserialized).abs();
let relative_diff = if value.abs() > epsilon {
diff / value.abs()
} else {
diff
};
prop_assert!(
relative_diff < epsilon,
"f64 round-trip failed: original={}, deserialized={}, relative_diff={}",
value, deserialized, relative_diff
);
}
#[test]
fn prop_bool_round_trip(value: bool, context in arbitrary_context()) {
let serdes = JsonSerDes::<bool>::new();
let serialized = serdes.serialize(&value, &context).unwrap();
let deserialized = serdes.deserialize(&serialized, &context).unwrap();
prop_assert_eq!(value, deserialized);
}
#[test]
fn prop_vec_round_trip(value: Vec<i32>, context in arbitrary_context()) {
let serdes = JsonSerDes::<Vec<i32>>::new();
let serialized = serdes.serialize(&value, &context).unwrap();
let deserialized = serdes.deserialize(&serialized, &context).unwrap();
prop_assert_eq!(value, deserialized);
}
#[test]
fn prop_option_round_trip(value: Option<String>, context in arbitrary_context()) {
let serdes = JsonSerDes::<Option<String>>::new();
let serialized = serdes.serialize(&value, &context).unwrap();
let deserialized = serdes.deserialize(&serialized, &context).unwrap();
prop_assert_eq!(value, deserialized);
}
#[test]
fn prop_complex_data_round_trip(
value in arbitrary_complex_data(),
context in arbitrary_context()
) {
let serdes = JsonSerDes::<ComplexData>::new();
let serialized = serdes.serialize(&value, &context).unwrap();
let deserialized = serdes.deserialize(&serialized, &context).unwrap();
prop_assert_eq!(value, deserialized);
}
#[test]
fn prop_nested_round_trip(value: std::collections::HashMap<String, Vec<i32>>, context in arbitrary_context()) {
let serdes = JsonSerDes::<std::collections::HashMap<String, Vec<i32>>>::new();
let serialized = serdes.serialize(&value, &context).unwrap();
let deserialized = serdes.deserialize(&serialized, &context).unwrap();
prop_assert_eq!(value, deserialized);
}
}
}
mod operation_round_trip {
use super::*;
use crate::operation::{
CallbackDetails, ChainedInvokeDetails, ContextDetails, ExecutionDetails, Operation,
OperationStatus, OperationType, StepDetails, WaitDetails,
};
fn operation_type_strategy() -> impl Strategy<Value = OperationType> {
prop_oneof![
Just(OperationType::Execution),
Just(OperationType::Step),
Just(OperationType::Wait),
Just(OperationType::Callback),
Just(OperationType::Invoke),
Just(OperationType::Context),
]
}
fn operation_status_strategy() -> impl Strategy<Value = OperationStatus> {
prop_oneof![
Just(OperationStatus::Started),
Just(OperationStatus::Pending),
Just(OperationStatus::Ready),
Just(OperationStatus::Succeeded),
Just(OperationStatus::Failed),
Just(OperationStatus::Cancelled),
Just(OperationStatus::TimedOut),
Just(OperationStatus::Stopped),
]
}
fn operation_id_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z][a-zA-Z0-9_-]{0,63}".prop_map(|s| s)
}
fn optional_string_strategy() -> impl Strategy<Value = Option<String>> {
prop_oneof![Just(None), "[a-zA-Z0-9_-]{1,32}".prop_map(Some),]
}
fn optional_result_strategy() -> impl Strategy<Value = Option<String>> {
prop_oneof![
Just(None),
Just(Some("null".to_string())),
Just(Some("42".to_string())),
Just(Some("\"test-result\"".to_string())),
Just(Some("{\"key\":\"value\"}".to_string())),
Just(Some("[1,2,3]".to_string())),
]
}
fn optional_timestamp_strategy() -> impl Strategy<Value = Option<i64>> {
prop_oneof![
Just(None),
(1000000000000i64..2000000000000i64).prop_map(Some),
]
}
fn operation_strategy() -> impl Strategy<Value = Operation> {
(
operation_id_strategy(),
operation_type_strategy(),
operation_status_strategy(),
optional_string_strategy(), optional_string_strategy(), optional_string_strategy(), optional_timestamp_strategy(), optional_timestamp_strategy(), )
.prop_flat_map(
|(id, op_type, status, parent_id, name, sub_type, start_ts, end_ts)| {
let details_strategy = match op_type {
OperationType::Step => optional_result_strategy()
.prop_map(move |result| {
let mut op = Operation::new(id.clone(), op_type);
op.status = status;
op.parent_id = parent_id.clone();
op.name = name.clone();
op.sub_type = sub_type.clone();
op.start_timestamp = start_ts;
op.end_timestamp = end_ts;
op.step_details = Some(StepDetails {
result,
attempt: Some(0),
next_attempt_timestamp: None,
error: None,
payload: None,
});
op
})
.boxed(),
OperationType::Wait => Just(())
.prop_map(move |_| {
let mut op = Operation::new(id.clone(), op_type);
op.status = status;
op.parent_id = parent_id.clone();
op.name = name.clone();
op.sub_type = sub_type.clone();
op.start_timestamp = start_ts;
op.end_timestamp = end_ts;
op.wait_details = Some(WaitDetails {
scheduled_end_timestamp: Some(1234567890000),
});
op
})
.boxed(),
OperationType::Callback => optional_result_strategy()
.prop_map(move |result| {
let mut op = Operation::new(id.clone(), op_type);
op.status = status;
op.parent_id = parent_id.clone();
op.name = name.clone();
op.sub_type = sub_type.clone();
op.start_timestamp = start_ts;
op.end_timestamp = end_ts;
op.callback_details = Some(CallbackDetails {
callback_id: Some(format!("cb-{}", id.clone())),
result,
error: None,
});
op
})
.boxed(),
OperationType::Invoke => optional_result_strategy()
.prop_map(move |result| {
let mut op = Operation::new(id.clone(), op_type);
op.status = status;
op.parent_id = parent_id.clone();
op.name = name.clone();
op.sub_type = sub_type.clone();
op.start_timestamp = start_ts;
op.end_timestamp = end_ts;
op.chained_invoke_details = Some(ChainedInvokeDetails {
result,
error: None,
});
op
})
.boxed(),
OperationType::Context => optional_result_strategy()
.prop_map(move |result| {
let mut op = Operation::new(id.clone(), op_type);
op.status = status;
op.parent_id = parent_id.clone();
op.name = name.clone();
op.sub_type = sub_type.clone();
op.start_timestamp = start_ts;
op.end_timestamp = end_ts;
op.context_details = Some(ContextDetails {
result,
replay_children: Some(true),
error: None,
});
op
})
.boxed(),
OperationType::Execution => optional_result_strategy()
.prop_map(move |input| {
let mut op = Operation::new(id.clone(), op_type);
op.status = status;
op.parent_id = parent_id.clone();
op.name = name.clone();
op.sub_type = sub_type.clone();
op.start_timestamp = start_ts;
op.end_timestamp = end_ts;
op.execution_details = Some(ExecutionDetails {
input_payload: input,
});
op
})
.boxed(),
};
details_strategy
},
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_operation_json_round_trip(op in operation_strategy()) {
let json = serde_json::to_string(&op).unwrap();
let deserialized: Operation = serde_json::from_str(&json).unwrap();
prop_assert_eq!(&op.operation_id, &deserialized.operation_id);
prop_assert_eq!(op.operation_type, deserialized.operation_type);
prop_assert_eq!(op.status, deserialized.status);
prop_assert_eq!(&op.parent_id, &deserialized.parent_id);
prop_assert_eq!(&op.name, &deserialized.name);
prop_assert_eq!(&op.sub_type, &deserialized.sub_type);
prop_assert_eq!(op.start_timestamp, deserialized.start_timestamp);
prop_assert_eq!(op.end_timestamp, deserialized.end_timestamp);
match op.operation_type {
OperationType::Step => {
prop_assert!(deserialized.step_details.is_some());
let orig = op.step_details.as_ref().unwrap();
let deser = deserialized.step_details.as_ref().unwrap();
prop_assert_eq!(&orig.result, &deser.result);
prop_assert_eq!(orig.attempt, deser.attempt);
}
OperationType::Wait => {
prop_assert!(deserialized.wait_details.is_some());
}
OperationType::Callback => {
prop_assert!(deserialized.callback_details.is_some());
let orig = op.callback_details.as_ref().unwrap();
let deser = deserialized.callback_details.as_ref().unwrap();
prop_assert_eq!(&orig.callback_id, &deser.callback_id);
prop_assert_eq!(&orig.result, &deser.result);
}
OperationType::Invoke => {
prop_assert!(deserialized.chained_invoke_details.is_some());
let orig = op.chained_invoke_details.as_ref().unwrap();
let deser = deserialized.chained_invoke_details.as_ref().unwrap();
prop_assert_eq!(&orig.result, &deser.result);
}
OperationType::Context => {
prop_assert!(deserialized.context_details.is_some());
let orig = op.context_details.as_ref().unwrap();
let deser = deserialized.context_details.as_ref().unwrap();
prop_assert_eq!(&orig.result, &deser.result);
prop_assert_eq!(orig.replay_children, deser.replay_children);
}
OperationType::Execution => {
prop_assert!(deserialized.execution_details.is_some());
let orig = op.execution_details.as_ref().unwrap();
let deser = deserialized.execution_details.as_ref().unwrap();
prop_assert_eq!(&orig.input_payload, &deser.input_payload);
}
}
}
}
}
mod timestamp_format_equivalence {
use super::*;
use crate::operation::Operation;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_timestamp_integer_round_trip(timestamp in 1000000000000i64..2000000000000i64) {
let json = format!(
r#"{{"Id":"test-op","Type":"STEP","Status":"STARTED","StartTimestamp":{}}}"#,
timestamp
);
let op: Operation = serde_json::from_str(&json).unwrap();
prop_assert_eq!(op.start_timestamp, Some(timestamp));
}
#[test]
fn prop_timestamp_iso8601_parsing(
year in 2020u32..2030u32,
month in 1u32..=12u32,
day in 1u32..=28u32, hour in 0u32..24u32,
minute in 0u32..60u32,
second in 0u32..60u32,
) {
let iso_string = format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}+00:00",
year, month, day, hour, minute, second
);
let json = format!(
r#"{{"Id":"test-op","Type":"STEP","Status":"STARTED","StartTimestamp":"{}"}}"#,
iso_string
);
let op: Operation = serde_json::from_str(&json).unwrap();
prop_assert!(op.start_timestamp.is_some());
let ts = op.start_timestamp.unwrap();
prop_assert!(ts > 1577836800000); }
#[test]
fn prop_timestamp_float_parsing(
seconds in 1577836800i64..1893456000i64, millis in 1u32..1000u32, ) {
let float_ts = seconds as f64 + (millis as f64 / 1000.0);
let json = format!(
r#"{{"Id":"test-op","Type":"STEP","Status":"STARTED","StartTimestamp":{}}}"#,
float_ts
);
let op: Operation = serde_json::from_str(&json).unwrap();
prop_assert!(op.start_timestamp.is_some());
let expected_millis = seconds * 1000 + millis as i64;
let actual_millis = op.start_timestamp.unwrap();
let diff = (expected_millis - actual_millis).abs();
prop_assert!(diff <= 1, "Timestamp difference too large: expected {}, got {}, diff {}",
expected_millis, actual_millis, diff);
}
}
}
mod operation_update_serialization {
use super::*;
use crate::error::ErrorObject;
use crate::operation::{OperationAction, OperationType, OperationUpdate};
fn operation_action_strategy() -> impl Strategy<Value = OperationAction> {
prop_oneof![
Just(OperationAction::Start),
Just(OperationAction::Succeed),
Just(OperationAction::Fail),
Just(OperationAction::Cancel),
Just(OperationAction::Retry),
]
}
fn operation_type_strategy() -> impl Strategy<Value = OperationType> {
prop_oneof![
Just(OperationType::Execution),
Just(OperationType::Step),
Just(OperationType::Wait),
Just(OperationType::Callback),
Just(OperationType::Invoke),
Just(OperationType::Context),
]
}
fn operation_id_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z][a-zA-Z0-9_-]{0,63}".prop_map(|s| s)
}
fn optional_string_strategy() -> impl Strategy<Value = Option<String>> {
prop_oneof![Just(None), "[a-zA-Z0-9_-]{1,32}".prop_map(Some),]
}
fn optional_result_strategy() -> impl Strategy<Value = Option<String>> {
prop_oneof![
Just(None),
Just(Some("null".to_string())),
Just(Some("42".to_string())),
Just(Some("\"test\"".to_string())),
]
}
fn operation_update_strategy() -> impl Strategy<Value = OperationUpdate> {
(
operation_id_strategy(),
operation_action_strategy(),
operation_type_strategy(),
optional_result_strategy(),
optional_string_strategy(), optional_string_strategy(), )
.prop_map(|(id, action, op_type, result, parent_id, name)| {
let mut update = match action {
OperationAction::Start => {
if op_type == OperationType::Wait {
OperationUpdate::start_wait(&id, 60)
} else {
OperationUpdate::start(&id, op_type)
}
}
OperationAction::Succeed => {
OperationUpdate::succeed(&id, op_type, result.clone())
}
OperationAction::Fail => {
let err = ErrorObject::new("TestError", "Test error message");
OperationUpdate::fail(&id, op_type, err)
}
OperationAction::Cancel => OperationUpdate::cancel(&id, op_type),
OperationAction::Retry => {
OperationUpdate::retry(&id, op_type, result.clone(), None)
}
};
update.parent_id = parent_id;
update.name = name;
update
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_operation_update_serialization_valid(update in operation_update_strategy()) {
let json = serde_json::to_string(&update).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
prop_assert!(parsed.get("Id").is_some(), "Missing Id field");
prop_assert!(parsed.get("Action").is_some(), "Missing Action field");
prop_assert!(parsed.get("Type").is_some(), "Missing Type field");
prop_assert_eq!(
parsed.get("Id").unwrap().as_str().unwrap(),
&update.operation_id
);
}
#[test]
fn prop_operation_update_round_trip(update in operation_update_strategy()) {
let json = serde_json::to_string(&update).unwrap();
let deserialized: OperationUpdate = serde_json::from_str(&json).unwrap();
prop_assert_eq!(&update.operation_id, &deserialized.operation_id);
prop_assert_eq!(update.action, deserialized.action);
prop_assert_eq!(update.operation_type, deserialized.operation_type);
prop_assert_eq!(&update.result, &deserialized.result);
prop_assert_eq!(&update.parent_id, &deserialized.parent_id);
prop_assert_eq!(&update.name, &deserialized.name);
}
#[test]
fn prop_wait_options_serialization(wait_seconds in 1u64..86400u64) {
let update = OperationUpdate::start_wait("test-wait", wait_seconds);
let json = serde_json::to_string(&update).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
prop_assert!(parsed.get("WaitOptions").is_some(), "Missing WaitOptions field");
let wait_opts = parsed.get("WaitOptions").unwrap();
prop_assert_eq!(
wait_opts.get("WaitSeconds").unwrap().as_u64().unwrap(),
wait_seconds
);
}
}
}
}