1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash)]
7pub struct Arn {
8 pub partition: String,
9 pub service: String,
10 pub region: String,
11 pub account_id: String,
12 pub resource: String,
13}
14
15impl Arn {
16 pub fn new(service: &str, region: &str, account_id: &str, resource: &str) -> Self {
17 Self {
18 partition: "aws".to_string(),
19 service: service.to_string(),
20 region: region.to_string(),
21 account_id: account_id.to_string(),
22 resource: resource.to_string(),
23 }
24 }
25
26 pub fn global(service: &str, account_id: &str, resource: &str) -> Self {
28 Self::new(service, "", account_id, resource)
29 }
30
31 pub fn s3(resource: &str) -> Self {
34 Self {
35 partition: "aws".to_string(),
36 service: "s3".to_string(),
37 region: String::new(),
38 account_id: String::new(),
39 resource: resource.to_string(),
40 }
41 }
42
43 pub fn with_partition(mut self, partition: &str) -> Self {
45 self.partition = partition.to_string();
46 self
47 }
48}
49
50pub fn partition_for(region: &str) -> &'static str {
54 if region.starts_with("cn-") {
55 "aws-cn"
56 } else if region.starts_with("us-gov-") {
57 "aws-us-gov"
58 } else if region.starts_with("us-iso-") {
59 "aws-iso"
60 } else if region.starts_with("us-isob-") {
61 "aws-iso-b"
62 } else {
63 "aws"
64 }
65}
66
67impl fmt::Display for Arn {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 write!(
70 f,
71 "arn:{}:{}:{}:{}:{}",
72 self.partition, self.service, self.region, self.account_id, self.resource
73 )
74 }
75}
76
77impl std::str::FromStr for Arn {
78 type Err = ArnParseError;
79
80 fn from_str(s: &str) -> Result<Self, Self::Err> {
81 let parts: Vec<&str> = s.splitn(6, ':').collect();
82 if parts.len() != 6 || parts[0] != "arn" {
83 return Err(ArnParseError(s.to_string()));
84 }
85 Ok(Self {
86 partition: parts[1].to_string(),
87 service: parts[2].to_string(),
88 region: parts[3].to_string(),
89 account_id: parts[4].to_string(),
90 resource: parts[5].to_string(),
91 })
92 }
93}
94
95#[derive(Debug, thiserror::Error)]
96#[error("invalid ARN: {0}")]
97pub struct ArnParseError(String);
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn round_trip() {
105 let arn = Arn::new("sqs", "us-east-1", "123456789012", "my-queue");
106 let s = arn.to_string();
107 assert_eq!(s, "arn:aws:sqs:us-east-1:123456789012:my-queue");
108 assert_eq!(s.parse::<Arn>().unwrap(), arn);
109 }
110
111 #[test]
112 fn global_arn() {
113 let arn = Arn::global("iam", "123456789012", "user/admin");
114 assert_eq!(arn.to_string(), "arn:aws:iam::123456789012:user/admin");
115 }
116
117 #[test]
118 fn s3_arn() {
119 let arn = Arn::s3("my-bucket");
120 assert_eq!(arn.to_string(), "arn:aws:s3:::my-bucket");
121 let object = Arn::s3("my-bucket/key.txt");
122 assert_eq!(object.to_string(), "arn:aws:s3:::my-bucket/key.txt");
123 }
124
125 #[test]
126 fn with_partition_overrides() {
127 let arn = Arn::new("sqs", "cn-north-1", "123", "q").with_partition("aws-cn");
128 assert_eq!(arn.to_string(), "arn:aws-cn:sqs:cn-north-1:123:q");
129 }
130
131 #[test]
132 fn partition_for_region() {
133 assert_eq!(partition_for("us-east-1"), "aws");
134 assert_eq!(partition_for("eu-west-1"), "aws");
135 assert_eq!(partition_for("cn-north-1"), "aws-cn");
136 assert_eq!(partition_for("cn-northwest-1"), "aws-cn");
137 assert_eq!(partition_for("us-gov-west-1"), "aws-us-gov");
138 assert_eq!(partition_for("us-iso-east-1"), "aws-iso");
139 assert_eq!(partition_for("us-isob-east-1"), "aws-iso-b");
140 }
141}