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 s3_access_point(region: &str, account_id: &str, name: &str) -> Self {
46 Self {
47 partition: partition_for(region).to_string(),
48 service: "s3".to_string(),
49 region: region.to_string(),
50 account_id: account_id.to_string(),
51 resource: format!("accesspoint/{name}"),
52 }
53 }
54
55 pub fn with_partition(mut self, partition: &str) -> Self {
57 self.partition = partition.to_string();
58 self
59 }
60}
61
62pub fn partition_for(region: &str) -> &'static str {
66 if region.starts_with("cn-") {
67 "aws-cn"
68 } else if region.starts_with("us-gov-") {
69 "aws-us-gov"
70 } else if region.starts_with("us-iso-") {
71 "aws-iso"
72 } else if region.starts_with("us-isob-") {
73 "aws-iso-b"
74 } else {
75 "aws"
76 }
77}
78
79impl fmt::Display for Arn {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(
82 f,
83 "arn:{}:{}:{}:{}:{}",
84 self.partition, self.service, self.region, self.account_id, self.resource
85 )
86 }
87}
88
89impl std::str::FromStr for Arn {
90 type Err = ArnParseError;
91
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
93 let parts: Vec<&str> = s.splitn(6, ':').collect();
94 if parts.len() != 6 || parts[0] != "arn" {
95 return Err(ArnParseError(s.to_string()));
96 }
97 Ok(Self {
98 partition: parts[1].to_string(),
99 service: parts[2].to_string(),
100 region: parts[3].to_string(),
101 account_id: parts[4].to_string(),
102 resource: parts[5].to_string(),
103 })
104 }
105}
106
107#[derive(Debug, thiserror::Error)]
108#[error("invalid ARN: {0}")]
109pub struct ArnParseError(String);
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn round_trip() {
117 let arn = Arn::new("sqs", "us-east-1", "123456789012", "my-queue");
118 let s = arn.to_string();
119 assert_eq!(s, "arn:aws:sqs:us-east-1:123456789012:my-queue");
120 assert_eq!(s.parse::<Arn>().unwrap(), arn);
121 }
122
123 #[test]
124 fn global_arn() {
125 let arn = Arn::global("iam", "123456789012", "user/admin");
126 assert_eq!(arn.to_string(), "arn:aws:iam::123456789012:user/admin");
127 }
128
129 #[test]
130 fn s3_arn() {
131 let arn = Arn::s3("my-bucket");
132 assert_eq!(arn.to_string(), "arn:aws:s3:::my-bucket");
133 let object = Arn::s3("my-bucket/key.txt");
134 assert_eq!(object.to_string(), "arn:aws:s3:::my-bucket/key.txt");
135 }
136
137 #[test]
138 fn with_partition_overrides() {
139 let arn = Arn::new("sqs", "cn-north-1", "123", "q").with_partition("aws-cn");
140 assert_eq!(arn.to_string(), "arn:aws-cn:sqs:cn-north-1:123:q");
141 }
142
143 #[test]
144 fn partition_for_region() {
145 assert_eq!(partition_for("us-east-1"), "aws");
146 assert_eq!(partition_for("eu-west-1"), "aws");
147 assert_eq!(partition_for("cn-north-1"), "aws-cn");
148 assert_eq!(partition_for("cn-northwest-1"), "aws-cn");
149 assert_eq!(partition_for("us-gov-west-1"), "aws-us-gov");
150 assert_eq!(partition_for("us-iso-east-1"), "aws-iso");
151 assert_eq!(partition_for("us-isob-east-1"), "aws-iso-b");
152 }
153}