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
32pub(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
78macro_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 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
219mod 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(®ion_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 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 assert_eq!(resolve(&resolver, "mars-east-1").dns_suffix, "amazonaws.com");
554 assert_eq!(resolve(&resolver, "mars-east-2").dns_suffix, "mars.aws");
556 }
557}