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    /// Create an S3 Access Point ARN.
44    /// Format: `arn:aws:s3:<region>:<account-id>:accesspoint/<name>`.
45    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    /// Override the partition (default `aws`). Use for `aws-cn` / `aws-us-gov`.
56    pub fn with_partition(mut self, partition: &str) -> Self {
57        self.partition = partition.to_string();
58        self
59    }
60}
61
62/// Map an AWS region name to its partition. Mirrors the AWS SDK's
63/// region-to-partition lookup so synthesized ARNs in cn/gov-cloud
64/// regions emit the correct partition prefix.
65pub 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}