Skip to main content

fakecloud_aws/
arn.rs

1use std::fmt;
2
3/// An Amazon Resource Name.
4///
5/// Format: `arn:partition:service:region:account-id:resource`
6#[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    /// Create an ARN with no region (global services like IAM).
27    pub fn global(service: &str, account_id: &str, resource: &str) -> Self {
28        Self::new(service, "", account_id, resource)
29    }
30
31    /// Create an S3 ARN — no region, no account.
32    /// Format: `arn:aws:s3:::resource`.
33    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    /// Override the partition (default `aws`). Use for `aws-cn` / `aws-us-gov`.
44    pub fn with_partition(mut self, partition: &str) -> Self {
45        self.partition = partition.to_string();
46        self
47    }
48}
49
50/// Map an AWS region name to its partition. Mirrors the AWS SDK's
51/// region-to-partition lookup so synthesized ARNs in cn/gov-cloud
52/// regions emit the correct partition prefix.
53pub 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}