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}
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
72macro_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 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
209mod 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(®ion_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 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 assert_eq!(resolve(&resolver, "mars-east-1").dns_suffix, "amazonaws.com");
533 assert_eq!(resolve(&resolver, "mars-east-2").dns_suffix, "mars.aws");
535 }
536}