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