1use 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#[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#[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
79macro_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 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
220mod 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(®ion_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 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 assert_eq!(resolve(&resolver, "mars-east-1").dns_suffix, "amazonaws.com");
555 assert_eq!(resolve(&resolver, "mars-east-2").dns_suffix, "mars.aws");
557 }
558}