use std::fmt;
use std::hash::Hash;
use std::ops::Deref;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError {
pub type_name: &'static str,
pub message: String,
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.type_name, self.message)
}
}
impl std::error::Error for ValidationError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct OperationId(String);
impl OperationId {
pub fn new(id: impl Into<String>) -> Result<Self, ValidationError> {
let id = id.into();
if id.is_empty() {
return Err(ValidationError {
type_name: "OperationId",
message: "value cannot be empty".to_string(),
});
}
Ok(Self(id))
}
#[inline]
pub fn new_unchecked(id: impl Into<String>) -> Self {
Self(id.into())
}
#[inline]
pub fn into_inner(self) -> String {
self.0
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for OperationId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Deref for OperationId {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for OperationId {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<String> for OperationId {
#[inline]
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for OperationId {
#[inline]
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ExecutionArn(String);
impl ExecutionArn {
pub fn new(arn: impl Into<String>) -> Result<Self, ValidationError> {
let arn = arn.into();
Self::validate(&arn)?;
Ok(Self(arn))
}
#[inline]
pub fn new_unchecked(arn: impl Into<String>) -> Self {
Self(arn.into())
}
#[inline]
pub fn into_inner(self) -> String {
self.0
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
fn validate(value: &str) -> Result<(), ValidationError> {
if value.is_empty() {
return Err(ValidationError {
type_name: "ExecutionArn",
message: "value cannot be empty".to_string(),
});
}
if !value.starts_with("arn:") {
return Err(ValidationError {
type_name: "ExecutionArn",
message: "must start with 'arn:'".to_string(),
});
}
if !value.contains(":lambda:") {
return Err(ValidationError {
type_name: "ExecutionArn",
message: "must contain ':lambda:' service identifier".to_string(),
});
}
if !value.contains(":durable:") {
return Err(ValidationError {
type_name: "ExecutionArn",
message: "must contain ':durable:' segment".to_string(),
});
}
Ok(())
}
}
impl fmt::Display for ExecutionArn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Deref for ExecutionArn {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for ExecutionArn {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<String> for ExecutionArn {
#[inline]
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for ExecutionArn {
#[inline]
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CallbackId(String);
impl CallbackId {
pub fn new(id: impl Into<String>) -> Result<Self, ValidationError> {
let id = id.into();
if id.is_empty() {
return Err(ValidationError {
type_name: "CallbackId",
message: "value cannot be empty".to_string(),
});
}
Ok(Self(id))
}
#[inline]
pub fn new_unchecked(id: impl Into<String>) -> Self {
Self(id.into())
}
#[inline]
pub fn into_inner(self) -> String {
self.0
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CallbackId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Deref for CallbackId {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for CallbackId {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<String> for CallbackId {
#[inline]
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for CallbackId {
#[inline]
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_operation_id_from_string() {
let id = OperationId::from("op-123".to_string());
assert_eq!(id.as_str(), "op-123");
}
#[test]
fn test_operation_id_from_str() {
let id = OperationId::from("op-456");
assert_eq!(id.as_str(), "op-456");
}
#[test]
fn test_operation_id_into() {
let id: OperationId = "op-789".into();
assert_eq!(id.as_str(), "op-789");
}
#[test]
fn test_operation_id_new_valid() {
let id = OperationId::new("op-abc").unwrap();
assert_eq!(id.as_str(), "op-abc");
}
#[test]
fn test_operation_id_new_empty_rejected() {
let result = OperationId::new("");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.type_name, "OperationId");
assert!(err.message.contains("empty"));
}
#[test]
fn test_operation_id_display() {
let id = OperationId::from("op-display");
assert_eq!(format!("{}", id), "op-display");
}
#[test]
fn test_operation_id_debug() {
let id = OperationId::from("op-debug");
let debug_str = format!("{:?}", id);
assert!(debug_str.contains("op-debug"));
}
#[test]
fn test_operation_id_deref() {
let id = OperationId::from("op-deref-test");
assert!(id.starts_with("op-"));
assert_eq!(id.len(), 13);
}
#[test]
fn test_operation_id_as_ref() {
let id = OperationId::from("op-asref");
let s: &str = id.as_ref();
assert_eq!(s, "op-asref");
}
#[test]
fn test_operation_id_hash_and_eq() {
let id1 = OperationId::from("op-hash");
let id2 = OperationId::from("op-hash");
let id3 = OperationId::from("op-different");
assert_eq!(id1, id2);
assert_ne!(id1, id3);
let mut map: HashMap<OperationId, String> = HashMap::new();
map.insert(id1.clone(), "value1".to_string());
assert_eq!(map.get(&id2), Some(&"value1".to_string()));
assert_eq!(map.get(&id3), None);
}
#[test]
fn test_operation_id_serde_roundtrip() {
let id = OperationId::from("op-serde-test");
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "\"op-serde-test\"");
let deserialized: OperationId = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, id);
}
#[test]
fn test_operation_id_into_inner() {
let id = OperationId::from("op-inner");
let inner = id.into_inner();
assert_eq!(inner, "op-inner");
}
#[test]
fn test_execution_arn_from_string() {
let arn = ExecutionArn::from(
"arn:aws:lambda:us-east-1:123456789012:function:my-func:durable:abc123".to_string(),
);
assert!(arn.as_str().starts_with("arn:aws:lambda:"));
}
#[test]
fn test_execution_arn_from_str() {
let arn =
ExecutionArn::from("arn:aws:lambda:us-west-2:123456789012:function:test:durable:xyz");
assert!(arn.contains(":durable:"));
}
#[test]
fn test_execution_arn_new_valid() {
let arn =
ExecutionArn::new("arn:aws:lambda:eu-west-1:123456789012:function:func:durable:id123");
assert!(arn.is_ok());
}
#[test]
fn test_execution_arn_new_empty_rejected() {
let result = ExecutionArn::new("");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.type_name, "ExecutionArn");
assert!(err.message.contains("empty"));
}
#[test]
fn test_execution_arn_new_invalid_prefix_rejected() {
let result = ExecutionArn::new("not-an-arn");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.message.contains("arn:"));
}
#[test]
fn test_execution_arn_new_missing_lambda_rejected() {
let result = ExecutionArn::new("arn:aws:s3:us-east-1:123456789012:bucket:durable:abc");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.message.contains(":lambda:"));
}
#[test]
fn test_execution_arn_new_missing_durable_rejected() {
let result = ExecutionArn::new("arn:aws:lambda:us-east-1:123456789012:function:my-func");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.message.contains(":durable:"));
}
#[test]
fn test_execution_arn_new_aws_cn_partition() {
let arn = ExecutionArn::new(
"arn:aws-cn:lambda:cn-north-1:123456789012:function:my-func:durable:abc123",
);
assert!(arn.is_ok());
}
#[test]
fn test_execution_arn_new_aws_us_gov_partition() {
let arn = ExecutionArn::new(
"arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:my-func:durable:abc123",
);
assert!(arn.is_ok());
}
#[test]
fn test_execution_arn_new_aws_iso_partition() {
let arn = ExecutionArn::new(
"arn:aws-iso:lambda:us-iso-east-1:123456789012:function:my-func:durable:abc123",
);
assert!(arn.is_ok());
}
#[test]
fn test_execution_arn_display() {
let arn = ExecutionArn::from("arn:aws:lambda:us-east-1:123456789012:function:f:durable:x");
assert_eq!(
format!("{}", arn),
"arn:aws:lambda:us-east-1:123456789012:function:f:durable:x"
);
}
#[test]
fn test_execution_arn_deref() {
let arn = ExecutionArn::from("arn:aws:lambda:us-east-1:123456789012:function:f:durable:x");
assert!(arn.starts_with("arn:"));
assert!(arn.contains("lambda"));
}
#[test]
fn test_execution_arn_hash_and_eq() {
let arn1 = ExecutionArn::from("arn:aws:lambda:us-east-1:123:function:f:durable:a");
let arn2 = ExecutionArn::from("arn:aws:lambda:us-east-1:123:function:f:durable:a");
let arn3 = ExecutionArn::from("arn:aws:lambda:us-east-1:123:function:f:durable:b");
assert_eq!(arn1, arn2);
assert_ne!(arn1, arn3);
let mut map: HashMap<ExecutionArn, i32> = HashMap::new();
map.insert(arn1.clone(), 42);
assert_eq!(map.get(&arn2), Some(&42));
}
#[test]
fn test_execution_arn_serde_roundtrip() {
let arn = ExecutionArn::from("arn:aws:lambda:us-east-1:123:function:f:durable:serde");
let json = serde_json::to_string(&arn).unwrap();
let deserialized: ExecutionArn = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, arn);
}
#[test]
fn test_callback_id_from_string() {
let id = CallbackId::from("callback-123".to_string());
assert_eq!(id.as_str(), "callback-123");
}
#[test]
fn test_callback_id_from_str() {
let id = CallbackId::from("callback-456");
assert_eq!(id.as_str(), "callback-456");
}
#[test]
fn test_callback_id_new_valid() {
let id = CallbackId::new("callback-abc").unwrap();
assert_eq!(id.as_str(), "callback-abc");
}
#[test]
fn test_callback_id_new_empty_rejected() {
let result = CallbackId::new("");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.type_name, "CallbackId");
assert!(err.message.contains("empty"));
}
#[test]
fn test_callback_id_display() {
let id = CallbackId::from("callback-display");
assert_eq!(format!("{}", id), "callback-display");
}
#[test]
fn test_callback_id_deref() {
let id = CallbackId::from("callback-deref");
assert!(id.starts_with("callback-"));
assert_eq!(id.len(), 14);
}
#[test]
fn test_callback_id_as_ref() {
let id = CallbackId::from("callback-asref");
let s: &str = id.as_ref();
assert_eq!(s, "callback-asref");
}
#[test]
fn test_callback_id_hash_and_eq() {
let id1 = CallbackId::from("callback-hash");
let id2 = CallbackId::from("callback-hash");
let id3 = CallbackId::from("callback-other");
assert_eq!(id1, id2);
assert_ne!(id1, id3);
let mut map: HashMap<CallbackId, bool> = HashMap::new();
map.insert(id1.clone(), true);
assert_eq!(map.get(&id2), Some(&true));
}
#[test]
fn test_callback_id_serde_roundtrip() {
let id = CallbackId::from("callback-serde");
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "\"callback-serde\"");
let deserialized: CallbackId = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, id);
}
#[test]
fn test_validation_error_display() {
let err = ValidationError {
type_name: "TestType",
message: "test error message".to_string(),
};
assert_eq!(format!("{}", err), "TestType: test error message");
}
#[test]
fn test_backward_compat_deserialize_plain_string_to_operation_id() {
let existing_json = "\"existing-op-id-12345\"";
let id: OperationId = serde_json::from_str(existing_json).unwrap();
assert_eq!(id.as_str(), "existing-op-id-12345");
}
#[test]
fn test_backward_compat_deserialize_plain_string_to_execution_arn() {
let existing_json =
"\"arn:aws:lambda:us-east-1:123456789012:function:my-func:durable:abc123\"";
let arn: ExecutionArn = serde_json::from_str(existing_json).unwrap();
assert!(arn.contains(":durable:"));
}
#[test]
fn test_backward_compat_deserialize_plain_string_to_callback_id() {
let existing_json = "\"callback-xyz-789\"";
let id: CallbackId = serde_json::from_str(existing_json).unwrap();
assert_eq!(id.as_str(), "callback-xyz-789");
}
#[test]
fn test_backward_compat_impl_into_with_string() {
fn accept_operation_id(id: impl Into<OperationId>) -> OperationId {
id.into()
}
fn accept_execution_arn(arn: impl Into<ExecutionArn>) -> ExecutionArn {
arn.into()
}
fn accept_callback_id(id: impl Into<CallbackId>) -> CallbackId {
id.into()
}
let op_id = accept_operation_id("op-123".to_string());
assert_eq!(op_id.as_str(), "op-123");
let op_id = accept_operation_id("op-456");
assert_eq!(op_id.as_str(), "op-456");
let arn =
accept_execution_arn("arn:aws:lambda:us-east-1:123:function:f:durable:x".to_string());
assert!(arn.contains(":durable:"));
let cb_id = accept_callback_id("callback-abc");
assert_eq!(cb_id.as_str(), "callback-abc");
}
#[test]
fn test_backward_compat_serialize_same_as_string() {
let op_id = OperationId::from("op-123");
let plain_string = "op-123".to_string();
let op_id_json = serde_json::to_string(&op_id).unwrap();
let string_json = serde_json::to_string(&plain_string).unwrap();
assert_eq!(op_id_json, string_json);
assert_eq!(op_id_json, "\"op-123\"");
}
#[test]
fn test_backward_compat_string_operations() {
let op_id = OperationId::from("op-123-suffix");
assert!(op_id.starts_with("op-"));
assert!(op_id.ends_with("-suffix"));
assert!(op_id.contains("123"));
assert_eq!(op_id.len(), 13);
fn process_str(s: &str) -> String {
s.to_uppercase()
}
assert_eq!(process_str(&op_id), "OP-123-SUFFIX");
}
use proptest::prelude::*;
fn non_empty_string_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z0-9_-]{1,64}".prop_map(|s| s)
}
fn valid_execution_arn_strategy() -> impl Strategy<Value = String> {
(
prop_oneof![
Just("aws"),
Just("aws-cn"),
Just("aws-us-gov"),
Just("aws-iso"),
Just("aws-iso-b"),
],
prop_oneof![
Just("us-east-1"),
Just("us-west-2"),
Just("eu-west-1"),
Just("cn-north-1"),
Just("us-gov-west-1"),
],
"[0-9]{12}",
"[a-zA-Z0-9_-]{1,32}",
"[a-zA-Z0-9]{8,32}",
)
.prop_map(|(partition, region, account, func, exec_id)| {
format!(
"arn:{}:lambda:{}:{}:function:{}:durable:{}",
partition, region, account, func, exec_id
)
})
}
fn invalid_arn_strategy() -> impl Strategy<Value = String> {
prop_oneof![
Just("".to_string()),
"[a-zA-Z0-9]{10,30}".prop_map(|s| s),
"[a-zA-Z0-9]{5,20}".prop_map(|s| format!("arn:aws:s3:us-east-1:123456789012:{}", s)),
"[a-zA-Z0-9]{5,20}"
.prop_map(|s| format!("arn:aws:lambda:us-east-1:123456789012:function:{}", s)),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_operation_id_validation_and_roundtrip(s in non_empty_string_strategy()) {
let id = OperationId::new(&s).expect("non-empty string should be valid");
let json = serde_json::to_string(&id).expect("serialization should succeed");
let deserialized: OperationId = serde_json::from_str(&json).expect("deserialization should succeed");
prop_assert_eq!(&id, &deserialized);
prop_assert_eq!(deserialized.as_str(), s.as_str());
}
#[test]
fn prop_operation_id_empty_string_rejected(_dummy in Just(())) {
let result = OperationId::new("");
prop_assert!(result.is_err());
let err = result.unwrap_err();
prop_assert_eq!(err.type_name, "OperationId");
prop_assert!(err.message.contains("empty"));
}
#[test]
fn prop_operation_id_from_roundtrip(s in ".*") {
let id = OperationId::from(s.clone());
let json = serde_json::to_string(&id).expect("serialization should succeed");
let deserialized: OperationId = serde_json::from_str(&json).expect("deserialization should succeed");
prop_assert_eq!(&id, &deserialized);
prop_assert_eq!(deserialized.as_str(), s.as_str());
}
#[test]
fn prop_execution_arn_validation_and_roundtrip(arn_str in valid_execution_arn_strategy()) {
let arn = ExecutionArn::new(&arn_str).expect("valid ARN should be accepted");
let json = serde_json::to_string(&arn).expect("serialization should succeed");
let deserialized: ExecutionArn = serde_json::from_str(&json).expect("deserialization should succeed");
prop_assert_eq!(&arn, &deserialized);
prop_assert_eq!(deserialized.as_str(), arn_str.as_str());
}
#[test]
fn prop_execution_arn_invalid_rejected(invalid_arn in invalid_arn_strategy()) {
let result = ExecutionArn::new(&invalid_arn);
prop_assert!(result.is_err(), "Invalid ARN '{}' should be rejected", invalid_arn);
}
#[test]
fn prop_execution_arn_from_roundtrip(s in ".*") {
let arn = ExecutionArn::from(s.clone());
let json = serde_json::to_string(&arn).expect("serialization should succeed");
let deserialized: ExecutionArn = serde_json::from_str(&json).expect("deserialization should succeed");
prop_assert_eq!(&arn, &deserialized);
prop_assert_eq!(deserialized.as_str(), s.as_str());
}
#[test]
fn prop_callback_id_validation_and_roundtrip(s in non_empty_string_strategy()) {
let id = CallbackId::new(&s).expect("non-empty string should be valid");
let json = serde_json::to_string(&id).expect("serialization should succeed");
let deserialized: CallbackId = serde_json::from_str(&json).expect("deserialization should succeed");
prop_assert_eq!(&id, &deserialized);
prop_assert_eq!(deserialized.as_str(), s.as_str());
}
#[test]
fn prop_callback_id_empty_string_rejected(_dummy in Just(())) {
let result = CallbackId::new("");
prop_assert!(result.is_err());
let err = result.unwrap_err();
prop_assert_eq!(err.type_name, "CallbackId");
prop_assert!(err.message.contains("empty"));
}
#[test]
fn prop_callback_id_from_roundtrip(s in ".*") {
let id = CallbackId::from(s.clone());
let json = serde_json::to_string(&id).expect("serialization should succeed");
let deserialized: CallbackId = serde_json::from_str(&json).expect("deserialization should succeed");
prop_assert_eq!(&id, &deserialized);
prop_assert_eq!(deserialized.as_str(), s.as_str());
}
#[test]
fn prop_operation_id_hashmap_key(s in non_empty_string_strategy(), value in any::<i32>()) {
let id1 = OperationId::from(s.clone());
let id2 = OperationId::from(s.clone());
let mut map: HashMap<OperationId, i32> = HashMap::new();
map.insert(id1, value);
prop_assert_eq!(map.get(&id2), Some(&value));
}
#[test]
fn prop_execution_arn_hashmap_key(arn_str in valid_execution_arn_strategy(), value in any::<i32>()) {
let arn1 = ExecutionArn::from(arn_str.clone());
let arn2 = ExecutionArn::from(arn_str.clone());
let mut map: HashMap<ExecutionArn, i32> = HashMap::new();
map.insert(arn1, value);
prop_assert_eq!(map.get(&arn2), Some(&value));
}
#[test]
fn prop_callback_id_hashmap_key(s in non_empty_string_strategy(), value in any::<i32>()) {
let id1 = CallbackId::from(s.clone());
let id2 = CallbackId::from(s.clone());
let mut map: HashMap<CallbackId, i32> = HashMap::new();
map.insert(id1, value);
prop_assert_eq!(map.get(&id2), Some(&value));
}
#[test]
fn prop_different_keys_no_collision(
s1 in non_empty_string_strategy(),
s2 in non_empty_string_strategy(),
value in any::<i32>()
) {
prop_assume!(s1 != s2);
let id1 = OperationId::from(s1);
let id2 = OperationId::from(s2);
let mut map: HashMap<OperationId, i32> = HashMap::new();
map.insert(id1.clone(), value);
prop_assert_eq!(map.get(&id2), None);
prop_assert_eq!(map.get(&id1), Some(&value));
}
}
}