Skip to main content

rustfs_targets/
arn.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::TargetError;
16use rustfs_config::notify::{ARN_PREFIX, DEFAULT_ARN_PARTITION, DEFAULT_ARN_SERVICE};
17use serde::{Deserialize, Deserializer, Serialize, Serializer};
18use std::fmt;
19use std::str::FromStr;
20use thiserror::Error;
21
22#[derive(Debug, Error)]
23pub enum TargetIDError {
24    #[error("Invalid TargetID format '{0}', expect 'ID:Name'")]
25    InvalidFormat(String),
26}
27
28/// Target ID, used to identify notification targets
29#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
30pub struct TargetID {
31    pub id: String,
32    pub name: String,
33}
34
35impl TargetID {
36    pub fn new(id: String, name: String) -> Self {
37        Self { id, name }
38    }
39
40    /// Create an ARN
41    pub fn to_arn(&self, region: &str) -> ARN {
42        ARN {
43            target_id: self.clone(),
44            region: region.to_string(),
45            service: DEFAULT_ARN_SERVICE.to_string(),     // Default Service
46            partition: DEFAULT_ARN_PARTITION.to_string(), // Default partition
47        }
48    }
49}
50
51impl fmt::Display for TargetID {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "{}:{}", self.id, self.name)
54    }
55}
56
57impl FromStr for TargetID {
58    type Err = TargetIDError;
59
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        let parts: Vec<&str> = s.splitn(2, ':').collect();
62        if parts.len() == 2 {
63            Ok(TargetID {
64                id: parts[0].to_string(),
65                name: parts[1].to_string(),
66            })
67        } else {
68            Err(TargetIDError::InvalidFormat(s.to_string()))
69        }
70    }
71}
72
73impl Serialize for TargetID {
74    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
75    where
76        S: Serializer,
77    {
78        serializer.serialize_str(&self.to_string())
79    }
80}
81
82impl<'de> Deserialize<'de> for TargetID {
83    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
84    where
85        D: Deserializer<'de>,
86    {
87        let s = String::deserialize(deserializer)?;
88        TargetID::from_str(&s).map_err(serde::de::Error::custom)
89    }
90}
91
92#[derive(Debug, Error)]
93pub enum ArnError {
94    #[error("Invalid ARN format '{0}'")]
95    InvalidFormat(String),
96    #[error("ARN component missing")]
97    MissingComponents,
98}
99
100/// ARN - AWS resource name representation
101#[derive(Debug, Clone, Eq, PartialEq)]
102pub struct ARN {
103    pub target_id: TargetID,
104    pub region: String,
105    // Service types, such as "sqs", "sns", "lambda", etc. This defaults to "sqs" to match the Go example.
106    pub service: String,
107    // Partitions such as "aws", "aws-cn", or customizations such as "rustfs", etc.
108    pub partition: String,
109}
110
111impl ARN {
112    pub fn new(target_id: TargetID, region: String) -> Self {
113        ARN {
114            target_id,
115            region,
116            service: DEFAULT_ARN_SERVICE.to_string(),     // Default is sqs
117            partition: DEFAULT_ARN_PARTITION.to_string(), // Default is rustfs partition
118        }
119    }
120
121    /// Returns the string representation of ARN
122    /// Returns the ARN string in the format "{ARN_PREFIX}:{region}:{target_id}"
123    #[allow(clippy::inherent_to_string)]
124    pub fn to_arn_string(&self) -> String {
125        if self.target_id.id.is_empty() && self.target_id.name.is_empty() && self.region.is_empty() {
126            return String::new();
127        }
128        format!("{}:{}:{}", ARN_PREFIX, self.region, self.target_id)
129    }
130
131    /// Parsing ARN from string
132    /// Only accepts ARNs with the RustFS prefix: "arn:rustfs:sqs:"
133    /// Format: arn:rustfs:sqs:{region}:{id}:{name}
134    pub fn parse(s: &str) -> Result<Self, TargetError> {
135        if !s.starts_with(ARN_PREFIX) {
136            return Err(TargetError::InvalidARN(s.to_string()));
137        }
138
139        let tokens: Vec<&str> = s.split(':').collect();
140        if tokens.len() != 6 {
141            return Err(TargetError::InvalidARN(s.to_string()));
142        }
143
144        if tokens[4].is_empty() || tokens[5].is_empty() {
145            return Err(TargetError::InvalidARN(s.to_string()));
146        }
147
148        Ok(ARN {
149            region: tokens[3].to_string(),
150            target_id: TargetID {
151                id: tokens[4].to_string(),
152                name: tokens[5].to_string(),
153            },
154            service: tokens[2].to_string(),   // Service Type
155            partition: tokens[1].to_string(), // Partition
156        })
157    }
158}
159
160impl fmt::Display for ARN {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        if self.target_id.id.is_empty() && self.target_id.name.is_empty() && self.region.is_empty() {
163            // Returns an empty string if all parts are empty
164            return Ok(());
165        }
166        write!(
167            f,
168            "arn:{}:{}:{}:{}:{}",
169            self.partition, self.service, self.region, self.target_id.id, self.target_id.name
170        )
171    }
172}
173
174impl FromStr for ARN {
175    type Err = ArnError;
176
177    fn from_str(s: &str) -> Result<Self, Self::Err> {
178        let parts: Vec<&str> = s.split(':').collect();
179        if parts.len() < 6 {
180            return Err(ArnError::InvalidFormat(s.to_string()));
181        }
182
183        if parts[0] != "arn" {
184            return Err(ArnError::InvalidFormat(s.to_string()));
185        }
186
187        let partition = parts[1].to_string();
188        let service = parts[2].to_string();
189        let region = parts[3].to_string();
190        let id = parts[4].to_string();
191        let name = parts[5..].join(":"); // The name section may contain colons, although this is not usually the case in SQS ARN
192
193        if id.is_empty() || name.is_empty() {
194            return Err(ArnError::MissingComponents);
195        }
196
197        Ok(ARN {
198            target_id: TargetID { id, name },
199            region,
200            service,
201            partition,
202        })
203    }
204}
205
206// Serialization implementation
207impl Serialize for ARN {
208    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
209    where
210        S: Serializer,
211    {
212        serializer.serialize_str(&self.to_string())
213    }
214}
215
216impl<'de> Deserialize<'de> for ARN {
217    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
218    where
219        D: Deserializer<'de>,
220    {
221        // deserializer.deserialize_str(ARNVisitor)
222        let s = String::deserialize(deserializer)?;
223        if s.is_empty() {
224            // Handle an empty ARN string, for example, creating an empty or default Arn instance
225            // Or return an error based on business logic
226            // Here we create an empty TargetID and region Arn
227            return Ok(ARN {
228                target_id: TargetID {
229                    id: String::new(),
230                    name: String::new(),
231                },
232                region: String::new(),
233                service: DEFAULT_ARN_SERVICE.to_string(),
234                partition: DEFAULT_ARN_PARTITION.to_string(),
235            });
236        }
237        ARN::from_str(&s).map_err(serde::de::Error::custom)
238    }
239}