aws_arn/
lib.rs

1/*!
2* Provides types, builders, and other helpers to manipulate AWS [Amazon
3* Resource Name
4* (ResourceName)](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html)
5* strings.
6*
7* The ResourceName is a key component of all AWS service APIs and yet nearly
8* all client toolkits treat it simply as a string. While this may be a
9* reasonable and expedient decision, it seems there might be a need to not
10* only ensure correctness of ResourceNames with validators but also
11* constructors that allow making these strings correclt in the first place.
12*
13* # ResourceName Types
14*
15* This crate provides a number of levels of ResourceName manipulation, the
16* first is the direct construction of an ResourceName type using the core
17* `ResourceName`, `Identifier`, `AccountIdentifier`, and `ResourceIdentifier`
18* types.
19*
20* ```rust
21* use aws_arn::{ResourceName, ResourceIdentifier};
22* use aws_arn::known::{Partition, Service};
23* use std::str::FromStr;
24*
25* let arn = ResourceName {
26*     partition: Some(Partition::default().into()),
27*     service: Service::S3.into(),
28*     region: None,
29*     account_id: None,
30*     resource: ResourceIdentifier::from_str("mythings/thing-1").unwrap()
31* };
32* ```
33*
34* In the example above, as we are defining a minimal ResourceName we could use one of the defined constructor
35* functions.
36*
37* ```rust
38* use aws_arn::{ResourceName, ResourceIdentifier};
39* use aws_arn::known::Service;
40* use std::str::FromStr;
41*
42* let arn = ResourceName::aws(
43*     Service::S3.into(),
44*     ResourceIdentifier::from_str("mythings/thing-1").unwrap()
45* );
46* ```
47*
48* Alternatively, using `FromStr,` you can parse an existing ResourceName string into an ResourceName value.
49*
50* ```rust
51* use aws_arn::ResourceName;
52* use std::str::FromStr;
53*
54* let arn: ResourceName = "arn:aws:s3:::mythings/thing-1"
55*     .parse()
56*     .expect("didn't look like an ResourceName");
57* ```
58*
59* Another approach is to use a more readable *builder* which also allows you to ignore those fields
60* in the ResourceName you don't always need and uses a more fluent style of ResourceName construction.
61*
62* ```rust
63* use aws_arn::builder::{ArnBuilder, ResourceBuilder};
64* use aws_arn::known::{Partition, Service};
65* use aws_arn::{ResourceName, Identifier, IdentifierLike};
66* use std::str::FromStr;
67*
68* let arn: ResourceName = ArnBuilder::service_id(Service::S3.into())
69*     .resource(ResourceBuilder::named(Identifier::from_str("mythings").unwrap())
70*         .resource_name(Identifier::new_unchecked("my-layer"))
71*         .build_resource_path())
72*     .in_partition_id(Partition::Aws.into())
73*     .into();
74* ```
75*
76* Finally, it is possible to use resource-type specific functions that allow an even more direct and
77* simple construction (module `aws_arn::builder::{service}` - *service builder functions*, although
78* at this time there are few supported services.
79*
80* ```rust
81* use aws_arn::builder::s3;
82* use aws_arn::Identifier;
83* use std::str::FromStr;
84*
85* let arn = s3::object(
86*     Identifier::from_str("mythings").unwrap(),
87*     Identifier::from_str("thing-1").unwrap(),
88* );
89* ```
90*
91* For more, see the AWS documentation for [Amazon Resource Name
92* (ResourceName)](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) documentation.
93*
94* # Optional Features
95*
96* This crate has attempted to be as lean as possible, with a really minimal set of dependencies,
97* we have include the following capabilities as optional features.
98*
99* * `builders` adds the builder module. This feature is enabled by default, it also requires the
100*   `known` feature.
101* * `known` adds a module containing enums for partitions, regions, and services.
102*   This feature is enabled by default.
103* * `serde_support` adds derived `Serialize` and `Deserialize` implementations for the `ResourceName` and
104*   `Resource` types. This feature is enabled by default.
105*
106*/
107
108// ------------------------------------------------------------------------------------------------
109// Preamble
110// ------------------------------------------------------------------------------------------------
111
112#![warn(
113    // ---------- Stylistic
114    future_incompatible,
115    nonstandard_style,
116    rust_2018_idioms,
117    trivial_casts,
118    trivial_numeric_casts,
119    // ---------- Public
120    missing_debug_implementations,
121    missing_docs,
122    unreachable_pub,
123    // ---------- Unsafe
124    unsafe_code,
125    // ---------- Unused
126    unused_extern_crates,
127    unused_import_braces,
128    unused_qualifications,
129    unused_results,
130)]
131
132use lazy_static::lazy_static;
133use regex::{Captures, Regex};
134#[cfg(feature = "serde_support")]
135use serde::{Deserialize, Serialize};
136use std::collections::HashMap;
137use std::fmt::{Debug, Display, Formatter};
138use std::ops::Deref;
139use std::str::FromStr;
140
141// ------------------------------------------------------------------------------------------------
142// Public Types
143// ------------------------------------------------------------------------------------------------
144
145/// This trait is implemented by the `ResourceName` component types. It
146/// represents a string-based identifier that is generally constructed using
147/// `FromStr::from_str`.
148///
149pub trait IdentifierLike
150where
151    Self: Clone + Display + FromStr + Deref<Target = str>,
152{
153    /// Construct a new `Identifier` from the provided string **without** checking it's validity.
154    /// This can be a useful method to improve performance for statically, or well-known, values;
155    /// however, in general `FromStr::from_str` should be used.
156    fn new_unchecked(s: &str) -> Self
157    where
158        Self: Sized;
159
160    /// Returns `true` if the provided string is a valid `Identifier` value, else `false`.
161    fn is_valid(s: &str) -> bool;
162
163    /// Construct an account identifier that represents *any*.
164    fn any() -> Self {
165        Self::new_unchecked(STRING_WILD_ANY)
166    }
167
168    /// Return `true` if this is simply the *any* wildcard, else `false`.
169    fn is_any(&self) -> bool {
170        self.deref().chars().any(|c| c == CHAR_WILD_ANY)
171    }
172
173    /// Returns `true` if this identifier contains any wildcard characeters,
174    /// else `false`.
175    fn has_wildcards(&self) -> bool {
176        self.deref()
177            .chars()
178            .any(|c| c == CHAR_WILD_ONE || c == CHAR_WILD_ANY)
179    }
180
181    /// Return `true` if this identifier has no wildcards, else `false`.
182    fn is_plain(&self) -> bool {
183        !self.has_wildcards()
184    }
185}
186
187///
188/// A string value that is used to capture the partition, service, and region components
189/// of an ResourceName. These are ASCII only, may not include control characters, spaces, '/', or ':'.
190///
191#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
192#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
193pub struct Identifier(String);
194
195///
196/// A string value that is used to capture the account ID component
197/// of an ResourceName. These are ASCII digits only and a fixed length of 12 characters.
198///
199#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
200#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
201pub struct AccountIdentifier(String);
202
203///
204/// A string value that is used to capture the resource component of an ResourceName. These are ASCII only,
205/// may not include control characters but unlike `Identifier` they may include spaces, '/', and ':'.
206///
207/// > *The content of this part of the ResourceName varies by service. A resource identifier can be the name
208/// > or ID of the resource (for example, `user/Bob` or `instance/i-1234567890abcdef0`) or a
209/// > resource path. For example, some resource identifiers include a parent resource
210/// > (`sub-resource-type/parent-resource/sub-resource`) or a qualifier such as a version
211/// > (`resource-type:resource-name:qualifier`).*
212///
213/// > *Some resource ResourceNames can include a path. For example, in Amazon S3, the resource identifier
214/// > is an object name that can include slashes ('/') to form a path. Similarly, IAM user names
215/// > and group names can include paths.*
216///
217/// > *In some circumstances, paths can include a wildcard character, namely an asterisk ('*').*
218///
219#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
220#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
221pub struct ResourceIdentifier(String);
222
223///
224/// Amazon Resource Names (ResourceNames) uniquely identify AWS resources. We require an ResourceName when you
225/// need to specify a resource unambiguously across all of AWS, such as in IAM policies,
226/// Amazon Relational Database Service (Amazon RDS) tags, and API calls.
227///
228/// The following are the general formats for ResourceNames; the specific components and values used
229/// depend on the AWS service.
230///
231/// ```text
232/// arn:partition:service:region:account-id:resource-id
233/// arn:partition:service:region:account-id:resource-type/resource-id
234/// arn:partition:service:region:account-id:resource-type:resource-id
235/// ```
236///
237/// From [ResourceName Format](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arns-syntax)
238///
239#[allow(clippy::upper_case_acronyms)]
240#[derive(Debug, Clone, PartialEq)]
241#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
242pub struct ResourceName {
243    /// The partition that the resource is in. For standard AWS Regions, the partition is` aws`.
244    /// If you have resources in other partitions, the partition is `aws-partitionname`. For
245    /// example, the partition for resources in the China partition is `aws-cn`. The module
246    /// `known::partition` provides common values as constants (if the `known` feature is
247    /// enabled).
248    pub partition: Option<Identifier>,
249    /// The service namespace that identifies the AWS. The module `known::service` provides
250    //  common values as constants (if the `known` feature is enabled).
251    pub service: Identifier,
252    /// The Region that the resource resides in. The ResourceNames for some resources do not require
253    /// a Region, so this component might be omitted. The module `known::region` provides
254    /// common values as constants (if the `known` feature is enabled).
255    pub region: Option<Identifier>,
256    /// The ID of the AWS account that owns the resource, without the hyphens. For example,
257    /// `123456789012`. The ResourceNames for some resources don't require an account number, so this
258    /// component may be omitted.
259    pub account_id: Option<AccountIdentifier>,
260    /// The content of this part of the ResourceName varies by service. A resource identifier can
261    /// be the name or ID of the resource (for example, `user/Bob` or
262    /// `instance/i-1234567890abcdef0`) or a resource path. For example, some resource
263    /// identifiers include a parent resource
264    /// (`sub-resource-type/parent-resource/sub-resource`) or a qualifier such as a
265    /// version (`resource-type:resource-name:qualifier`).
266    pub resource: ResourceIdentifier,
267}
268
269// ------------------------------------------------------------------------------------------------
270// Implementations
271// ------------------------------------------------------------------------------------------------
272
273const ARN_PREFIX: &str = "arn";
274
275const PART_SEPARATOR: char = ':';
276const PATH_SEPARATOR: char = '/';
277
278const STRING_WILD_ANY: &str = "*";
279
280const CHAR_ASCII_START: char = '\u{1F}';
281const CHAR_ASCII_END: char = '\u{7F}';
282const CHAR_SPACE: char = ' ';
283const CHAR_WILD_ONE: char = '?';
284const CHAR_WILD_ANY: char = '*';
285
286const REQUIRED_COMPONENT_COUNT: usize = 6;
287
288const PARTITION_AWS_PREFIX: &str = "aws";
289const PARTITION_AWS_OTHER_PREFIX: &str = "aws-";
290
291lazy_static! {
292    static ref REGEX_VARIABLE: Regex = Regex::new(r"\$\{([^$}]+)\}").unwrap();
293}
294
295// ------------------------------------------------------------------------------------------------
296
297impl Display for Identifier {
298    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
299        write!(f, "{}", self.0)
300    }
301}
302
303impl FromStr for Identifier {
304    type Err = Error;
305
306    fn from_str(s: &str) -> Result<Self, Self::Err> {
307        if Self::is_valid(s) {
308            Ok(Self(s.to_string()))
309        } else {
310            Err(Error::InvalidIdentifier(s.to_string()))
311        }
312    }
313}
314
315impl From<Identifier> for String {
316    fn from(v: Identifier) -> Self {
317        v.0
318    }
319}
320
321impl Deref for Identifier {
322    type Target = str;
323
324    fn deref(&self) -> &Self::Target {
325        &self.0
326    }
327}
328
329impl IdentifierLike for Identifier {
330    fn new_unchecked(s: &str) -> Self {
331        Self(s.to_string())
332    }
333
334    fn is_valid(s: &str) -> bool {
335        !s.is_empty()
336            && s.chars().all(|c| {
337                c > CHAR_ASCII_START
338                    && c < CHAR_ASCII_END
339                    && c != CHAR_SPACE
340                    && c != PATH_SEPARATOR
341                    && c != PART_SEPARATOR
342            })
343    }
344}
345
346// ------------------------------------------------------------------------------------------------
347
348impl Display for AccountIdentifier {
349    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
350        write!(f, "{}", self.0)
351    }
352}
353
354impl FromStr for AccountIdentifier {
355    type Err = Error;
356
357    fn from_str(s: &str) -> Result<Self, Self::Err> {
358        if Self::is_valid(s) {
359            Ok(Self(s.to_string()))
360        } else {
361            Err(Error::InvalidAccountId(s.to_string()))
362        }
363    }
364}
365
366impl From<AccountIdentifier> for String {
367    fn from(v: AccountIdentifier) -> Self {
368        v.0
369    }
370}
371
372impl From<AccountIdentifier> for ResourceName {
373    fn from(account: AccountIdentifier) -> Self {
374        ResourceName::from_str(&format!("arn:aws:iam::{}:root", account)).unwrap()
375    }
376}
377
378impl Deref for AccountIdentifier {
379    type Target = str;
380
381    fn deref(&self) -> &Self::Target {
382        &self.0
383    }
384}
385
386impl IdentifierLike for AccountIdentifier {
387    fn new_unchecked(s: &str) -> Self {
388        Self(s.to_string())
389    }
390
391    fn is_valid(s: &str) -> bool {
392        (s.len() == 12 && s.chars().all(|c| c.is_ascii_digit()))
393            || (!s.is_empty()
394                && s.len() <= 12
395                && s.chars()
396                    .all(|c| c.is_ascii_digit() || c == CHAR_WILD_ONE || c == CHAR_WILD_ANY)
397                && s.chars().any(|c| c == CHAR_WILD_ONE || c == CHAR_WILD_ANY))
398    }
399}
400
401// ------------------------------------------------------------------------------------------------
402
403impl Display for ResourceIdentifier {
404    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
405        write!(f, "{}", self.0)
406    }
407}
408
409impl FromStr for ResourceIdentifier {
410    type Err = Error;
411
412    fn from_str(s: &str) -> Result<Self, Self::Err> {
413        if Self::is_valid(s) {
414            Ok(Self(s.to_string()))
415        } else {
416            Err(Error::InvalidResource(s.to_string()))
417        }
418    }
419}
420
421impl From<ResourceIdentifier> for String {
422    fn from(v: ResourceIdentifier) -> Self {
423        v.0
424    }
425}
426
427impl From<Identifier> for ResourceIdentifier {
428    fn from(v: Identifier) -> Self {
429        ResourceIdentifier::new_unchecked(&v.0)
430    }
431}
432
433impl Deref for ResourceIdentifier {
434    type Target = str;
435
436    fn deref(&self) -> &Self::Target {
437        &self.0
438    }
439}
440
441impl IdentifierLike for ResourceIdentifier {
442    fn new_unchecked(s: &str) -> Self {
443        Self(s.to_string())
444    }
445
446    fn is_valid(s: &str) -> bool {
447        !s.is_empty() && s.chars().all(|c| c > '\u{1F}' && c < '\u{7F}')
448    }
449
450    fn is_plain(&self) -> bool {
451        !self.has_wildcards() && !self.has_variables()
452    }
453}
454
455impl ResourceIdentifier {
456    /// Construct a resource identifier, as a path, using the `Identifier` path components.
457    pub fn from_id_path(path: &[Identifier]) -> Self {
458        Self::new_unchecked(
459            &path
460                .iter()
461                .map(Identifier::to_string)
462                .collect::<Vec<String>>()
463                .join(&PATH_SEPARATOR.to_string()),
464        )
465    }
466
467    /// Construct a resource identifier, as a qualified ID, using the `Identifier` path components.
468    pub fn from_qualified_id(qualified: &[Identifier]) -> Self {
469        Self::new_unchecked(
470            &qualified
471                .iter()
472                .map(Identifier::to_string)
473                .collect::<Vec<String>>()
474                .join(&PART_SEPARATOR.to_string()),
475        )
476    }
477
478    /// Construct a resource identifier, as a path, using the `ResourceIdentifier` path components.
479    pub fn from_path(path: &[ResourceIdentifier]) -> Self {
480        Self::new_unchecked(
481            &path
482                .iter()
483                .map(ResourceIdentifier::to_string)
484                .collect::<Vec<String>>()
485                .join(&PATH_SEPARATOR.to_string()),
486        )
487    }
488
489    /// Construct a resource identifier, as a qualified ID, using the `ResourceIdentifier` path components.
490    pub fn from_qualified(qualified: &[ResourceIdentifier]) -> Self {
491        Self::new_unchecked(
492            &qualified
493                .iter()
494                .map(ResourceIdentifier::to_string)
495                .collect::<Vec<String>>()
496                .join(&PART_SEPARATOR.to_string()),
497        )
498    }
499
500    /// Return `true` if this identifier contains path separator characters, else `false`.
501    pub fn contains_path(&self) -> bool {
502        self.0.contains(PATH_SEPARATOR)
503    }
504
505    /// Return the list of path components when split using the path separator character.
506    pub fn path_split(&self) -> Vec<ResourceIdentifier> {
507        self.0
508            .split(PATH_SEPARATOR)
509            .map(ResourceIdentifier::new_unchecked)
510            .collect()
511    }
512
513    /// Return `true` if this identifier contains qualifier separator characters, else `false`.
514    pub fn contains_qualified(&self) -> bool {
515        self.0.contains(PART_SEPARATOR)
516    }
517
518    /// Return the list of path components when split using the qualifier separator character.
519    pub fn qualifier_split(&self) -> Vec<ResourceIdentifier> {
520        self.0
521            .split(PART_SEPARATOR)
522            .map(ResourceIdentifier::new_unchecked)
523            .collect()
524    }
525
526    /// Return `true` if the identifier contains variables of the form
527    /// `${name}`, else `false`.
528    pub fn has_variables(&self) -> bool {
529        REGEX_VARIABLE.is_match(self.deref())
530    }
531
532    /// Replace any variables in the string with values from the context,
533    /// returning a new value if the replacements result in a legal identifier
534    /// string. The
535    pub fn replace_variables<V>(&self, context: &HashMap<String, V>) -> Result<Self, Error>
536    where
537        V: Clone + Into<String>,
538    {
539        let new_text = REGEX_VARIABLE.replace_all(self.deref(), |caps: &Captures<'_>| {
540            if let Some(value) = context.get(&caps[1]) {
541                value.clone().into()
542            } else {
543                format!("${{{}}}", &caps[1])
544            }
545        });
546        Self::from_str(&new_text)
547    }
548}
549
550// ------------------------------------------------------------------------------------------------
551
552impl Display for ResourceName {
553    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
554        write!(
555            f,
556            "{}",
557            vec![
558                ARN_PREFIX.to_string(),
559                self.partition
560                    .as_ref()
561                    .unwrap_or(&known::Partition::default().into())
562                    .to_string(),
563                self.service.to_string(),
564                self.region
565                    .as_ref()
566                    .unwrap_or(&Identifier::default())
567                    .to_string(),
568                self.account_id
569                    .as_ref()
570                    .unwrap_or(&AccountIdentifier::default())
571                    .to_string(),
572                self.resource.to_string()
573            ]
574            .join(&PART_SEPARATOR.to_string())
575        )
576    }
577}
578
579impl FromStr for ResourceName {
580    type Err = Error;
581
582    ///
583    /// Format:
584    ///
585    /// * `arn:partition:service:region:account-id: | resource part |`
586    ///
587    fn from_str(s: &str) -> Result<Self, Self::Err> {
588        let mut parts: Vec<&str> = s.split(PART_SEPARATOR).collect();
589        if parts.len() < REQUIRED_COMPONENT_COUNT {
590            Err(Error::TooFewComponents)
591        } else if parts[0] != ARN_PREFIX {
592            Err(Error::MissingPrefix)
593        } else {
594            let new_arn = ResourceName {
595                partition: if parts[1].is_empty() {
596                    None
597                } else if parts[1] == PARTITION_AWS_PREFIX
598                    || parts[1].starts_with(PARTITION_AWS_OTHER_PREFIX)
599                {
600                    Some(Identifier::from_str(parts[1])?)
601                } else {
602                    return Err(Error::InvalidPartition);
603                },
604                service: Identifier::from_str(parts[2])?,
605                region: if parts[3].is_empty() {
606                    None
607                } else {
608                    Some(Identifier::from_str(parts[3])?)
609                },
610                account_id: if parts[4].is_empty() {
611                    None
612                } else {
613                    Some(AccountIdentifier::from_str(parts[4])?)
614                },
615                resource: {
616                    let resource_parts: Vec<&str> = parts.drain(5..).collect();
617                    ResourceIdentifier::from_str(&resource_parts.join(&PART_SEPARATOR.to_string()))?
618                },
619            };
620
621            Ok(new_arn)
622        }
623    }
624}
625
626impl ResourceName {
627    /// Construct a minimal `ResourceName` value with simply a service and resource.
628    pub fn new(service: Identifier, resource: ResourceIdentifier) -> Self {
629        Self {
630            partition: None,
631            service,
632            region: None,
633            account_id: None,
634            resource,
635        }
636    }
637
638    /// Construct a minimal `ResourceName` value with simply a service and resource in the `aws` partition.
639    pub fn aws(service: Identifier, resource: ResourceIdentifier) -> Self {
640        Self {
641            partition: Some(known::Partition::default().into()),
642            service,
643            region: None,
644            account_id: None,
645            resource,
646        }
647    }
648
649    /// Return `true` if the identifier contains variables of the form
650    /// `${name}`, else `false`.
651    pub fn has_variables(&self) -> bool {
652        self.resource.has_variables()
653    }
654
655    /// Replace any variables in the string with values from the context,
656    /// returning a new value if the replacements result in a legal identifier
657    /// string. The
658    pub fn replace_variables<V>(&self, context: &HashMap<String, V>) -> Result<Self, Error>
659    where
660        V: Clone + Into<String>,
661    {
662        Ok(Self {
663            resource: self.resource.replace_variables(context)?,
664            ..self.clone()
665        })
666    }
667}
668
669// ------------------------------------------------------------------------------------------------
670// Modules
671// ------------------------------------------------------------------------------------------------
672
673#[cfg(feature = "builders")]
674pub mod builder;
675
676#[cfg(feature = "known")]
677pub mod known;
678
679#[doc(hidden)]
680mod error;
681pub use crate::error::Error;