Skip to main content

durable_execution_sdk/
types.rs

1//! Newtype wrappers for domain identifiers in the AWS Durable Execution SDK.
2//!
3//! This module provides type-safe wrappers for string identifiers used throughout
4//! the SDK. These newtypes prevent accidental mixing of different ID types at
5//! compile time while maintaining full compatibility with string-based APIs.
6//!
7//! # Example
8//!
9//! ```rust
10//! use durable_execution_sdk::types::{OperationId, ExecutionArn, CallbackId};
11//!
12//! // Create from String or &str (no validation)
13//! let op_id = OperationId::from("op-123");
14//! let op_id2: OperationId = "op-456".into();
15//!
16//! // Create with validation
17//! let op_id3 = OperationId::new("op-789").unwrap();
18//! assert!(OperationId::new("").is_err()); // Empty strings rejected
19//!
20//! // Use as string via Deref
21//! assert!(op_id.starts_with("op-"));
22//!
23//! // Use in HashMap (implements Hash, Eq)
24//! use std::collections::HashMap;
25//! let mut map: HashMap<OperationId, String> = HashMap::new();
26//! map.insert(op_id, "value".to_string());
27//! ```
28
29use std::fmt;
30use std::hash::Hash;
31use std::ops::Deref;
32
33use serde::{Deserialize, Serialize};
34
35/// Error returned when newtype validation fails.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ValidationError {
38    /// The type name that failed validation
39    pub type_name: &'static str,
40    /// Description of the validation failure
41    pub message: String,
42}
43
44impl fmt::Display for ValidationError {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "{}: {}", self.type_name, self.message)
47    }
48}
49
50impl std::error::Error for ValidationError {}
51
52/// A unique identifier for an operation within a durable execution.
53///
54/// `OperationId` wraps a `String` to provide type safety, preventing accidental
55/// mixing with other string-based identifiers like `ExecutionArn` or `CallbackId`.
56///
57/// # Construction
58///
59/// ```rust
60/// use durable_execution_sdk::types::OperationId;
61///
62/// // From String (no validation)
63/// let id1 = OperationId::from("op-123".to_string());
64///
65/// // From &str (no validation)
66/// let id2 = OperationId::from("op-456");
67///
68/// // Using Into trait (no validation)
69/// let id3: OperationId = "op-789".into();
70///
71/// // With validation
72/// let id4 = OperationId::new("op-abc").unwrap();
73/// assert!(OperationId::new("").is_err()); // Empty strings rejected
74/// ```
75///
76/// # String Access
77///
78/// ```rust
79/// use durable_execution_sdk::types::OperationId;
80///
81/// let id = OperationId::from("op-123");
82///
83/// // Via Deref (automatic)
84/// assert!(id.starts_with("op-"));
85/// assert_eq!(id.len(), 6);
86///
87/// // Via AsRef
88/// let s: &str = id.as_ref();
89/// assert_eq!(s, "op-123");
90/// ```
91#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
92#[serde(transparent)]
93pub struct OperationId(String);
94
95impl OperationId {
96    /// Creates a new `OperationId` with validation.
97    ///
98    /// Returns an error if the value is empty.
99    pub fn new(id: impl Into<String>) -> Result<Self, ValidationError> {
100        let id = id.into();
101        if id.is_empty() {
102            return Err(ValidationError {
103                type_name: "OperationId",
104                message: "value cannot be empty".to_string(),
105            });
106        }
107        Ok(Self(id))
108    }
109
110    /// Creates a new `OperationId` without validation.
111    ///
112    /// Use this when you know the value is valid or when migrating
113    /// from existing code that uses raw strings.
114    #[inline]
115    pub fn new_unchecked(id: impl Into<String>) -> Self {
116        Self(id.into())
117    }
118
119    /// Returns the inner string value.
120    #[inline]
121    pub fn into_inner(self) -> String {
122        self.0
123    }
124
125    /// Returns a reference to the inner string.
126    #[inline]
127    pub fn as_str(&self) -> &str {
128        &self.0
129    }
130}
131
132impl fmt::Display for OperationId {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "{}", self.0)
135    }
136}
137
138impl Deref for OperationId {
139    type Target = str;
140
141    #[inline]
142    fn deref(&self) -> &Self::Target {
143        &self.0
144    }
145}
146
147impl AsRef<str> for OperationId {
148    #[inline]
149    fn as_ref(&self) -> &str {
150        &self.0
151    }
152}
153
154impl From<String> for OperationId {
155    #[inline]
156    fn from(s: String) -> Self {
157        Self(s)
158    }
159}
160
161impl From<&str> for OperationId {
162    #[inline]
163    fn from(s: &str) -> Self {
164        Self(s.to_string())
165    }
166}
167
168/// The Amazon Resource Name identifying a durable execution.
169///
170/// `ExecutionArn` wraps a `String` to provide type safety for execution ARNs.
171/// It includes validation to ensure the ARN follows the expected format.
172///
173/// # ARN Format
174///
175/// A valid durable execution ARN follows the pattern:
176/// `arn:<partition>:lambda:<region>:<account>:function:<function-name>:durable:<execution-id>`
177///
178/// Supported partitions include: `aws`, `aws-cn`, `aws-us-gov`, `aws-iso`, `aws-iso-b`, etc.
179///
180/// # Construction
181///
182/// ```rust
183/// use durable_execution_sdk::types::ExecutionArn;
184///
185/// // From String (no validation)
186/// let arn1 = ExecutionArn::from("arn:aws:lambda:us-east-1:123456789012:function:my-func:durable:abc123".to_string());
187///
188/// // With validation
189/// let arn2 = ExecutionArn::new("arn:aws:lambda:us-east-1:123456789012:function:my-func:durable:abc123");
190/// assert!(arn2.is_ok());
191///
192/// // China region
193/// let arn_cn = ExecutionArn::new("arn:aws-cn:lambda:cn-north-1:123456789012:function:my-func:durable:abc123");
194/// assert!(arn_cn.is_ok());
195///
196/// // Invalid ARN rejected
197/// assert!(ExecutionArn::new("").is_err());
198/// assert!(ExecutionArn::new("not-an-arn").is_err());
199/// ```
200///
201/// # String Access
202///
203/// ```rust
204/// use durable_execution_sdk::types::ExecutionArn;
205///
206/// let arn = ExecutionArn::from("arn:aws:lambda:us-east-1:123456789012:function:my-func:durable:abc123");
207///
208/// // Via Deref (automatic)
209/// assert!(arn.starts_with("arn:"));
210///
211/// // Via AsRef
212/// let s: &str = arn.as_ref();
213/// ```
214#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
215#[serde(transparent)]
216pub struct ExecutionArn(String);
217
218impl ExecutionArn {
219    /// Creates a new `ExecutionArn` with validation.
220    ///
221    /// Returns an error if the value is empty, doesn't start with "arn:aws:lambda:",
222    /// or doesn't contain ":durable:".
223    pub fn new(arn: impl Into<String>) -> Result<Self, ValidationError> {
224        let arn = arn.into();
225        Self::validate(&arn)?;
226        Ok(Self(arn))
227    }
228
229    /// Creates a new `ExecutionArn` without validation.
230    ///
231    /// Use this when you know the value is valid or when migrating
232    /// from existing code that uses raw strings.
233    #[inline]
234    pub fn new_unchecked(arn: impl Into<String>) -> Self {
235        Self(arn.into())
236    }
237
238    /// Returns the inner string value.
239    #[inline]
240    pub fn into_inner(self) -> String {
241        self.0
242    }
243
244    /// Returns a reference to the inner string.
245    #[inline]
246    pub fn as_str(&self) -> &str {
247        &self.0
248    }
249
250    /// Validates that the string is a valid durable execution ARN format.
251    ///
252    /// A valid ARN should:
253    /// - Not be empty
254    /// - Start with "arn:" followed by a valid AWS partition (aws, aws-cn, aws-us-gov, etc.)
255    /// - Contain ":lambda:" service identifier
256    /// - Contain ":durable:" segment
257    fn validate(value: &str) -> Result<(), ValidationError> {
258        if value.is_empty() {
259            return Err(ValidationError {
260                type_name: "ExecutionArn",
261                message: "value cannot be empty".to_string(),
262            });
263        }
264
265        // Check for valid ARN prefix with any AWS partition
266        // Valid partitions: aws, aws-cn, aws-us-gov, aws-iso, aws-iso-b, etc.
267        if !value.starts_with("arn:") {
268            return Err(ValidationError {
269                type_name: "ExecutionArn",
270                message: "must start with 'arn:'".to_string(),
271            });
272        }
273
274        // Check for lambda service
275        if !value.contains(":lambda:") {
276            return Err(ValidationError {
277                type_name: "ExecutionArn",
278                message: "must contain ':lambda:' service identifier".to_string(),
279            });
280        }
281
282        if !value.contains(":durable:") {
283            return Err(ValidationError {
284                type_name: "ExecutionArn",
285                message: "must contain ':durable:' segment".to_string(),
286            });
287        }
288
289        Ok(())
290    }
291}
292
293impl fmt::Display for ExecutionArn {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        write!(f, "{}", self.0)
296    }
297}
298
299impl Deref for ExecutionArn {
300    type Target = str;
301
302    #[inline]
303    fn deref(&self) -> &Self::Target {
304        &self.0
305    }
306}
307
308impl AsRef<str> for ExecutionArn {
309    #[inline]
310    fn as_ref(&self) -> &str {
311        &self.0
312    }
313}
314
315impl From<String> for ExecutionArn {
316    #[inline]
317    fn from(s: String) -> Self {
318        Self(s)
319    }
320}
321
322impl From<&str> for ExecutionArn {
323    #[inline]
324    fn from(s: &str) -> Self {
325        Self(s.to_string())
326    }
327}
328
329/// A unique identifier for a callback operation.
330///
331/// `CallbackId` wraps a `String` to provide type safety for callback identifiers.
332/// Callback IDs are used by external systems to signal completion of asynchronous
333/// operations.
334///
335/// # Construction
336///
337/// ```rust
338/// use durable_execution_sdk::types::CallbackId;
339///
340/// // From String (no validation)
341/// let id1 = CallbackId::from("callback-123".to_string());
342///
343/// // From &str (no validation)
344/// let id2 = CallbackId::from("callback-456");
345///
346/// // With validation
347/// let id3 = CallbackId::new("callback-abc").unwrap();
348/// assert!(CallbackId::new("").is_err()); // Empty strings rejected
349/// ```
350///
351/// # String Access
352///
353/// ```rust
354/// use durable_execution_sdk::types::CallbackId;
355///
356/// let id = CallbackId::from("callback-123");
357///
358/// // Via Deref (automatic)
359/// assert!(id.starts_with("callback-"));
360///
361/// // Via AsRef
362/// let s: &str = id.as_ref();
363/// assert_eq!(s, "callback-123");
364/// ```
365#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
366#[serde(transparent)]
367pub struct CallbackId(String);
368
369impl CallbackId {
370    /// Creates a new `CallbackId` with validation.
371    ///
372    /// Returns an error if the value is empty.
373    pub fn new(id: impl Into<String>) -> Result<Self, ValidationError> {
374        let id = id.into();
375        if id.is_empty() {
376            return Err(ValidationError {
377                type_name: "CallbackId",
378                message: "value cannot be empty".to_string(),
379            });
380        }
381        Ok(Self(id))
382    }
383
384    /// Creates a new `CallbackId` without validation.
385    ///
386    /// Use this when you know the value is valid or when migrating
387    /// from existing code that uses raw strings.
388    #[inline]
389    pub fn new_unchecked(id: impl Into<String>) -> Self {
390        Self(id.into())
391    }
392
393    /// Returns the inner string value.
394    #[inline]
395    pub fn into_inner(self) -> String {
396        self.0
397    }
398
399    /// Returns a reference to the inner string.
400    #[inline]
401    pub fn as_str(&self) -> &str {
402        &self.0
403    }
404}
405
406impl fmt::Display for CallbackId {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        write!(f, "{}", self.0)
409    }
410}
411
412impl Deref for CallbackId {
413    type Target = str;
414
415    #[inline]
416    fn deref(&self) -> &Self::Target {
417        &self.0
418    }
419}
420
421impl AsRef<str> for CallbackId {
422    #[inline]
423    fn as_ref(&self) -> &str {
424        &self.0
425    }
426}
427
428impl From<String> for CallbackId {
429    #[inline]
430    fn from(s: String) -> Self {
431        Self(s)
432    }
433}
434
435impl From<&str> for CallbackId {
436    #[inline]
437    fn from(s: &str) -> Self {
438        Self(s.to_string())
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445    use std::collections::HashMap;
446
447    // ==================== OperationId Tests ====================
448
449    #[test]
450    fn test_operation_id_from_string() {
451        let id = OperationId::from("op-123".to_string());
452        assert_eq!(id.as_str(), "op-123");
453    }
454
455    #[test]
456    fn test_operation_id_from_str() {
457        let id = OperationId::from("op-456");
458        assert_eq!(id.as_str(), "op-456");
459    }
460
461    #[test]
462    fn test_operation_id_into() {
463        let id: OperationId = "op-789".into();
464        assert_eq!(id.as_str(), "op-789");
465    }
466
467    #[test]
468    fn test_operation_id_new_valid() {
469        let id = OperationId::new("op-abc").unwrap();
470        assert_eq!(id.as_str(), "op-abc");
471    }
472
473    #[test]
474    fn test_operation_id_new_empty_rejected() {
475        let result = OperationId::new("");
476        assert!(result.is_err());
477        let err = result.unwrap_err();
478        assert_eq!(err.type_name, "OperationId");
479        assert!(err.message.contains("empty"));
480    }
481
482    #[test]
483    fn test_operation_id_display() {
484        let id = OperationId::from("op-display");
485        assert_eq!(format!("{}", id), "op-display");
486    }
487
488    #[test]
489    fn test_operation_id_debug() {
490        let id = OperationId::from("op-debug");
491        let debug_str = format!("{:?}", id);
492        assert!(debug_str.contains("op-debug"));
493    }
494
495    #[test]
496    fn test_operation_id_deref() {
497        let id = OperationId::from("op-deref-test");
498        assert!(id.starts_with("op-"));
499        assert_eq!(id.len(), 13);
500    }
501
502    #[test]
503    fn test_operation_id_as_ref() {
504        let id = OperationId::from("op-asref");
505        let s: &str = id.as_ref();
506        assert_eq!(s, "op-asref");
507    }
508
509    #[test]
510    fn test_operation_id_hash_and_eq() {
511        let id1 = OperationId::from("op-hash");
512        let id2 = OperationId::from("op-hash");
513        let id3 = OperationId::from("op-different");
514
515        assert_eq!(id1, id2);
516        assert_ne!(id1, id3);
517
518        let mut map: HashMap<OperationId, String> = HashMap::new();
519        map.insert(id1.clone(), "value1".to_string());
520        assert_eq!(map.get(&id2), Some(&"value1".to_string()));
521        assert_eq!(map.get(&id3), None);
522    }
523
524    #[test]
525    fn test_operation_id_serde_roundtrip() {
526        let id = OperationId::from("op-serde-test");
527        let json = serde_json::to_string(&id).unwrap();
528        assert_eq!(json, "\"op-serde-test\"");
529
530        let deserialized: OperationId = serde_json::from_str(&json).unwrap();
531        assert_eq!(deserialized, id);
532    }
533
534    #[test]
535    fn test_operation_id_into_inner() {
536        let id = OperationId::from("op-inner");
537        let inner = id.into_inner();
538        assert_eq!(inner, "op-inner");
539    }
540
541    // ==================== ExecutionArn Tests ====================
542
543    #[test]
544    fn test_execution_arn_from_string() {
545        let arn = ExecutionArn::from(
546            "arn:aws:lambda:us-east-1:123456789012:function:my-func:durable:abc123".to_string(),
547        );
548        assert!(arn.as_str().starts_with("arn:aws:lambda:"));
549    }
550
551    #[test]
552    fn test_execution_arn_from_str() {
553        let arn =
554            ExecutionArn::from("arn:aws:lambda:us-west-2:123456789012:function:test:durable:xyz");
555        assert!(arn.contains(":durable:"));
556    }
557
558    #[test]
559    fn test_execution_arn_new_valid() {
560        let arn =
561            ExecutionArn::new("arn:aws:lambda:eu-west-1:123456789012:function:func:durable:id123");
562        assert!(arn.is_ok());
563    }
564
565    #[test]
566    fn test_execution_arn_new_empty_rejected() {
567        let result = ExecutionArn::new("");
568        assert!(result.is_err());
569        let err = result.unwrap_err();
570        assert_eq!(err.type_name, "ExecutionArn");
571        assert!(err.message.contains("empty"));
572    }
573
574    #[test]
575    fn test_execution_arn_new_invalid_prefix_rejected() {
576        let result = ExecutionArn::new("not-an-arn");
577        assert!(result.is_err());
578        let err = result.unwrap_err();
579        assert!(err.message.contains("arn:"));
580    }
581
582    #[test]
583    fn test_execution_arn_new_missing_lambda_rejected() {
584        let result = ExecutionArn::new("arn:aws:s3:us-east-1:123456789012:bucket:durable:abc");
585        assert!(result.is_err());
586        let err = result.unwrap_err();
587        assert!(err.message.contains(":lambda:"));
588    }
589
590    #[test]
591    fn test_execution_arn_new_missing_durable_rejected() {
592        let result = ExecutionArn::new("arn:aws:lambda:us-east-1:123456789012:function:my-func");
593        assert!(result.is_err());
594        let err = result.unwrap_err();
595        assert!(err.message.contains(":durable:"));
596    }
597
598    #[test]
599    fn test_execution_arn_new_aws_cn_partition() {
600        let arn = ExecutionArn::new(
601            "arn:aws-cn:lambda:cn-north-1:123456789012:function:my-func:durable:abc123",
602        );
603        assert!(arn.is_ok());
604    }
605
606    #[test]
607    fn test_execution_arn_new_aws_us_gov_partition() {
608        let arn = ExecutionArn::new(
609            "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:my-func:durable:abc123",
610        );
611        assert!(arn.is_ok());
612    }
613
614    #[test]
615    fn test_execution_arn_new_aws_iso_partition() {
616        let arn = ExecutionArn::new(
617            "arn:aws-iso:lambda:us-iso-east-1:123456789012:function:my-func:durable:abc123",
618        );
619        assert!(arn.is_ok());
620    }
621
622    #[test]
623    fn test_execution_arn_display() {
624        let arn = ExecutionArn::from("arn:aws:lambda:us-east-1:123456789012:function:f:durable:x");
625        assert_eq!(
626            format!("{}", arn),
627            "arn:aws:lambda:us-east-1:123456789012:function:f:durable:x"
628        );
629    }
630
631    #[test]
632    fn test_execution_arn_deref() {
633        let arn = ExecutionArn::from("arn:aws:lambda:us-east-1:123456789012:function:f:durable:x");
634        assert!(arn.starts_with("arn:"));
635        assert!(arn.contains("lambda"));
636    }
637
638    #[test]
639    fn test_execution_arn_hash_and_eq() {
640        let arn1 = ExecutionArn::from("arn:aws:lambda:us-east-1:123:function:f:durable:a");
641        let arn2 = ExecutionArn::from("arn:aws:lambda:us-east-1:123:function:f:durable:a");
642        let arn3 = ExecutionArn::from("arn:aws:lambda:us-east-1:123:function:f:durable:b");
643
644        assert_eq!(arn1, arn2);
645        assert_ne!(arn1, arn3);
646
647        let mut map: HashMap<ExecutionArn, i32> = HashMap::new();
648        map.insert(arn1.clone(), 42);
649        assert_eq!(map.get(&arn2), Some(&42));
650    }
651
652    #[test]
653    fn test_execution_arn_serde_roundtrip() {
654        let arn = ExecutionArn::from("arn:aws:lambda:us-east-1:123:function:f:durable:serde");
655        let json = serde_json::to_string(&arn).unwrap();
656        let deserialized: ExecutionArn = serde_json::from_str(&json).unwrap();
657        assert_eq!(deserialized, arn);
658    }
659
660    // ==================== CallbackId Tests ====================
661
662    #[test]
663    fn test_callback_id_from_string() {
664        let id = CallbackId::from("callback-123".to_string());
665        assert_eq!(id.as_str(), "callback-123");
666    }
667
668    #[test]
669    fn test_callback_id_from_str() {
670        let id = CallbackId::from("callback-456");
671        assert_eq!(id.as_str(), "callback-456");
672    }
673
674    #[test]
675    fn test_callback_id_new_valid() {
676        let id = CallbackId::new("callback-abc").unwrap();
677        assert_eq!(id.as_str(), "callback-abc");
678    }
679
680    #[test]
681    fn test_callback_id_new_empty_rejected() {
682        let result = CallbackId::new("");
683        assert!(result.is_err());
684        let err = result.unwrap_err();
685        assert_eq!(err.type_name, "CallbackId");
686        assert!(err.message.contains("empty"));
687    }
688
689    #[test]
690    fn test_callback_id_display() {
691        let id = CallbackId::from("callback-display");
692        assert_eq!(format!("{}", id), "callback-display");
693    }
694
695    #[test]
696    fn test_callback_id_deref() {
697        let id = CallbackId::from("callback-deref");
698        assert!(id.starts_with("callback-"));
699        assert_eq!(id.len(), 14);
700    }
701
702    #[test]
703    fn test_callback_id_as_ref() {
704        let id = CallbackId::from("callback-asref");
705        let s: &str = id.as_ref();
706        assert_eq!(s, "callback-asref");
707    }
708
709    #[test]
710    fn test_callback_id_hash_and_eq() {
711        let id1 = CallbackId::from("callback-hash");
712        let id2 = CallbackId::from("callback-hash");
713        let id3 = CallbackId::from("callback-other");
714
715        assert_eq!(id1, id2);
716        assert_ne!(id1, id3);
717
718        let mut map: HashMap<CallbackId, bool> = HashMap::new();
719        map.insert(id1.clone(), true);
720        assert_eq!(map.get(&id2), Some(&true));
721    }
722
723    #[test]
724    fn test_callback_id_serde_roundtrip() {
725        let id = CallbackId::from("callback-serde");
726        let json = serde_json::to_string(&id).unwrap();
727        assert_eq!(json, "\"callback-serde\"");
728
729        let deserialized: CallbackId = serde_json::from_str(&json).unwrap();
730        assert_eq!(deserialized, id);
731    }
732
733    // ==================== ValidationError Tests ====================
734
735    #[test]
736    fn test_validation_error_display() {
737        let err = ValidationError {
738            type_name: "TestType",
739            message: "test error message".to_string(),
740        };
741        assert_eq!(format!("{}", err), "TestType: test error message");
742    }
743
744    // ==================== Backward Compatibility Tests ====================
745
746    /// Tests that existing serialized data (plain JSON strings) can be deserialized
747    /// into newtypes, ensuring backward compatibility with existing checkpoints.
748    #[test]
749    fn test_backward_compat_deserialize_plain_string_to_operation_id() {
750        // Simulate existing serialized data (plain JSON string)
751        let existing_json = "\"existing-op-id-12345\"";
752
753        // Should deserialize successfully into OperationId
754        let id: OperationId = serde_json::from_str(existing_json).unwrap();
755        assert_eq!(id.as_str(), "existing-op-id-12345");
756    }
757
758    #[test]
759    fn test_backward_compat_deserialize_plain_string_to_execution_arn() {
760        // Simulate existing serialized data (plain JSON string)
761        let existing_json =
762            "\"arn:aws:lambda:us-east-1:123456789012:function:my-func:durable:abc123\"";
763
764        // Should deserialize successfully into ExecutionArn
765        let arn: ExecutionArn = serde_json::from_str(existing_json).unwrap();
766        assert!(arn.contains(":durable:"));
767    }
768
769    #[test]
770    fn test_backward_compat_deserialize_plain_string_to_callback_id() {
771        // Simulate existing serialized data (plain JSON string)
772        let existing_json = "\"callback-xyz-789\"";
773
774        // Should deserialize successfully into CallbackId
775        let id: CallbackId = serde_json::from_str(existing_json).unwrap();
776        assert_eq!(id.as_str(), "callback-xyz-789");
777    }
778
779    /// Tests that functions accepting `impl Into<NewType>` work with String,
780    /// ensuring backward compatibility with existing code.
781    #[test]
782    fn test_backward_compat_impl_into_with_string() {
783        fn accept_operation_id(id: impl Into<OperationId>) -> OperationId {
784            id.into()
785        }
786
787        fn accept_execution_arn(arn: impl Into<ExecutionArn>) -> ExecutionArn {
788            arn.into()
789        }
790
791        fn accept_callback_id(id: impl Into<CallbackId>) -> CallbackId {
792            id.into()
793        }
794
795        // Should work with String
796        let op_id = accept_operation_id("op-123".to_string());
797        assert_eq!(op_id.as_str(), "op-123");
798
799        // Should work with &str
800        let op_id = accept_operation_id("op-456");
801        assert_eq!(op_id.as_str(), "op-456");
802
803        // Should work with ExecutionArn
804        let arn =
805            accept_execution_arn("arn:aws:lambda:us-east-1:123:function:f:durable:x".to_string());
806        assert!(arn.contains(":durable:"));
807
808        // Should work with CallbackId
809        let cb_id = accept_callback_id("callback-abc");
810        assert_eq!(cb_id.as_str(), "callback-abc");
811    }
812
813    /// Tests that newtypes serialize to the same format as plain strings,
814    /// ensuring backward compatibility with existing consumers.
815    #[test]
816    fn test_backward_compat_serialize_same_as_string() {
817        let op_id = OperationId::from("op-123");
818        let plain_string = "op-123".to_string();
819
820        let op_id_json = serde_json::to_string(&op_id).unwrap();
821        let string_json = serde_json::to_string(&plain_string).unwrap();
822
823        // Both should serialize to the same JSON
824        assert_eq!(op_id_json, string_json);
825        assert_eq!(op_id_json, "\"op-123\"");
826    }
827
828    /// Tests that newtypes can be used interchangeably with &str in string operations.
829    #[test]
830    fn test_backward_compat_string_operations() {
831        let op_id = OperationId::from("op-123-suffix");
832
833        // Should work with string methods via Deref
834        assert!(op_id.starts_with("op-"));
835        assert!(op_id.ends_with("-suffix"));
836        assert!(op_id.contains("123"));
837        assert_eq!(op_id.len(), 13); // "op-123-suffix" is 13 characters
838
839        // Should work with functions expecting &str
840        fn process_str(s: &str) -> String {
841            s.to_uppercase()
842        }
843        assert_eq!(process_str(&op_id), "OP-123-SUFFIX");
844    }
845
846    // ==================== Property-Based Tests ====================
847
848    use proptest::prelude::*;
849
850    /// Strategy for generating non-empty strings (valid for OperationId and CallbackId)
851    fn non_empty_string_strategy() -> impl Strategy<Value = String> {
852        "[a-zA-Z0-9_-]{1,64}".prop_map(|s| s)
853    }
854
855    /// Strategy for generating valid ExecutionArn strings
856    fn valid_execution_arn_strategy() -> impl Strategy<Value = String> {
857        (
858            prop_oneof![
859                Just("aws"),
860                Just("aws-cn"),
861                Just("aws-us-gov"),
862                Just("aws-iso"),
863                Just("aws-iso-b"),
864            ],
865            prop_oneof![
866                Just("us-east-1"),
867                Just("us-west-2"),
868                Just("eu-west-1"),
869                Just("cn-north-1"),
870                Just("us-gov-west-1"),
871            ],
872            "[0-9]{12}",
873            "[a-zA-Z0-9_-]{1,32}",
874            "[a-zA-Z0-9]{8,32}",
875        )
876            .prop_map(|(partition, region, account, func, exec_id)| {
877                format!(
878                    "arn:{}:lambda:{}:{}:function:{}:durable:{}",
879                    partition, region, account, func, exec_id
880                )
881            })
882    }
883
884    /// Strategy for generating invalid ARN strings (missing required components)
885    fn invalid_arn_strategy() -> impl Strategy<Value = String> {
886        prop_oneof![
887            // Empty string
888            Just("".to_string()),
889            // Missing arn: prefix
890            "[a-zA-Z0-9]{10,30}".prop_map(|s| s),
891            // Has arn: but missing lambda
892            "[a-zA-Z0-9]{5,20}".prop_map(|s| format!("arn:aws:s3:us-east-1:123456789012:{}", s)),
893            // Has arn: and lambda but missing durable
894            "[a-zA-Z0-9]{5,20}"
895                .prop_map(|s| format!("arn:aws:lambda:us-east-1:123456789012:function:{}", s)),
896        ]
897    }
898
899    proptest! {
900        #![proptest_config(ProptestConfig::with_cases(100))]
901
902        // ==================== OperationId Property Tests ====================
903
904        /// Feature: rust-sdk-test-suite, Property 8: OperationId Validation and Round-Trip
905        /// For any non-empty string, OperationId::new() SHALL succeed and round-trip through serde.
906        /// **Validates: Requirements 4.1**
907        #[test]
908        fn prop_operation_id_validation_and_roundtrip(s in non_empty_string_strategy()) {
909            // Validation should succeed for non-empty strings
910            let id = OperationId::new(&s).expect("non-empty string should be valid");
911
912            // Round-trip through serde
913            let json = serde_json::to_string(&id).expect("serialization should succeed");
914            let deserialized: OperationId = serde_json::from_str(&json).expect("deserialization should succeed");
915
916            // Should produce the same value
917            prop_assert_eq!(&id, &deserialized);
918            prop_assert_eq!(deserialized.as_str(), s.as_str());
919        }
920
921        /// Feature: rust-sdk-test-suite, Property 8: OperationId Empty String Validation
922        /// For any empty string, OperationId::new() SHALL return ValidationError.
923        /// **Validates: Requirements 4.4**
924        #[test]
925        fn prop_operation_id_empty_string_rejected(_dummy in Just(())) {
926            let result = OperationId::new("");
927            prop_assert!(result.is_err());
928            let err = result.unwrap_err();
929            prop_assert_eq!(err.type_name, "OperationId");
930            prop_assert!(err.message.contains("empty"));
931        }
932
933        /// Feature: rust-sdk-test-suite, Property 8: OperationId From/Into Round-Trip
934        /// For any string, creating via From and serializing should round-trip.
935        #[test]
936        fn prop_operation_id_from_roundtrip(s in ".*") {
937            let id = OperationId::from(s.clone());
938
939            // Round-trip through serde
940            let json = serde_json::to_string(&id).expect("serialization should succeed");
941            let deserialized: OperationId = serde_json::from_str(&json).expect("deserialization should succeed");
942
943            prop_assert_eq!(&id, &deserialized);
944            prop_assert_eq!(deserialized.as_str(), s.as_str());
945        }
946
947        // ==================== ExecutionArn Property Tests ====================
948
949        /// Feature: rust-sdk-test-suite, Property 9: ExecutionArn Validation and Round-Trip
950        /// For any valid ARN string, ExecutionArn::new() SHALL succeed and round-trip through serde.
951        /// **Validates: Requirements 4.2**
952        #[test]
953        fn prop_execution_arn_validation_and_roundtrip(arn_str in valid_execution_arn_strategy()) {
954            // Validation should succeed for valid ARN strings
955            let arn = ExecutionArn::new(&arn_str).expect("valid ARN should be accepted");
956
957            // Round-trip through serde
958            let json = serde_json::to_string(&arn).expect("serialization should succeed");
959            let deserialized: ExecutionArn = serde_json::from_str(&json).expect("deserialization should succeed");
960
961            // Should produce the same value
962            prop_assert_eq!(&arn, &deserialized);
963            prop_assert_eq!(deserialized.as_str(), arn_str.as_str());
964        }
965
966        /// Feature: rust-sdk-test-suite, Property 9: ExecutionArn Invalid String Validation
967        /// For any string not matching ARN pattern, ExecutionArn::new() SHALL return ValidationError.
968        /// **Validates: Requirements 4.5**
969        #[test]
970        fn prop_execution_arn_invalid_rejected(invalid_arn in invalid_arn_strategy()) {
971            let result = ExecutionArn::new(&invalid_arn);
972            prop_assert!(result.is_err(), "Invalid ARN '{}' should be rejected", invalid_arn);
973        }
974
975        /// Feature: rust-sdk-test-suite, Property 9: ExecutionArn From/Into Round-Trip
976        /// For any string, creating via From and serializing should round-trip.
977        #[test]
978        fn prop_execution_arn_from_roundtrip(s in ".*") {
979            let arn = ExecutionArn::from(s.clone());
980
981            // Round-trip through serde
982            let json = serde_json::to_string(&arn).expect("serialization should succeed");
983            let deserialized: ExecutionArn = serde_json::from_str(&json).expect("deserialization should succeed");
984
985            prop_assert_eq!(&arn, &deserialized);
986            prop_assert_eq!(deserialized.as_str(), s.as_str());
987        }
988
989        // ==================== CallbackId Property Tests ====================
990
991        /// Feature: rust-sdk-test-suite, Property 10: CallbackId Validation and Round-Trip
992        /// For any non-empty string, CallbackId::new() SHALL succeed and round-trip through serde.
993        /// **Validates: Requirements 4.3**
994        #[test]
995        fn prop_callback_id_validation_and_roundtrip(s in non_empty_string_strategy()) {
996            // Validation should succeed for non-empty strings
997            let id = CallbackId::new(&s).expect("non-empty string should be valid");
998
999            // Round-trip through serde
1000            let json = serde_json::to_string(&id).expect("serialization should succeed");
1001            let deserialized: CallbackId = serde_json::from_str(&json).expect("deserialization should succeed");
1002
1003            // Should produce the same value
1004            prop_assert_eq!(&id, &deserialized);
1005            prop_assert_eq!(deserialized.as_str(), s.as_str());
1006        }
1007
1008        /// Feature: rust-sdk-test-suite, Property 10: CallbackId Empty String Validation
1009        /// For any empty string, CallbackId::new() SHALL return ValidationError.
1010        /// **Validates: Requirements 4.4 (applies to CallbackId as well)**
1011        #[test]
1012        fn prop_callback_id_empty_string_rejected(_dummy in Just(())) {
1013            let result = CallbackId::new("");
1014            prop_assert!(result.is_err());
1015            let err = result.unwrap_err();
1016            prop_assert_eq!(err.type_name, "CallbackId");
1017            prop_assert!(err.message.contains("empty"));
1018        }
1019
1020        /// Feature: rust-sdk-test-suite, Property 10: CallbackId From/Into Round-Trip
1021        /// For any string, creating via From and serializing should round-trip.
1022        #[test]
1023        fn prop_callback_id_from_roundtrip(s in ".*") {
1024            let id = CallbackId::from(s.clone());
1025
1026            // Round-trip through serde
1027            let json = serde_json::to_string(&id).expect("serialization should succeed");
1028            let deserialized: CallbackId = serde_json::from_str(&json).expect("deserialization should succeed");
1029
1030            prop_assert_eq!(&id, &deserialized);
1031            prop_assert_eq!(deserialized.as_str(), s.as_str());
1032        }
1033
1034        // ==================== HashMap Key Behavior Property Tests ====================
1035
1036        /// Feature: rust-sdk-test-suite, Property 11: OperationId HashMap Key Behavior
1037        /// For any OperationId instance, inserting into a HashMap and retrieving by an equal key
1038        /// SHALL return the same value.
1039        /// **Validates: Requirements 4.6**
1040        #[test]
1041        fn prop_operation_id_hashmap_key(s in non_empty_string_strategy(), value in any::<i32>()) {
1042            let id1 = OperationId::from(s.clone());
1043            let id2 = OperationId::from(s.clone());
1044
1045            let mut map: HashMap<OperationId, i32> = HashMap::new();
1046            map.insert(id1, value);
1047
1048            // Retrieving by equal key should return the same value
1049            prop_assert_eq!(map.get(&id2), Some(&value));
1050        }
1051
1052        /// Feature: rust-sdk-test-suite, Property 11: ExecutionArn HashMap Key Behavior
1053        /// For any ExecutionArn instance, inserting into a HashMap and retrieving by an equal key
1054        /// SHALL return the same value.
1055        /// **Validates: Requirements 4.6**
1056        #[test]
1057        fn prop_execution_arn_hashmap_key(arn_str in valid_execution_arn_strategy(), value in any::<i32>()) {
1058            let arn1 = ExecutionArn::from(arn_str.clone());
1059            let arn2 = ExecutionArn::from(arn_str.clone());
1060
1061            let mut map: HashMap<ExecutionArn, i32> = HashMap::new();
1062            map.insert(arn1, value);
1063
1064            // Retrieving by equal key should return the same value
1065            prop_assert_eq!(map.get(&arn2), Some(&value));
1066        }
1067
1068        /// Feature: rust-sdk-test-suite, Property 11: CallbackId HashMap Key Behavior
1069        /// For any CallbackId instance, inserting into a HashMap and retrieving by an equal key
1070        /// SHALL return the same value.
1071        /// **Validates: Requirements 4.6**
1072        #[test]
1073        fn prop_callback_id_hashmap_key(s in non_empty_string_strategy(), value in any::<i32>()) {
1074            let id1 = CallbackId::from(s.clone());
1075            let id2 = CallbackId::from(s.clone());
1076
1077            let mut map: HashMap<CallbackId, i32> = HashMap::new();
1078            map.insert(id1, value);
1079
1080            // Retrieving by equal key should return the same value
1081            prop_assert_eq!(map.get(&id2), Some(&value));
1082        }
1083
1084        /// Feature: rust-sdk-test-suite, Property 11: Different keys should not collide
1085        /// For any two different strings, their newtypes should not be equal and should
1086        /// not collide in HashMap lookups.
1087        #[test]
1088        fn prop_different_keys_no_collision(
1089            s1 in non_empty_string_strategy(),
1090            s2 in non_empty_string_strategy(),
1091            value in any::<i32>()
1092        ) {
1093            prop_assume!(s1 != s2);
1094
1095            let id1 = OperationId::from(s1);
1096            let id2 = OperationId::from(s2);
1097
1098            let mut map: HashMap<OperationId, i32> = HashMap::new();
1099            map.insert(id1.clone(), value);
1100
1101            // Different key should not find the value
1102            prop_assert_eq!(map.get(&id2), None);
1103            // Original key should still find the value
1104            prop_assert_eq!(map.get(&id1), Some(&value));
1105        }
1106    }
1107}