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