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;