Skip to main content

aws_sdk_cloudtrail/endpoint_lib/
partition.rs

1// Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT.
2/*
3 *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 *  SPDX-License-Identifier: Apache-2.0
5 */
6
7//! Partition function to determine a partition for a given region
8//!
9//! This function supports adding regions dynamically, parsing a JSON file, and builder construction.
10//!
11//! If, at a future point, this interface stabilizes it is a good candidate for extraction into a
12//! shared crate.
13use crate::endpoint_lib::diagnostic::DiagnosticCollector;
14use crate::endpoint_lib::partition::deser::deserialize_partitions;
15use aws_smithy_json::deserialize::error::DeserializeError;
16use regex_lite::Regex;
17use std::borrow::Cow;
18use std::collections::HashMap;
19
20/// Determine the AWS partition metadata for a given region
21#[derive(Clone, Debug, Default)]
22pub(crate) struct PartitionResolver {
23    partitions: Vec<PartitionMetadata>,
24}
25
26impl PartitionResolver {
27    pub(crate) fn from_partitions(partitions: Vec<PartitionMetadata>) -> Self {
28        Self { partitions }
29    }
30}
31
32/// Partition result returned from partition resolver
33#[derive(Debug, Clone)]
34pub(crate) struct Partition<'a> {
35    name: &'a str,
36    dns_suffix: &'a str,
37    dual_stack_dns_suffix: &'a str,
38    supports_fips: bool,
39    supports_dual_stack: bool,
40    implicit_global_region: &'a str,
41}
42
43#[allow(unused)]
44impl Partition<'_> {
45    pub(crate) fn name(&self) -> &str {
46        self.name
47    }
48
49    pub(crate) fn dns_suffix(&self) -> &str {
50        self.dns_suffix
51    }
52
53    pub(crate) fn supports_fips(&self) -> bool {
54        self.supports_fips
55    }
56
57    pub(crate) fn dual_stack_dns_suffix(&self) -> &str {
58        self.dual_stack_dns_suffix
59    }
60
61    pub(crate) fn supports_dual_stack(&self) -> bool {
62        self.supports_dual_stack
63    }
64
65    pub(crate) fn implicit_global_region(&self) -> &str {
66        self.implicit_global_region
67    }
68}
69
70static DEFAULT_OVERRIDE: &PartitionOutputOverride = &PartitionOutputOverride {
71    name: None,
72    dns_suffix: None,
73    dual_stack_dns_suffix: None,
74    supports_fips: None,
75    supports_dual_stack: None,
76    implicit_global_region: None,
77};
78
79/// Merge the base output and the override output, dealing with `Cow`s
80macro_rules! merge {
81    ($base: expr, $output: expr, $field: ident) => {
82        $output.$field.as_ref().map(|s| s.as_ref()).unwrap_or($base.outputs.$field.as_ref())
83    };
84}
85
86impl PartitionResolver {
87    #[allow(unused)]
88    pub(crate) fn empty() -> PartitionResolver {
89        PartitionResolver { partitions: vec![] }
90    }
91
92    #[allow(unused)]
93    pub(crate) fn add_partition(&mut self, partition: PartitionMetadata) {
94        self.partitions.push(partition);
95    }
96
97    pub(crate) fn new_from_json(partition_dot_json: &[u8]) -> Result<PartitionResolver, DeserializeError> {
98        deserialize_partitions(partition_dot_json)
99    }
100
101    /// Resolve a partition for a given region
102    ///
103    /// 1. Enumerate each partition in the `partitions` array, and determine if the identifier to be
104    ///    resolved matches an explicit region listed in the `regions` array for a given partition.
105    ///    If identifier matches, proceed to step 4, otherwise continue to step 2.
106    /// 2. Enumerate each partition in the `partitions` array, use the regular expression
107    ///    `regionRegex` to determine if the identifier matches the regular expression. If the
108    ///    identifier matches, proceed to step 4, otherwise continue to step 3.
109    /// 3. If no partition is matched after exhausting step 1 and step 2, then fallback to matching
110    ///    the identifier to the partition where `id == "aws"`, and proceed to step 4. If no `aws`
111    ///    partition is present, return `None`.
112    /// 4. After matching the identifier to a partition using one of the previous steps, the partition function should return a
113    ///    typed data structure containing the fields in `outputs` in the matched partition. **Important:** If a specific region
114    ///    was matched, the properties associated with that region **MUST** be merged with the `outputs` field.
115    pub(crate) fn resolve_partition(&self, region: &str, e: &mut DiagnosticCollector) -> Option<Partition<'_>> {
116        let mut explicit_match_partition = self.partitions.iter().flat_map(|part| part.explicit_match(region));
117        let mut regex_match_partition = self.partitions.iter().flat_map(|part| part.regex_match(region));
118
119        let (base, region_override) = explicit_match_partition.next().or_else(|| regex_match_partition.next()).or_else(|| {
120            match self.partitions.iter().find(|p| p.id == "aws") {
121                Some(partition) => Some((partition, None)),
122                None => {
123                    e.report_error("no AWS partition!");
124                    None
125                }
126            }
127        })?;
128        let region_override = region_override.as_ref().unwrap_or(&DEFAULT_OVERRIDE);
129        Some(Partition {
130            name: merge!(base, region_override, name),
131            dns_suffix: merge!(base, region_override, dns_suffix),
132            dual_stack_dns_suffix: merge!(base, region_override, dual_stack_dns_suffix),
133            supports_fips: region_override.supports_fips.unwrap_or(base.outputs.supports_fips),
134            supports_dual_stack: region_override.supports_dual_stack.unwrap_or(base.outputs.supports_dual_stack),
135            implicit_global_region: merge!(base, region_override, implicit_global_region),
136        })
137    }
138}
139
140type Str = Cow<'static, str>;
141
142#[derive(Clone, Debug)]
143pub(crate) struct PartitionMetadata {
144    id: Str,
145    region_regex: Regex,
146    regions: HashMap<Str, PartitionOutputOverride>,
147    outputs: PartitionOutput,
148}
149
150#[derive(Default)]
151pub(crate) struct PartitionMetadataBuilder {
152    pub(crate) id: Option<Str>,
153    pub(crate) region_regex: Option<Regex>,
154    pub(crate) regions: HashMap<Str, PartitionOutputOverride>,
155    pub(crate) outputs: Option<PartitionOutputOverride>,
156}
157
158impl PartitionMetadataBuilder {
159    pub(crate) fn build(self) -> PartitionMetadata {
160        PartitionMetadata {
161            id: self.id.expect("id must be defined"),
162            region_regex: self.region_regex.expect("region regex must be defined"),
163            regions: self.regions,
164            outputs: self
165                .outputs
166                .expect("outputs must be defined")
167                .into_partition_output()
168                .expect("missing fields on outputs"),
169        }
170    }
171}
172
173impl PartitionMetadata {
174    fn explicit_match(&self, region: &str) -> Option<(&PartitionMetadata, Option<&PartitionOutputOverride>)> {
175        self.regions.get(region).map(|output_override| (self, Some(output_override)))
176    }
177
178    fn regex_match(&self, region: &str) -> Option<(&PartitionMetadata, Option<&PartitionOutputOverride>)> {
179        if self.region_regex.is_match(region) {
180            Some((self, None))
181        } else {
182            None
183        }
184    }
185}
186
187#[derive(Clone, Debug)]
188pub(crate) struct PartitionOutput {
189    name: Str,
190    dns_suffix: Str,
191    dual_stack_dns_suffix: Str,
192    supports_fips: bool,
193    supports_dual_stack: bool,
194    implicit_global_region: Str,
195}
196
197#[derive(Clone, Debug, Default)]
198pub(crate) struct PartitionOutputOverride {
199    name: Option<Str>,
200    dns_suffix: Option<Str>,
201    dual_stack_dns_suffix: Option<Str>,
202    supports_fips: Option<bool>,
203    supports_dual_stack: Option<bool>,
204    implicit_global_region: Option<Str>,
205}
206
207impl PartitionOutputOverride {
208    pub(crate) fn into_partition_output(self) -> Result<PartitionOutput, Box<dyn std::error::Error>> {
209        Ok(PartitionOutput {
210            name: self.name.ok_or("missing name")?,
211            dns_suffix: self.dns_suffix.ok_or("missing dnsSuffix")?,
212            dual_stack_dns_suffix: self.dual_stack_dns_suffix.ok_or("missing dual_stackDnsSuffix")?,
213            supports_fips: self.supports_fips.ok_or("missing supports fips")?,
214            supports_dual_stack: self.supports_dual_stack.ok_or("missing supportsDualstack")?,
215            implicit_global_region: self.implicit_global_region.ok_or("missing implicitGlobalRegion")?,
216        })
217    }
218}
219
220/// JSON deserializers for partition metadata
221///
222/// This code was generated by smithy-rs and then hand edited for clarity
223mod deser {
224    use crate::endpoint_lib::partition::{PartitionMetadata, PartitionMetadataBuilder, PartitionOutputOverride, PartitionResolver};
225    use aws_smithy_json::deserialize::token::{expect_bool_or_null, expect_start_object, expect_string_or_null, skip_value};
226    use aws_smithy_json::deserialize::{error::DeserializeError, json_token_iter, Token};
227    use regex_lite::Regex;
228    use std::borrow::Cow;
229    use std::collections::HashMap;
230
231    pub(crate) fn deserialize_partitions(value: &[u8]) -> Result<PartitionResolver, DeserializeError> {
232        let mut tokens_owned = json_token_iter(value).peekable();
233        let tokens = &mut tokens_owned;
234        expect_start_object(tokens.next())?;
235        let mut resolver = None;
236        loop {
237            match tokens.next().transpose()? {
238                Some(Token::EndObject { .. }) => break,
239                Some(Token::ObjectKey { key, .. }) => match key.to_unescaped()?.as_ref() {
240                    "partitions" => {
241                        resolver = Some(PartitionResolver::from_partitions(deser_partitions(tokens)?));
242                    }
243                    _ => skip_value(tokens)?,
244                },
245                other => return Err(DeserializeError::custom(format!("expected object key or end object, found: {other:?}",))),
246            }
247        }
248        if tokens.next().is_some() {
249            return Err(DeserializeError::custom("found more JSON tokens after completing parsing"));
250        }
251        resolver.ok_or_else(|| DeserializeError::custom("did not find partitions array"))
252    }
253
254    fn deser_partitions<'a, I>(tokens: &mut std::iter::Peekable<I>) -> Result<Vec<PartitionMetadata>, DeserializeError>
255    where
256        I: Iterator<Item = Result<Token<'a>, DeserializeError>>,
257    {
258        match tokens.next().transpose()? {
259            Some(Token::StartArray { .. }) => {
260                let mut items = Vec::new();
261                loop {
262                    match tokens.peek() {
263                        Some(Ok(Token::EndArray { .. })) => {
264                            tokens.next().transpose().unwrap();
265                            break;
266                        }
267                        _ => {
268                            items.push(deser_partition(tokens)?);
269                        }
270                    }
271                }
272                Ok(items)
273            }
274            _ => Err(DeserializeError::custom("expected start array")),
275        }
276    }
277
278    pub(crate) fn deser_partition<'a, I>(tokens: &mut std::iter::Peekable<I>) -> Result<PartitionMetadata, DeserializeError>
279    where
280        I: Iterator<Item = Result<Token<'a>, DeserializeError>>,
281    {
282        match tokens.next().transpose()? {
283            Some(Token::StartObject { .. }) => {
284                let mut builder = PartitionMetadataBuilder::default();
285                loop {
286                    match tokens.next().transpose()? {
287                        Some(Token::EndObject { .. }) => break,
288                        Some(Token::ObjectKey { key, .. }) => match key.to_unescaped()?.as_ref() {
289                            "id" => {
290                                builder.id = token_to_str(tokens.next())?;
291                            }
292                            "regionRegex" => {
293                                builder.region_regex = token_to_str(tokens.next())?
294                                    .map(|region_regex| Regex::new(&region_regex))
295                                    .transpose()
296                                    .map_err(|_e| DeserializeError::custom("invalid regex"))?;
297                            }
298                            "regions" => {
299                                builder.regions = deser_explicit_regions(tokens)?;
300                            }
301                            "outputs" => {
302                                builder.outputs = deser_outputs(tokens)?;
303                            }
304                            _ => skip_value(tokens)?,
305                        },
306                        other => return Err(DeserializeError::custom(format!("expected object key or end object, found: {other:?}"))),
307                    }
308                }
309                Ok(builder.build())
310            }
311            _ => Err(DeserializeError::custom("expected start object")),
312        }
313    }
314
315    #[allow(clippy::type_complexity, non_snake_case)]
316    pub(crate) fn deser_explicit_regions<'a, I>(
317        tokens: &mut std::iter::Peekable<I>,
318    ) -> Result<HashMap<super::Str, PartitionOutputOverride>, DeserializeError>
319    where
320        I: Iterator<Item = Result<Token<'a>, DeserializeError>>,
321    {
322        match tokens.next().transpose()? {
323            Some(Token::StartObject { .. }) => {
324                let mut map = HashMap::new();
325                loop {
326                    match tokens.next().transpose()? {
327                        Some(Token::EndObject { .. }) => break,
328                        Some(Token::ObjectKey { key, .. }) => {
329                            let key = key.to_unescaped().map(|u| u.into_owned())?;
330                            let value = deser_outputs(tokens)?;
331                            if let Some(value) = value {
332                                map.insert(key.into(), value);
333                            }
334                        }
335                        other => return Err(DeserializeError::custom(format!("expected object key or end object, found: {other:?}"))),
336                    }
337                }
338                Ok(map)
339            }
340            _ => Err(DeserializeError::custom("expected start object")),
341        }
342    }
343
344    /// Convert a token to `Str` (a potentially static String)
345    fn token_to_str(token: Option<Result<Token, DeserializeError>>) -> Result<Option<super::Str>, DeserializeError> {
346        Ok(expect_string_or_null(token)?
347            .map(|s| s.to_unescaped().map(|u| u.into_owned()))
348            .transpose()?
349            .map(Cow::Owned))
350    }
351
352    fn deser_outputs<'a, I>(tokens: &mut std::iter::Peekable<I>) -> Result<Option<PartitionOutputOverride>, DeserializeError>
353    where
354        I: Iterator<Item = Result<Token<'a>, DeserializeError>>,
355    {
356        match tokens.next().transpose()? {
357            Some(Token::StartObject { .. }) => {
358                #[allow(unused_mut)]
359                let mut builder = PartitionOutputOverride::default();
360                loop {
361                    match tokens.next().transpose()? {
362                        Some(Token::EndObject { .. }) => break,
363                        Some(Token::ObjectKey { key, .. }) => match key.to_unescaped()?.as_ref() {
364                            "name" => {
365                                builder.name = token_to_str(tokens.next())?;
366                            }
367                            "dnsSuffix" => {
368                                builder.dns_suffix = token_to_str(tokens.next())?;
369                            }
370                            "dualStackDnsSuffix" => {
371                                builder.dual_stack_dns_suffix = token_to_str(tokens.next())?;
372                            }
373                            "supportsFIPS" => {
374                                builder.supports_fips = expect_bool_or_null(tokens.next())?;
375                            }
376                            "supportsDualStack" => {
377                                builder.supports_dual_stack = expect_bool_or_null(tokens.next())?;
378                            }
379                            "implicitGlobalRegion" => {
380                                builder.implicit_global_region = token_to_str(tokens.next())?;
381                            }
382                            _ => skip_value(tokens)?,
383                        },
384                        other => return Err(DeserializeError::custom(format!("expected object key or end object, found: {other:?}",))),
385                    }
386                }
387                Ok(Some(builder))
388            }
389            _ => Err(DeserializeError::custom("expected start object")),
390        }
391    }
392}
393
394#[cfg(test)]
395mod test {
396    use crate::endpoint_lib::diagnostic::DiagnosticCollector;
397    use crate::endpoint_lib::partition::{Partition, PartitionMetadata, PartitionOutput, PartitionOutputOverride, PartitionResolver};
398    use regex_lite::Regex;
399    use std::collections::HashMap;
400
401    fn resolve<'a>(resolver: &'a PartitionResolver, region: &str) -> Partition<'a> {
402        resolver
403            .resolve_partition(region, &mut DiagnosticCollector::new())
404            .expect("could not resolve partition")
405    }
406
407    #[test]
408    fn deserialize_partitions() {
409        let partitions = r#"{
410  "version": "1.1",
411  "partitions": [
412    {
413      "id": "aws",
414      "regionRegex": "^(us|eu|ap|sa|ca|me|af)-\\w+-\\d+$",
415      "regions": {
416        "af-south-1": {},
417        "af-east-1": {},
418        "ap-northeast-1": {},
419        "ap-northeast-2": {},
420        "ap-northeast-3": {},
421        "ap-south-1": {},
422        "ap-southeast-1": {},
423        "ap-southeast-2": {},
424        "ap-southeast-3": {},
425        "ca-central-1": {},
426        "eu-central-1": {},
427        "eu-north-1": {},
428        "eu-south-1": {},
429        "eu-west-1": {},
430        "eu-west-2": {},
431        "eu-west-3": {},
432        "me-south-1": {},
433        "sa-east-1": {},
434        "us-east-1": {},
435        "us-east-2": {},
436        "us-west-1": {},
437        "us-west-2": {},
438        "aws-global": {}
439      },
440      "outputs": {
441        "name": "aws",
442        "dnsSuffix": "amazonaws.com",
443        "dualStackDnsSuffix": "api.aws",
444        "supportsFIPS": true,
445        "supportsDualStack": true,
446        "implicitGlobalRegion": "us-east-1"
447      }
448    },
449    {
450      "id": "aws-us-gov",
451      "regionRegex": "^us\\-gov\\-\\w+\\-\\d+$",
452      "regions": {
453        "us-gov-west-1": {},
454        "us-gov-east-1": {},
455        "aws-us-gov-global": {}
456      },
457      "outputs": {
458        "name": "aws-us-gov",
459        "dnsSuffix": "amazonaws.com",
460        "dualStackDnsSuffix": "api.aws",
461        "supportsFIPS": true,
462        "supportsDualStack": true,
463        "implicitGlobalRegion": "us-gov-east-1"
464      }
465    },
466    {
467      "id": "aws-cn",
468      "regionRegex": "^cn\\-\\w+\\-\\d+$",
469      "regions": {
470        "cn-north-1": {},
471        "cn-northwest-1": {},
472        "aws-cn-global": {}
473      },
474      "outputs": {
475        "name": "aws-cn",
476        "dnsSuffix": "amazonaws.com.cn",
477        "dualStackDnsSuffix": "api.amazonwebservices.com.cn",
478        "supportsFIPS": true,
479        "supportsDualStack": true,
480        "implicitGlobalRegion": "cn-north-1"
481      }
482    },
483    {
484      "id": "aws-iso",
485      "regionRegex": "^us\\-iso\\-\\w+\\-\\d+$",
486      "outputs": {
487        "name": "aws-iso",
488        "dnsSuffix": "c2s.ic.gov",
489        "supportsFIPS": true,
490        "supportsDualStack": false,
491        "dualStackDnsSuffix": "c2s.ic.gov",
492        "implicitGlobalRegion": "us-iso-foo-1"
493      },
494      "regions": {}
495    },
496    {
497      "id": "aws-iso-b",
498      "regionRegex": "^us\\-isob\\-\\w+\\-\\d+$",
499      "outputs": {
500        "name": "aws-iso-b",
501        "dnsSuffix": "sc2s.sgov.gov",
502        "supportsFIPS": true,
503        "supportsDualStack": false,
504        "dualStackDnsSuffix": "sc2s.sgov.gov",
505        "implicitGlobalRegion": "us-isob-foo-1"
506      },
507      "regions": {}
508    }
509  ]
510}"#;
511        let resolver = super::deser::deserialize_partitions(partitions.as_bytes()).expect("valid resolver");
512        assert_eq!(resolve(&resolver, "cn-north-1").name, "aws-cn");
513        assert_eq!(resolve(&resolver, "cn-north-1").dns_suffix, "amazonaws.com.cn");
514        assert_eq!(resolver.partitions.len(), 5);
515        assert_eq!(resolve(&resolver, "af-south-1").implicit_global_region, "us-east-1");
516    }
517
518    #[test]
519    fn resolve_partitions() {
520        let mut resolver = PartitionResolver::empty();
521        let new_suffix = PartitionOutputOverride {
522            dns_suffix: Some("mars.aws".into()),
523            ..Default::default()
524        };
525        resolver.add_partition(PartitionMetadata {
526            id: "aws".into(),
527            region_regex: Regex::new("^(us|eu|ap|sa|ca|me|af)-\\w+-\\d+$").unwrap(),
528            regions: HashMap::from([("mars-east-2".into(), new_suffix)]),
529            outputs: PartitionOutput {
530                name: "aws".into(),
531                dns_suffix: "amazonaws.com".into(),
532                dual_stack_dns_suffix: "api.aws".into(),
533                supports_fips: true,
534                supports_dual_stack: true,
535                implicit_global_region: "us-east-1".into(),
536            },
537        });
538        resolver.add_partition(PartitionMetadata {
539            id: "other".into(),
540            region_regex: Regex::new("^(other)-\\w+-\\d+$").unwrap(),
541            regions: Default::default(),
542            outputs: PartitionOutput {
543                name: "other".into(),
544                dns_suffix: "other.amazonaws.com".into(),
545                dual_stack_dns_suffix: "other.aws".into(),
546                supports_fips: false,
547                supports_dual_stack: true,
548                implicit_global_region: "other-south-2".into(),
549            },
550        });
551        assert_eq!(resolve(&resolver, "us-east-1").name, "aws");
552        assert_eq!(resolve(&resolver, "other-west-2").name, "other");
553        // mars-east-1 hits aws through the default fallback
554        assert_eq!(resolve(&resolver, "mars-east-1").dns_suffix, "amazonaws.com");
555        // mars-east-2 hits aws through the region override
556        assert_eq!(resolve(&resolver, "mars-east-2").dns_suffix, "mars.aws");
557    }
558}