aws_resource_id/
general.rs

1//! # AWS Resource IDs in a General Format
2//!
3//! This module handles AWS resource IDs that follow a specific format:
4//!
5//! 1. Prefix: a short string specific to each resource type (e.g., `ami-` for
6//!    AMIs)
7//! 2. Identifier: an 8 or 17 character unique string containing only:
8//!    - Lowercase letters (a-z)
9//!    - Numbers (0-9)
10//!
11//! ## Resource ID length
12//!
13//! > Prior to January 2016, the IDs assigned to newly created resources of
14//! > certain resource types used 8 characters after the hyphen (for example,
15//! > i-1a2b3c4d). From January 2016 to June 2018, we changed the IDs of these
16//! > resource types to use 17 characters after the hyphen (for example,
17//! > i-1234567890abcdef0). Depending on when your account was created, you
18//! > might have some existing resources with short IDs, however, any new
19//! > resources will receive the longer IDs.
20//! > <https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/resource-ids.html>
21#[cfg(feature = "sqlx-postgres")]
22use sqlx::{
23    postgres::{PgTypeInfo, PgValueRef},
24    Postgres, Type,
25};
26use std::{convert::TryFrom, fmt, str::FromStr};
27
28/// Error encountered when parsing an AWS resource ID in the general format
29#[derive(Debug, thiserror::Error)]
30#[error("failed to initialize {target_type} from \"{input}\": {error_detail}")]
31pub struct GeneralResourceError {
32    /// The AWS resource type being parsed (e.g., [`AwsAmiId`])
33    target_type: &'static str,
34    /// The input string that failed to parse
35    input: String,
36    /// Detailed description of the error
37    error_detail: GeneralResourceErrorDetail,
38}
39
40/// Specific details about errors encountered when parsing AWS resource IDs in
41/// the general format
42#[derive(Debug, thiserror::Error)]
43pub enum GeneralResourceErrorDetail {
44    /// Incorrect prefix for the resource type
45    #[error("incorrect prefix, expected \"{0}\"")]
46    WrongPrefix(&'static str),
47    /// Invalid length of the unique identifier part
48    #[error("the unique part must be 8 or 17, not {0} characters long")]
49    IdLength(usize),
50    /// The unique identifier contains invalid characters
51    #[error("the unique part contains non ascii alphanumeric characters")]
52    NonAsciiAlphanumeric,
53}
54
55/// The unique alphanumeric part of an AWS resource id in the general format
56#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57enum UniquePart {
58    C8([u8; 8]),
59    C17([u8; 17]),
60}
61
62impl UniquePart {
63    fn as_slice(&self) -> &[u8] {
64        match self {
65            Self::C8(x) => x,
66            Self::C17(x) => x,
67        }
68    }
69}
70
71macro_rules! impl_resource_id {
72    ($type:ident, $prefix:literal, $doc:literal) => {
73        #[doc = $doc]
74        #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
75        pub struct $type(UniquePart);
76
77        impl $type {
78            const PREFIX: &'static str = $prefix;
79        }
80
81        impl TryFrom<&str> for $type {
82            type Error = $crate::Error;
83
84            fn try_from(s: &str) -> Result<Self, Self::Error> {
85                if !s.starts_with(Self::PREFIX) {
86                    return Err(GeneralResourceError::new(
87                        short_type_name::<$type>(),
88                        s,
89                        GeneralResourceErrorDetail::WrongPrefix(Self::PREFIX),
90                    )
91                    .into());
92                }
93                if !s[Self::PREFIX.len()..]
94                    .chars()
95                    .all(|c| c.is_ascii_alphanumeric())
96                {
97                    return Err(GeneralResourceError::new(
98                        short_type_name::<$type>(),
99                        s,
100                        GeneralResourceErrorDetail::NonAsciiAlphanumeric,
101                    )
102                    .into());
103                }
104
105                let id = &s[Self::PREFIX.len()..];
106                if id.len() == 8 {
107                    let mut arr = [0u8; 8];
108                    arr.copy_from_slice(id.as_bytes());
109                    Ok($type(UniquePart::C8(arr)))
110                } else if id.len() == 17 {
111                    let mut arr = [0u8; 17];
112                    arr.copy_from_slice(id.as_bytes());
113                    Ok($type(UniquePart::C17(arr)))
114                } else {
115                    Err(GeneralResourceError::new(
116                        short_type_name::<$type>(),
117                        s,
118                        GeneralResourceErrorDetail::IdLength(id.len()),
119                    )
120                    .into())
121                }
122            }
123        }
124
125        impl TryFrom<String> for $type {
126            type Error = $crate::Error;
127
128            fn try_from(s: String) -> Result<Self, Self::Error> {
129                Self::try_from(s.as_str())
130            }
131        }
132
133        impl TryFrom<&String> for $type {
134            type Error = $crate::Error;
135
136            fn try_from(s: &String) -> Result<Self, Self::Error> {
137                Self::try_from(s.as_str())
138            }
139        }
140
141        impl FromStr for $type {
142            type Err = $crate::Error;
143
144            fn from_str(s: &str) -> Result<Self, Self::Err> {
145                Self::try_from(s)
146            }
147        }
148
149        impl fmt::Display for $type {
150            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151                write!(f, "{}", Self::PREFIX)?;
152                write!(
153                    f,
154                    "{}",
155                    std::str::from_utf8(self.0.as_slice()).unwrap_or_default()
156                )
157            }
158        }
159
160        impl fmt::Debug for $type {
161            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162                f.debug_tuple(short_type_name::<Self>())
163                    .field(&self.to_string())
164                    .finish()
165            }
166        }
167
168        impl From<$type> for String {
169            fn from(value: $type) -> Self {
170                value.to_string()
171            }
172        }
173
174        #[cfg(feature = "sqlx-postgres")]
175        impl Type<Postgres> for $type {
176            fn type_info() -> PgTypeInfo {
177                <String as Type<Postgres>>::type_info()
178            }
179
180            fn compatible(ty: &PgTypeInfo) -> bool {
181                <String as Type<Postgres>>::compatible(ty)
182            }
183        }
184
185        #[cfg(feature = "sqlx-postgres")]
186        impl<'q> sqlx::encode::Encode<'q, Postgres> for $type {
187            fn encode_by_ref(
188                &self,
189                buf: &mut sqlx::postgres::PgArgumentBuffer,
190            ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync>> {
191                <String as sqlx::encode::Encode<Postgres>>::encode_by_ref(&self.to_string(), buf)
192            }
193        }
194
195        #[cfg(feature = "sqlx-postgres")]
196        impl<'r> sqlx::decode::Decode<'r, Postgres> for $type {
197            fn decode(
198                value: PgValueRef<'r>,
199            ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
200                let s = <&str as sqlx::decode::Decode<Postgres>>::decode(value)?;
201                Ok($type::try_from(s).map_err(|e| Box::new(sqlx::Error::Decode(e.into())))?)
202            }
203        }
204
205        #[cfg(feature = "serde")]
206        impl serde::Serialize for $type {
207            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
208            where
209                S: serde::Serializer,
210            {
211                serializer.serialize_str(&self.to_string())
212            }
213        }
214
215        #[cfg(feature = "serde")]
216        impl<'de> serde::Deserialize<'de> for $type {
217            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
218            where
219                D: serde::Deserializer<'de>,
220            {
221                let s = String::deserialize(deserializer)?;
222                $type::try_from(s).map_err(serde::de::Error::custom)
223            }
224        }
225    };
226}
227
228fn short_type_name<T>() -> &'static str {
229    let name = std::any::type_name::<T>();
230    name.split("::").last().unwrap_or(name)
231}
232
233impl GeneralResourceError {
234    fn new(
235        target_type: &'static str,
236        input: impl Into<String>,
237        error_detail: GeneralResourceErrorDetail,
238    ) -> Self {
239        Self {
240            target_type,
241            input: input.into(),
242            error_detail,
243        }
244    }
245}
246
247impl_resource_id!(
248    AwsNetworkAclId,
249    "acl-",
250    "AWS Network ACL (Access Control List) ID"
251);
252impl_resource_id!(AwsAmiId, "ami-", "AWS AMI (Amazon Machine Image) ID");
253impl_resource_id!(AwsCustomerGatewayId, "cgw-", "AWS Customer Gateway ID");
254impl_resource_id!(AwsElasticIpId, "eipalloc-", "AWS Elastic IP ID");
255impl_resource_id!(
256    AwsEfsFileSystemId,
257    "fs-",
258    "AWS EFS (Elastic File System) ID"
259);
260impl_resource_id!(AwsEfsMountTargetId, "fsmt-", "AWS EFS Mount Target ID");
261impl_resource_id!(
262    AwsCloudFormationStackId,
263    "stack-",
264    "AWS CloudFormation Stack ID"
265);
266impl_resource_id!(
267    AwsElasticBeanstalkEnvironmentId,
268    "e-",
269    "AWS Elastic Beanstalk Environment ID"
270);
271impl_resource_id!(AwsInstanceId, "i-", "AWS EC2 Instance ID");
272impl_resource_id!(AwsInternetGatewayId, "igw-", "AWS Internet Gateway ID");
273impl_resource_id!(AwsKeyPairId, "key-", "AWS Key Pair ID");
274impl_resource_id!(AwsLoadBalancerId, "elbv2-", "AWS Elastic Load Balancer ID");
275impl_resource_id!(AwsNatGatewayId, "nat-", "AWS NAT Gateway ID");
276impl_resource_id!(AwsNetworkInterfaceId, "eni-", "AWS Network Interface ID");
277impl_resource_id!(AwsPlacementGroupId, "pg-", "AWS Placement Group ID");
278impl_resource_id!(AwsRdsInstanceId, "db-", "AWS RDS Instance ID");
279impl_resource_id!(AwsRedshiftClusterId, "redshift-", "AWS Redshift Cluster ID");
280impl_resource_id!(AwsRouteTableId, "rtb-", "AWS Route Table ID");
281impl_resource_id!(AwsSecurityGroupId, "sg-", "AWS Security Group ID");
282impl_resource_id!(AwsSnapshotId, "snap-", "AWS EBS Snapshot ID");
283impl_resource_id!(AwsSubnetId, "subnet-", "AWS VPC Subnet ID");
284impl_resource_id!(AwsTargetGroupId, "tg-", "AWS Target Group ID");
285impl_resource_id!(
286    AwsTransitGatewayAttachmentId,
287    "tgw-attach-",
288    "AWS Transit Gateway Attachment ID"
289);
290impl_resource_id!(AwsTransitGatewayId, "tgw-", "AWS Transit Gateway ID");
291impl_resource_id!(AwsVolumeId, "vol-", "AWS EBS Volume ID");
292impl_resource_id!(AwsVpcId, "vpc-", "AWS VPC (Virtual Private Cloud) ID");
293impl_resource_id!(AwsVpnConnectionId, "vpn-", "AWS VPN Connection ID");
294impl_resource_id!(AwsVpnGatewayId, "vgw-", "AWS VPN Gateway ID");
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    fn ami(s: &str) -> AwsAmiId {
301        AwsAmiId::try_from(s).unwrap()
302    }
303
304    #[test]
305    fn test_eq() {
306        assert_eq!(ami("ami-12345678"), ami("ami-12345678"));
307        assert_ne!(ami("ami-12345678"), ami("ami-abcdefgh"));
308    }
309
310    #[test]
311    fn test_fmt_display() {
312        assert_eq!(format!("{}", ami("ami-12345678")), "ami-12345678");
313    }
314
315    #[test]
316    fn test_fmt_debug() {
317        assert_eq!(
318            format!("{:?}", ami("ami-12345678")),
319            r#"AwsAmiId("ami-12345678")"#
320        );
321    }
322
323    #[test]
324    fn test_into_string() {
325        let s: String = ami("ami-12345678").into();
326        assert_eq!(s, "ami-12345678");
327    }
328
329    #[test]
330    fn test_tryfrom_str() {
331        assert!(AwsAmiId::try_from("ami-12345678").is_ok());
332    }
333
334    #[test]
335    fn test_tryfrom_string() {
336        assert!(AwsAmiId::try_from("ami-12345678".to_string()).is_ok());
337    }
338
339    #[test]
340    fn test_tryfrom_refstring() {
341        assert!(AwsAmiId::try_from(&"ami-12345678".to_string()).is_ok());
342    }
343
344    #[test]
345    fn test_fromstr() {
346        assert!("ami-12345678".parse::<AwsAmiId>().is_ok(),);
347        assert!("ami-12345678".to_string().parse::<AwsAmiId>().is_ok(),);
348    }
349
350    #[cfg(feature = "serde")]
351    #[test]
352    fn test_serialize() {
353        assert_eq!(
354            serde_json::to_string(&ami("ami-12345678")).unwrap(),
355            "\"ami-12345678\""
356        );
357    }
358
359    #[cfg(feature = "serde")]
360    #[test]
361    fn test_deserialize() {
362        assert_eq!(
363            serde_json::from_str::<AwsAmiId>("\"ami-12345678\"").unwrap(),
364            ami("ami-12345678"),
365        );
366    }
367
368    #[test]
369    fn test_wrong_prefix() {
370        let result = AwsAmiId::try_from("amx-12345678");
371        assert!(result.is_err());
372        assert_eq!(
373            result.unwrap_err().to_string(),
374            "failed to initialize AwsAmiId from \"amx-12345678\": incorrect prefix, expected \"ami-\""
375        );
376    }
377
378    #[test]
379    fn test_error_wrong_length() {
380        let result = AwsAmiId::try_from("ami-1234567");
381        assert!(result.is_err());
382        assert_eq!(
383            result.unwrap_err().to_string(),
384            "failed to initialize AwsAmiId from \"ami-1234567\": the unique part must be 8 or 17, not 7 characters long"
385        );
386
387        let result = AwsAmiId::try_from("ami-123456789012345678");
388        assert!(result.is_err());
389        assert_eq!(
390            result.unwrap_err().to_string(),
391            "failed to initialize AwsAmiId from \"ami-123456789012345678\": the unique part must be 8 or 17, not 18 characters long"
392        );
393    }
394
395    #[test]
396    fn test_error_non_alphanumeric() {
397        let result = AwsAmiId::try_from("ami-1234567!");
398        assert!(result.is_err());
399        assert_eq!(
400            result.unwrap_err().to_string(),
401           "failed to initialize AwsAmiId from \"ami-1234567!\": the unique part contains non ascii alphanumeric characters"
402        );
403    }
404
405    #[test]
406    fn test_valid_short_ids() {
407        assert_eq!(
408            AwsNetworkAclId::try_from("acl-1234abcd")
409                .unwrap()
410                .to_string(),
411            "acl-1234abcd"
412        );
413        assert_eq!(
414            AwsAmiId::try_from("ami-1234abcd").unwrap().to_string(),
415            "ami-1234abcd"
416        );
417        assert_eq!(
418            AwsCustomerGatewayId::try_from("cgw-1234abcd")
419                .unwrap()
420                .to_string(),
421            "cgw-1234abcd"
422        );
423        assert_eq!(
424            AwsElasticIpId::try_from("eipalloc-1234abcd")
425                .unwrap()
426                .to_string(),
427            "eipalloc-1234abcd"
428        );
429        assert_eq!(
430            AwsEfsFileSystemId::try_from("fs-1234abcd")
431                .unwrap()
432                .to_string(),
433            "fs-1234abcd"
434        );
435        assert_eq!(
436            AwsEfsMountTargetId::try_from("fsmt-1234abcd")
437                .unwrap()
438                .to_string(),
439            "fsmt-1234abcd"
440        );
441        assert_eq!(
442            AwsCloudFormationStackId::try_from("stack-1234abcd")
443                .unwrap()
444                .to_string(),
445            "stack-1234abcd"
446        );
447        assert_eq!(
448            AwsElasticBeanstalkEnvironmentId::try_from("e-1234abcd")
449                .unwrap()
450                .to_string(),
451            "e-1234abcd"
452        );
453        assert_eq!(
454            AwsInstanceId::try_from("i-1234abcd").unwrap().to_string(),
455            "i-1234abcd"
456        );
457        assert_eq!(
458            AwsInternetGatewayId::try_from("igw-1234abcd")
459                .unwrap()
460                .to_string(),
461            "igw-1234abcd"
462        );
463        assert_eq!(
464            AwsKeyPairId::try_from("key-1234abcd").unwrap().to_string(),
465            "key-1234abcd"
466        );
467        assert_eq!(
468            AwsLoadBalancerId::try_from("elbv2-1234abcd")
469                .unwrap()
470                .to_string(),
471            "elbv2-1234abcd"
472        );
473        assert_eq!(
474            AwsNatGatewayId::try_from("nat-1234abcd")
475                .unwrap()
476                .to_string(),
477            "nat-1234abcd"
478        );
479        assert_eq!(
480            AwsNetworkInterfaceId::try_from("eni-1234abcd")
481                .unwrap()
482                .to_string(),
483            "eni-1234abcd"
484        );
485        assert_eq!(
486            AwsPlacementGroupId::try_from("pg-1234abcd")
487                .unwrap()
488                .to_string(),
489            "pg-1234abcd"
490        );
491        assert_eq!(
492            AwsRdsInstanceId::try_from("db-1234abcd")
493                .unwrap()
494                .to_string(),
495            "db-1234abcd"
496        );
497        assert_eq!(
498            AwsRedshiftClusterId::try_from("redshift-1234abcd")
499                .unwrap()
500                .to_string(),
501            "redshift-1234abcd"
502        );
503        assert_eq!(
504            AwsRouteTableId::try_from("rtb-1234abcd")
505                .unwrap()
506                .to_string(),
507            "rtb-1234abcd"
508        );
509        assert_eq!(
510            AwsSecurityGroupId::try_from("sg-1234abcd")
511                .unwrap()
512                .to_string(),
513            "sg-1234abcd"
514        );
515        assert_eq!(
516            AwsSnapshotId::try_from("snap-1234abcd")
517                .unwrap()
518                .to_string(),
519            "snap-1234abcd"
520        );
521        assert_eq!(
522            AwsSubnetId::try_from("subnet-1234abcd")
523                .unwrap()
524                .to_string(),
525            "subnet-1234abcd"
526        );
527        assert_eq!(
528            AwsTargetGroupId::try_from("tg-1234abcd")
529                .unwrap()
530                .to_string(),
531            "tg-1234abcd"
532        );
533        assert_eq!(
534            AwsTransitGatewayAttachmentId::try_from("tgw-attach-1234abcd")
535                .unwrap()
536                .to_string(),
537            "tgw-attach-1234abcd"
538        );
539        assert_eq!(
540            AwsTransitGatewayId::try_from("tgw-1234abcd")
541                .unwrap()
542                .to_string(),
543            "tgw-1234abcd"
544        );
545        assert_eq!(
546            AwsVolumeId::try_from("vol-1234abcd").unwrap().to_string(),
547            "vol-1234abcd"
548        );
549        assert_eq!(
550            AwsVpcId::try_from("vpc-1234abcd").unwrap().to_string(),
551            "vpc-1234abcd"
552        );
553        assert_eq!(
554            AwsVpnConnectionId::try_from("vpn-1234abcd")
555                .unwrap()
556                .to_string(),
557            "vpn-1234abcd"
558        );
559        assert_eq!(
560            AwsVpnGatewayId::try_from("vgw-1234abcd")
561                .unwrap()
562                .to_string(),
563            "vgw-1234abcd"
564        );
565    }
566
567    #[test]
568    fn test_valid_long_ids() {
569        assert_eq!(
570            AwsNetworkAclId::try_from("acl-1a2b3c4d5e6f7j8h9")
571                .unwrap()
572                .to_string(),
573            "acl-1a2b3c4d5e6f7j8h9"
574        );
575        assert_eq!(
576            AwsAmiId::try_from("ami-1a2b3c4d5e6f7j8h9")
577                .unwrap()
578                .to_string(),
579            "ami-1a2b3c4d5e6f7j8h9"
580        );
581        assert_eq!(
582            AwsCustomerGatewayId::try_from("cgw-1a2b3c4d5e6f7j8h9")
583                .unwrap()
584                .to_string(),
585            "cgw-1a2b3c4d5e6f7j8h9"
586        );
587        assert_eq!(
588            AwsElasticIpId::try_from("eipalloc-1a2b3c4d5e6f7j8h9")
589                .unwrap()
590                .to_string(),
591            "eipalloc-1a2b3c4d5e6f7j8h9"
592        );
593        assert_eq!(
594            AwsEfsFileSystemId::try_from("fs-1a2b3c4d5e6f7j8h9")
595                .unwrap()
596                .to_string(),
597            "fs-1a2b3c4d5e6f7j8h9"
598        );
599        assert_eq!(
600            AwsEfsMountTargetId::try_from("fsmt-1a2b3c4d5e6f7j8h9")
601                .unwrap()
602                .to_string(),
603            "fsmt-1a2b3c4d5e6f7j8h9"
604        );
605        assert_eq!(
606            AwsCloudFormationStackId::try_from("stack-1a2b3c4d5e6f7j8h9")
607                .unwrap()
608                .to_string(),
609            "stack-1a2b3c4d5e6f7j8h9"
610        );
611        assert_eq!(
612            AwsElasticBeanstalkEnvironmentId::try_from("e-1a2b3c4d5e6f7j8h9")
613                .unwrap()
614                .to_string(),
615            "e-1a2b3c4d5e6f7j8h9"
616        );
617        assert_eq!(
618            AwsInstanceId::try_from("i-1a2b3c4d5e6f7j8h9")
619                .unwrap()
620                .to_string(),
621            "i-1a2b3c4d5e6f7j8h9"
622        );
623        assert_eq!(
624            AwsInternetGatewayId::try_from("igw-1a2b3c4d5e6f7j8h9")
625                .unwrap()
626                .to_string(),
627            "igw-1a2b3c4d5e6f7j8h9"
628        );
629        assert_eq!(
630            AwsKeyPairId::try_from("key-1a2b3c4d5e6f7j8h9")
631                .unwrap()
632                .to_string(),
633            "key-1a2b3c4d5e6f7j8h9"
634        );
635        assert_eq!(
636            AwsLoadBalancerId::try_from("elbv2-1a2b3c4d5e6f7j8h9")
637                .unwrap()
638                .to_string(),
639            "elbv2-1a2b3c4d5e6f7j8h9"
640        );
641        assert_eq!(
642            AwsNatGatewayId::try_from("nat-1a2b3c4d5e6f7j8h9")
643                .unwrap()
644                .to_string(),
645            "nat-1a2b3c4d5e6f7j8h9"
646        );
647        assert_eq!(
648            AwsNetworkInterfaceId::try_from("eni-1a2b3c4d5e6f7j8h9")
649                .unwrap()
650                .to_string(),
651            "eni-1a2b3c4d5e6f7j8h9"
652        );
653        assert_eq!(
654            AwsPlacementGroupId::try_from("pg-1a2b3c4d5e6f7j8h9")
655                .unwrap()
656                .to_string(),
657            "pg-1a2b3c4d5e6f7j8h9"
658        );
659        assert_eq!(
660            AwsRdsInstanceId::try_from("db-1a2b3c4d5e6f7j8h9")
661                .unwrap()
662                .to_string(),
663            "db-1a2b3c4d5e6f7j8h9"
664        );
665        assert_eq!(
666            AwsRedshiftClusterId::try_from("redshift-1a2b3c4d5e6f7j8h9")
667                .unwrap()
668                .to_string(),
669            "redshift-1a2b3c4d5e6f7j8h9"
670        );
671        assert_eq!(
672            AwsRouteTableId::try_from("rtb-1a2b3c4d5e6f7j8h9")
673                .unwrap()
674                .to_string(),
675            "rtb-1a2b3c4d5e6f7j8h9"
676        );
677        assert_eq!(
678            AwsSecurityGroupId::try_from("sg-1a2b3c4d5e6f7j8h9")
679                .unwrap()
680                .to_string(),
681            "sg-1a2b3c4d5e6f7j8h9"
682        );
683        assert_eq!(
684            AwsSnapshotId::try_from("snap-1a2b3c4d5e6f7j8h9")
685                .unwrap()
686                .to_string(),
687            "snap-1a2b3c4d5e6f7j8h9"
688        );
689        assert_eq!(
690            AwsSubnetId::try_from("subnet-1a2b3c4d5e6f7j8h9")
691                .unwrap()
692                .to_string(),
693            "subnet-1a2b3c4d5e6f7j8h9"
694        );
695        assert_eq!(
696            AwsTargetGroupId::try_from("tg-1a2b3c4d5e6f7j8h9")
697                .unwrap()
698                .to_string(),
699            "tg-1a2b3c4d5e6f7j8h9"
700        );
701        assert_eq!(
702            AwsTransitGatewayAttachmentId::try_from("tgw-attach-1a2b3c4d5e6f7j8h9")
703                .unwrap()
704                .to_string(),
705            "tgw-attach-1a2b3c4d5e6f7j8h9"
706        );
707        assert_eq!(
708            AwsTransitGatewayId::try_from("tgw-1a2b3c4d5e6f7j8h9")
709                .unwrap()
710                .to_string(),
711            "tgw-1a2b3c4d5e6f7j8h9"
712        );
713        assert_eq!(
714            AwsVolumeId::try_from("vol-1a2b3c4d5e6f7j8h9")
715                .unwrap()
716                .to_string(),
717            "vol-1a2b3c4d5e6f7j8h9"
718        );
719        assert_eq!(
720            AwsVpcId::try_from("vpc-1a2b3c4d5e6f7j8h9")
721                .unwrap()
722                .to_string(),
723            "vpc-1a2b3c4d5e6f7j8h9"
724        );
725        assert_eq!(
726            AwsVpnConnectionId::try_from("vpn-1a2b3c4d5e6f7j8h9")
727                .unwrap()
728                .to_string(),
729            "vpn-1a2b3c4d5e6f7j8h9"
730        );
731        assert_eq!(
732            AwsVpnGatewayId::try_from("vgw-1a2b3c4d5e6f7j8h9")
733                .unwrap()
734                .to_string(),
735            "vgw-1a2b3c4d5e6f7j8h9"
736        );
737    }
738}
739
740#[cfg(feature = "sqlx-postgres")]
741#[cfg(test)]
742mod sqlx_tests {
743    use super::*;
744    use sqlx::PgPool;
745
746    #[sqlx::test]
747    async fn serialize_varchar(pool: PgPool) -> sqlx::Result<()> {
748        let ami_str = "ami-12345678";
749        let ami: AwsAmiId = ami_str.parse().unwrap();
750        let serialized = sqlx::query_scalar!("SELECT $1::varchar", ami as _)
751            .fetch_one(&pool)
752            .await?
753            .unwrap();
754        assert_eq!(serialized, ami_str);
755        Ok(())
756    }
757
758    #[sqlx::test]
759    async fn serialize_text(pool: PgPool) -> sqlx::Result<()> {
760        let ami_str = "ami-12345678";
761        let ami: AwsAmiId = ami_str.parse().unwrap();
762        let serialized = sqlx::query_scalar!("SELECT $1::text", ami as _)
763            .fetch_one(&pool)
764            .await?
765            .unwrap();
766        assert_eq!(serialized, ami_str);
767        Ok(())
768    }
769
770    #[sqlx::test]
771    async fn deserialize_varchar(pool: PgPool) -> sqlx::Result<()> {
772        let ami: AwsAmiId = "ami-12345678".parse().unwrap();
773        let deserialized =
774            sqlx::query_scalar!(r#"SELECT 'ami-12345678'::varchar as "val: AwsAmiId""#)
775                .fetch_one(&pool)
776                .await?
777                .unwrap();
778        assert_eq!(deserialized, ami);
779        Ok(())
780    }
781
782    #[sqlx::test]
783    async fn deserialize_text(pool: PgPool) -> sqlx::Result<()> {
784        let ami: AwsAmiId = "ami-12345678".parse().unwrap();
785        let deserialized = sqlx::query_scalar!(r#"SELECT 'ami-12345678' as "val: AwsAmiId""#)
786            .fetch_one(&pool)
787            .await?
788            .unwrap();
789        assert_eq!(deserialized, ami);
790        Ok(())
791    }
792}