1 /* 2 * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 package software.amazon.smithy.aws.go.codegen; 16 17 import java.util.List; 18 import java.util.Map; 19 import java.util.Optional; 20 import java.util.TreeMap; 21 import java.util.function.Consumer; 22 import java.util.stream.Collectors; 23 import software.amazon.smithy.aws.traits.ServiceTrait; 24 import software.amazon.smithy.codegen.core.CodegenException; 25 import software.amazon.smithy.codegen.core.Symbol; 26 import software.amazon.smithy.go.codegen.GoSettings; 27 import software.amazon.smithy.go.codegen.GoStackStepMiddlewareGenerator; 28 import software.amazon.smithy.go.codegen.GoWriter; 29 import software.amazon.smithy.go.codegen.MiddlewareIdentifier; 30 import software.amazon.smithy.go.codegen.SmithyGoDependency; 31 import software.amazon.smithy.go.codegen.SymbolUtils; 32 import software.amazon.smithy.go.codegen.TriConsumer; 33 import software.amazon.smithy.go.codegen.integration.ConfigField; 34 import software.amazon.smithy.go.codegen.integration.ProtocolUtils; 35 import software.amazon.smithy.model.Model; 36 import software.amazon.smithy.model.node.Node; 37 import software.amazon.smithy.model.node.ObjectNode; 38 import software.amazon.smithy.model.node.StringNode; 39 import software.amazon.smithy.model.shapes.ServiceShape; 40 import software.amazon.smithy.utils.IoUtils; 41 import software.amazon.smithy.utils.ListUtils; 42 43 /** 44 * Writes out a file that resolves endpoints using endpoints.json, but the 45 * created resolver resolves endpoints for a single service. 46 */ 47 public class EndpointGenerator implements Runnable { 48 public static final String MIDDLEWARE_NAME = "ResolveEndpoint"; 49 public static final String ADD_MIDDLEWARE_HELPER_NAME = String.format("add%sMiddleware", MIDDLEWARE_NAME); 50 public static final String RESOLVER_INTERFACE_NAME = "EndpointResolver"; 51 public static final String RESOLVER_FUNC_NAME = "EndpointResolverFunc"; 52 public static final String RESOLVER_OPTIONS = "EndpointResolverOptions"; 53 public static final String CLIENT_CONFIG_RESOLVER = "resolveDefaultEndpointConfiguration"; 54 public static final String RESOLVER_CONSTRUCTOR_NAME = "NewDefaultEndpointResolver"; 55 public static final String AWS_ENDPOINT_RESOLVER_HELPER = "withEndpointResolver"; 56 private static final String EndpointResolverFromURL = "EndpointResolverFromURL"; 57 private static final String ENDPOINT_SOURCE_CUSTOM = "EndpointSourceCustom"; 58 private static final Symbol AWS_ENDPOINT = SymbolUtils.createPointableSymbolBuilder( 59 "Endpoint", AwsGoDependency.AWS_CORE).build(); 60 61 private static final int ENDPOINT_MODEL_VERSION = 3; 62 private static final String INTERNAL_ENDPOINT_PACKAGE = "internal/endpoints"; 63 private static final String INTERNAL_RESOLVER_NAME = "Resolver"; 64 private static final String INTERNAL_RESOLVER_OPTIONS_NAME = "Options"; 65 private static final String INTERNAL_ENDPOINTS_DATA_NAME = "defaultPartitions"; 66 private static final List<ResolveConfigField> resolveConfigFields = ListUtils.of( 67 ResolveConfigField.builder() 68 .name("DisableHTTPS") 69 .type(SymbolUtils.createValueSymbolBuilder("bool").build()) 70 .shared(true) 71 .build() 72 ); 73 74 private final GoSettings settings; 75 private final Model model; 76 private final TriConsumer<String, String, Consumer<GoWriter>> writerFactory; 77 private final ServiceShape serviceShape; 78 private final ObjectNode endpointData; 79 private final String endpointPrefix; 80 private final Map<String, Partition> partitions = new TreeMap<>(); 81 private final Boolean isInternalOnly; 82 private final String resolvedSdkID; 83 EndpointGenerator( GoSettings settings, Model model, TriConsumer<String, String, Consumer<GoWriter>> writerFactory )84 public EndpointGenerator( 85 GoSettings settings, 86 Model model, 87 TriConsumer<String, String, Consumer<GoWriter>> writerFactory 88 ) { 89 this( 90 settings, 91 model, 92 writerFactory, 93 settings.getService(model).expectTrait(ServiceTrait.class) 94 .getSdkId(), 95 settings.getService(model).expectTrait(ServiceTrait.class) 96 .getArnNamespace(), 97 false 98 ); 99 } 100 EndpointGenerator( GoSettings settings, Model model, TriConsumer<String, String, Consumer<GoWriter>> writerFactory, String sdkID, String arnNamespace, Boolean internalOnly )101 public EndpointGenerator( 102 GoSettings settings, 103 Model model, 104 TriConsumer<String, String, Consumer<GoWriter>> writerFactory, 105 String sdkID, 106 String arnNamespace, 107 Boolean internalOnly 108 ) { 109 this.settings = settings; 110 this.model = model; 111 this.writerFactory = writerFactory; 112 serviceShape = settings.getService(model); 113 this.endpointPrefix = getEndpointPrefix(sdkID, arnNamespace); 114 this.endpointData = Node.parse(IoUtils.readUtf8Resource(getClass(), "endpoints.json")).expectObjectNode(); 115 this.isInternalOnly = internalOnly; 116 this.resolvedSdkID = sdkID; 117 validateVersion(); 118 loadPartitions(); 119 } 120 validateVersion()121 private void validateVersion() { 122 int version = endpointData.expectNumberMember("version").getValue().intValue(); 123 if (version != ENDPOINT_MODEL_VERSION) { 124 throw new CodegenException("Invalid endpoints.json version. Expected version 3, found " + version); 125 } 126 } 127 128 // Get service's endpoint prefix from a known list. If not found, fallback to ArnNamespace getEndpointPrefix(ServiceShape service)129 private String getEndpointPrefix(ServiceShape service) { 130 ObjectNode endpointPrefixData = Node.parse(IoUtils.readUtf8Resource(getClass(), "endpoint-prefix.json")) 131 .expectObjectNode(); 132 ServiceTrait serviceTrait = service.getTrait(ServiceTrait.class) 133 .orElseThrow(() -> new CodegenException("No service trait found on " + service.getId())); 134 return endpointPrefixData.getStringMemberOrDefault(serviceTrait.getSdkId(), serviceTrait.getArnNamespace()); 135 } 136 getEndpointPrefix(String sdkId, String arnNamespace)137 private String getEndpointPrefix(String sdkId, String arnNamespace) { 138 ObjectNode endpointPrefixData = Node.parse(IoUtils.readUtf8Resource(getClass(), "endpoint-prefix.json")) 139 .expectObjectNode(); 140 return endpointPrefixData.getStringMemberOrDefault(sdkId, arnNamespace); 141 } 142 loadPartitions()143 private void loadPartitions() { 144 List<ObjectNode> partitionObjects = endpointData 145 .expectArrayMember("partitions") 146 .getElementsAs(Node::expectObjectNode); 147 148 for (ObjectNode partition : partitionObjects) { 149 String partitionName = partition.expectStringMember("partition").getValue(); 150 partitions.put(partitionName, new Partition(partition, partitionName)); 151 } 152 } 153 154 @Override run()155 public void run() { 156 if (!this.isInternalOnly) { 157 writerFactory.accept("endpoints.go", settings.getModuleName(), writer -> { 158 generatePublicResolverTypes(writer); 159 generateMiddleware(writer); 160 generateAwsEndpointResolverWrapper(writer); 161 }); 162 } 163 164 String pkgName = isInternalOnly ? INTERNAL_ENDPOINT_PACKAGE + "/" + this.endpointPrefix : INTERNAL_ENDPOINT_PACKAGE; 165 writerFactory.accept(pkgName + "/endpoints.go", getInternalEndpointImportPath(), (writer) -> { 166 generateInternalResolverImplementation(writer); 167 generateInternalEndpointsModel(writer); 168 }); 169 170 if (!this.isInternalOnly) { 171 writerFactory.accept(INTERNAL_ENDPOINT_PACKAGE + "/endpoints_test.go", 172 getInternalEndpointImportPath(), (writer) -> { 173 writer.addUseImports(SmithyGoDependency.TESTING); 174 writer.openBlock("func TestRegexCompile(t *testing.T) {", "}", () -> { 175 writer.write("_ = $T", 176 getInternalEndpointsSymbol(INTERNAL_ENDPOINTS_DATA_NAME, false).build()); 177 }); 178 }); 179 } 180 181 } 182 generateAwsEndpointResolverWrapper(GoWriter writer)183 private void generateAwsEndpointResolverWrapper(GoWriter writer) { 184 Symbol awsEndpointResolver = SymbolUtils.createValueSymbolBuilder("EndpointResolver", AwsGoDependency.AWS_CORE) 185 .build(); 186 Symbol resolverInterface = SymbolUtils.createValueSymbolBuilder(RESOLVER_INTERFACE_NAME).build(); 187 188 Symbol wrappedResolverSymbol = SymbolUtils.createPointableSymbolBuilder("wrappedEndpointResolver").build(); 189 writer.openBlock("type $T struct {", "}", wrappedResolverSymbol, () -> { 190 writer.write("awsResolver $T", awsEndpointResolver); 191 writer.write("resolver $T", resolverInterface); 192 }); 193 writeExternalResolveEndpointImplementation(writer, wrappedResolverSymbol, "w", () -> { 194 writer.openBlock("if w.awsResolver == nil {", "}", () -> writer.write("goto fallback")); 195 196 writer.write("endpoint, err = w.awsResolver.ResolveEndpoint(ServiceID, region)"); 197 writer.openBlock("if err == nil {", "}", () -> writer.write("return endpoint, nil")); 198 writer.write(""); 199 200 writer.addUseImports(SmithyGoDependency.ERRORS); 201 writer.openBlock("if nf := (&$T{}); !errors.As(err, &nf) {", "}", 202 SymbolUtils.createValueSymbolBuilder("EndpointNotFoundError", AwsGoDependency.AWS_CORE).build(), 203 () -> writer.write("return endpoint, err")); 204 writer.write(""); 205 206 writer.write("fallback:"); 207 writer.openBlock("if w.resolver == nil {", "}", () -> { 208 writer.addUseImports(SmithyGoDependency.FMT); 209 writer.write("return endpoint, fmt.Errorf(\"default endpoint resolver provided was nil\")"); 210 }); 211 writer.write("return w.resolver.ResolveEndpoint(region, options)"); 212 }); 213 214 // Generate exported helper for constructing a wrapper around the AWS EndpointResolver type that is compatible 215 // with the clients EndpointResolver interface. 216 writer.writeDocs(String.format("%s returns an EndpointResolver that first delegates endpoint resolution " 217 + "to the awsResolver. If awsResolver returns `aws.EndpointNotFoundError` error, the resolver " 218 + "will use the the provided fallbackResolver for resolution. awsResolver and fallbackResolver " 219 + "must not be nil", 220 AWS_ENDPOINT_RESOLVER_HELPER)); 221 writer.openBlock("func $L(awsResolver $T, fallbackResolver $T) $T {", "}", AWS_ENDPOINT_RESOLVER_HELPER, 222 awsEndpointResolver, resolverInterface, resolverInterface, () -> { 223 writer.openBlock("return &$T{", "}", wrappedResolverSymbol, () -> { 224 writer.write("awsResolver: awsResolver,"); 225 writer.write("resolver: fallbackResolver,"); 226 }); 227 }); 228 } 229 generateMiddleware(GoWriter writer)230 private void generateMiddleware(GoWriter writer) { 231 // Generate middleware definition 232 GoStackStepMiddlewareGenerator middleware = GoStackStepMiddlewareGenerator.createSerializeStepMiddleware( 233 MIDDLEWARE_NAME, MiddlewareIdentifier.string(MIDDLEWARE_NAME)); 234 middleware.writeMiddleware(writer, this::generateMiddlewareResolverBody, 235 this::generateMiddlewareStructureMembers); 236 237 Symbol stackSymbol = SymbolUtils.createPointableSymbolBuilder("Stack", SmithyGoDependency.SMITHY_MIDDLEWARE) 238 .build(); 239 240 // Generate Middleware Adder Helper 241 writer.openBlock("func $L(stack $P, o Options) error {", "}", ADD_MIDDLEWARE_HELPER_NAME, stackSymbol, () -> { 242 writer.addUseImports(SmithyGoDependency.SMITHY_MIDDLEWARE); 243 String closeBlock = String.format("}, \"%s\", middleware.Before)", 244 ProtocolUtils.OPERATION_SERIALIZER_MIDDLEWARE_ID); 245 writer.openBlock("return stack.Serialize.Insert(&$T{", closeBlock, 246 middleware.getMiddlewareSymbol(), 247 () -> { 248 writer.write("Resolver: o.EndpointResolver,"); 249 writer.write("Options: o.EndpointOptions,"); 250 }); 251 }); 252 writer.write(""); 253 // Generate Middleware Remover Helper 254 writer.openBlock("func remove$LMiddleware(stack $P) error {", "}", middleware.getMiddlewareSymbol(), 255 stackSymbol, () -> { 256 writer.write("_, err := stack.Serialize.Remove((&$T{}).ID())", middleware.getMiddlewareSymbol()); 257 writer.write("return err"); 258 }); 259 } 260 generateMiddlewareResolverBody(GoStackStepMiddlewareGenerator g, GoWriter w)261 private void generateMiddlewareResolverBody(GoStackStepMiddlewareGenerator g, GoWriter w) { 262 w.addUseImports(SmithyGoDependency.FMT); 263 w.addUseImports(SmithyGoDependency.NET_URL); 264 w.addUseImports(AwsGoDependency.AWS_MIDDLEWARE); 265 w.addUseImports(SmithyGoDependency.SMITHY_MIDDLEWARE); 266 w.addUseImports(SmithyGoDependency.SMITHY_HTTP_TRANSPORT); 267 268 w.write("req, ok := in.Request.(*smithyhttp.Request)"); 269 w.openBlock("if !ok {", "}", () -> { 270 w.write("return out, metadata, fmt.Errorf(\"unknown transport type %T\", in.Request)"); 271 }); 272 w.write(""); 273 274 w.openBlock("if m.Resolver == nil {", "}", () -> { 275 w.write("return out, metadata, fmt.Errorf(\"expected endpoint resolver to not be nil\")"); 276 }); 277 w.write(""); 278 279 w.write("var endpoint $T", SymbolUtils.createValueSymbolBuilder("Endpoint", AwsGoDependency.AWS_CORE) 280 .build()); 281 w.write("endpoint, err = m.Resolver.ResolveEndpoint(awsmiddleware.GetRegion(ctx), m.Options)"); 282 w.openBlock("if err != nil {", "}", () -> { 283 w.write("return out, metadata, fmt.Errorf(\"failed to resolve service endpoint, %w\", err)"); 284 }); 285 w.write(""); 286 287 w.write("req.URL, err = url.Parse(endpoint.URL)"); 288 w.openBlock("if err != nil {", "}", () -> { 289 w.write("return out, metadata, fmt.Errorf(\"failed to parse endpoint URL: %w\", err)"); 290 }); 291 w.write(""); 292 293 w.openBlock("if len(awsmiddleware.GetSigningName(ctx)) == 0 {", "}", () -> { 294 w.write("signingName := endpoint.SigningName"); 295 w.openBlock("if len(signingName) == 0 {", "}", () -> { 296 w.write("signingName = $S", serviceShape.expectTrait(ServiceTrait.class).getArnNamespace()); 297 }); 298 w.write("ctx = awsmiddleware.SetSigningName(ctx, signingName)"); 299 }); 300 301 // set endoint source on context 302 w.write("ctx = awsmiddleware.SetEndpointSource(ctx, endpoint.Source)"); 303 // set host-name immutable on context 304 w.write("ctx = smithyhttp.SetHostnameImmutable(ctx, endpoint.HostnameImmutable)"); 305 // set signing region on context 306 w.write("ctx = awsmiddleware.SetSigningRegion(ctx, endpoint.SigningRegion)"); 307 // set partition id on context 308 w.write("ctx = awsmiddleware.SetPartitionID(ctx, endpoint.PartitionID)"); 309 310 w.insertTrailingNewline(); 311 w.write("return next.HandleSerialize(ctx, in)"); 312 } 313 generateMiddlewareStructureMembers(GoStackStepMiddlewareGenerator g, GoWriter w)314 private void generateMiddlewareStructureMembers(GoStackStepMiddlewareGenerator g, GoWriter w) { 315 w.write("Resolver $L", RESOLVER_INTERFACE_NAME); 316 w.write("Options $L", RESOLVER_OPTIONS); 317 } 318 getInternalEndpointsSymbol(String symbolName, boolean pointable)319 private Symbol.Builder getInternalEndpointsSymbol(String symbolName, boolean pointable) { 320 Symbol.Builder builder; 321 if (pointable) { 322 builder = SymbolUtils.createPointableSymbolBuilder(symbolName); 323 } else { 324 builder = SymbolUtils.createValueSymbolBuilder(symbolName); 325 } 326 return builder.namespace(getInternalEndpointImportPath(), "/") 327 .putProperty(SymbolUtils.NAMESPACE_ALIAS, "internalendpoints"); 328 } 329 getInternalEndpointImportPath()330 private String getInternalEndpointImportPath() { 331 return settings.getModuleName() + "/" + INTERNAL_ENDPOINT_PACKAGE; 332 } 333 generatePublicResolverTypes(GoWriter writer)334 private void generatePublicResolverTypes(GoWriter writer) { 335 Symbol awsEndpointSymbol = SymbolUtils.createValueSymbolBuilder("Endpoint", AwsGoDependency.AWS_CORE).build(); 336 Symbol internalEndpointsSymbol = getInternalEndpointsSymbol(INTERNAL_RESOLVER_NAME, true).build(); 337 338 Symbol resolverOptionsSymbol = SymbolUtils.createPointableSymbolBuilder(RESOLVER_OPTIONS).build(); 339 writer.writeDocs(String.format("%s is the service endpoint resolver options", 340 resolverOptionsSymbol.getName())); 341 writer.write("type $T = $T", resolverOptionsSymbol, getInternalEndpointsSymbol(INTERNAL_RESOLVER_OPTIONS_NAME, 342 false).build()); 343 writer.write(""); 344 345 // Generate Resolver Interface 346 writer.writeDocs(String.format("%s interface for resolving service endpoints.", RESOLVER_INTERFACE_NAME)); 347 writer.openBlock("type $L interface {", "}", RESOLVER_INTERFACE_NAME, () -> { 348 writer.write("ResolveEndpoint(region string, options $T) ($T, error)", resolverOptionsSymbol, 349 awsEndpointSymbol); 350 }); 351 writer.write("var _ $L = &$T{}", RESOLVER_INTERFACE_NAME, internalEndpointsSymbol); 352 writer.write(""); 353 354 // Resolver Constructor 355 writer.writeDocs(String.format("%s constructs a new service endpoint resolver", RESOLVER_CONSTRUCTOR_NAME)); 356 writer.openBlock("func $L() $P {", "}", RESOLVER_CONSTRUCTOR_NAME, internalEndpointsSymbol, () -> { 357 writer.write("return $T()", getInternalEndpointsSymbol("New", false) 358 .build()); 359 }); 360 361 Symbol resolverFuncSymbol = SymbolUtils.createValueSymbolBuilder(RESOLVER_FUNC_NAME).build(); 362 363 // Generate resolver function creator 364 writer.writeDocs(String.format("%s is a helper utility that wraps a function so it satisfies the %s " 365 + "interface. This is useful when you want to add additional endpoint resolving logic, or stub out " 366 + "specific endpoints with custom values.", RESOLVER_FUNC_NAME, RESOLVER_INTERFACE_NAME)); 367 writer.write("type $T func(region string, options $T) ($T, error)", 368 resolverFuncSymbol, resolverOptionsSymbol, awsEndpointSymbol); 369 370 writeExternalResolveEndpointImplementation(writer, resolverFuncSymbol, "fn", () -> { 371 writer.write("return fn(region, options)"); 372 }); 373 374 // Generate Client Options Configuration Resolver 375 writer.openBlock("func $L(o $P) {", "}", CLIENT_CONFIG_RESOLVER, 376 SymbolUtils.createPointableSymbolBuilder("Options").build(), () -> { 377 writer.openBlock("if o.EndpointResolver != nil {", "}", () -> writer.write("return")); 378 writer.write("o.EndpointResolver = $L()", RESOLVER_CONSTRUCTOR_NAME); 379 }); 380 381 // Generate EndpointResolverFromURL helper 382 writer.writeDocs(String.format("%s returns an EndpointResolver configured using the provided endpoint url. " 383 + "By default, the resolved endpoint resolver uses the client region as signing region, and " 384 + "the endpoint source is set to EndpointSourceCustom." 385 + "You can provide functional options to configure endpoint values for the resolved endpoint.", 386 EndpointResolverFromURL)); 387 writer.openBlock("func $L(url string, optFns ...func($P)) EndpointResolver {", "}", 388 EndpointResolverFromURL, AWS_ENDPOINT, () -> { 389 Symbol customEndpointSource = SymbolUtils.createValueSymbolBuilder( 390 ENDPOINT_SOURCE_CUSTOM, AwsGoDependency.AWS_CORE 391 ).build(); 392 writer.write("e := $T{ URL : url, Source : $T }", AWS_ENDPOINT, customEndpointSource); 393 writer.write("for _, fn := range optFns { fn(&e) }"); 394 writer.write(""); 395 396 writer.openBlock("return $T(", ")", resolverFuncSymbol, () -> { 397 writer.write("func(region string, options $L) ($T, error) {", RESOLVER_OPTIONS, AWS_ENDPOINT); 398 writer.write("if len(e.SigningRegion) == 0 { e.SigningRegion = region }"); 399 writer.write("return e, nil },"); 400 }); 401 }); 402 } 403 writeExternalResolveEndpointImplementation( GoWriter writer, Symbol receiverType, String receiverIdentifier, Runnable body )404 private void writeExternalResolveEndpointImplementation( 405 GoWriter writer, 406 Symbol receiverType, 407 String receiverIdentifier, 408 Runnable body 409 ) { 410 Symbol resolverOptionsSymbol = SymbolUtils.createPointableSymbolBuilder(RESOLVER_OPTIONS).build(); 411 writeResolveEndpointImplementation(writer, receiverType, receiverIdentifier, resolverOptionsSymbol, 412 body); 413 } 414 writeInternalResolveEndpointImplementation( GoWriter writer, Symbol receiverType, String receiverIdentifier, Runnable body )415 private void writeInternalResolveEndpointImplementation( 416 GoWriter writer, 417 Symbol receiverType, 418 String receiverIdentifier, 419 Runnable body 420 ) { 421 Symbol resolverOptionsSymbol = SymbolUtils.createPointableSymbolBuilder(INTERNAL_RESOLVER_OPTIONS_NAME).build(); 422 writeResolveEndpointImplementation(writer, receiverType, receiverIdentifier, resolverOptionsSymbol, 423 body); 424 } 425 426 /** 427 * Writes the ResolveEndpoint function signature to satisfy the EndpointResolver interface. 428 * 429 * @param writer the code writer 430 * @param receiverType the receiver symbol type should be can be value or pointer 431 * @param receiverIdentifier the identifier to use for the receiver 432 * @param resolverOptionsSymbol the symbol for the options 433 * @param body a runnable that will populate the function implementation. 434 */ writeResolveEndpointImplementation( GoWriter writer, Symbol receiverType, String receiverIdentifier, Symbol resolverOptionsSymbol, Runnable body )435 private void writeResolveEndpointImplementation( 436 GoWriter writer, 437 Symbol receiverType, 438 String receiverIdentifier, 439 Symbol resolverOptionsSymbol, 440 Runnable body 441 ) { 442 Symbol awsEndpointSymbol = SymbolUtils.createValueSymbolBuilder("Endpoint", AwsGoDependency.AWS_CORE).build(); 443 writer.openBlock("func ($L $P) ResolveEndpoint(region string, options $T) (endpoint $T, err error) {", "}", 444 receiverIdentifier, receiverType, resolverOptionsSymbol, awsEndpointSymbol, body::run) 445 .write(""); 446 } 447 generateInternalResolverImplementation(GoWriter writer)448 private void generateInternalResolverImplementation(GoWriter writer) { 449 // Options 450 Symbol resolverOptionsSymbol = SymbolUtils.createPointableSymbolBuilder(INTERNAL_RESOLVER_OPTIONS_NAME).build(); 451 writer.writeDocs(String.format("%s is the endpoint resolver configuration options", 452 resolverOptionsSymbol.getName())); 453 writer.openBlock("type $T struct {", "}", resolverOptionsSymbol, () -> { 454 resolveConfigFields.forEach(field -> { 455 writer.write("$L $T", field.getName(), field.getType()); 456 }); 457 }); 458 writer.write(""); 459 460 // Resolver 461 Symbol resolverImplSymbol = SymbolUtils.createPointableSymbolBuilder(INTERNAL_RESOLVER_NAME).build(); 462 463 464 writer.writeDocs(String.format("%s %s endpoint resolver", resolverImplSymbol.getName(), 465 this.resolvedSdkID)); 466 writer.openBlock("type $T struct {", "}", resolverImplSymbol, () -> { 467 writer.write("partitions $T", SymbolUtils.createValueSymbolBuilder("Partitions", 468 AwsGoDependency.AWS_ENDPOINTS).build()); 469 }); 470 writer.write(""); 471 writer.writeDocs("ResolveEndpoint resolves the service endpoint for the given region and options"); 472 writeInternalResolveEndpointImplementation(writer, resolverImplSymbol, "r", () -> { 473 // Currently all APIs require a region to derive the endpoint for that API. If there are ever a truly 474 // region-less API then this should be gated at codegen. 475 writer.addUseImports(AwsGoDependency.AWS_CORE); 476 writer.write("if len(region) == 0 { return endpoint, &aws.MissingRegionError{} }"); 477 writer.write(""); 478 479 Symbol sharedOptions = SymbolUtils.createPointableSymbolBuilder("Options", 480 AwsGoDependency.AWS_ENDPOINTS).build(); 481 writer.openBlock("opt := $T{", "}", sharedOptions, () -> { 482 resolveConfigFields.stream().filter(ResolveConfigField::isShared).forEach(field -> { 483 writer.write("$L: options.$L,", field.getName(), field.getName()); 484 }); 485 }); 486 writer.write("return r.partitions.ResolveEndpoint(region, opt)"); 487 }); 488 writer.write(""); 489 writer.writeDocs(String.format("New returns a new %s", resolverImplSymbol.getName())); 490 writer.openBlock("func New() *$T {", "}", resolverImplSymbol, () -> writer.openBlock("return &$T{", "}", 491 resolverImplSymbol, () -> { 492 writer.write("partitions: $L,", INTERNAL_ENDPOINTS_DATA_NAME); 493 })); 494 } 495 generateInternalEndpointsModel(GoWriter writer)496 private void generateInternalEndpointsModel(GoWriter writer) { 497 writer.addUseImports(AwsGoDependency.AWS_ENDPOINTS); 498 499 Symbol partitionsSymbol = SymbolUtils.createPointableSymbolBuilder("Partitions", AwsGoDependency.AWS_ENDPOINTS) 500 .build(); 501 writer.openBlock("var $L = $T{", "}", INTERNAL_ENDPOINTS_DATA_NAME, partitionsSymbol, () -> { 502 List<Partition> entries = partitions.entrySet().stream() 503 .sorted((x, y) -> { 504 // Always sort standard aws partition first 505 if (x.getKey().equals("aws")) { 506 return -1; 507 } 508 return x.getKey().compareTo(y.getKey()); 509 }).map(Map.Entry::getValue).collect(Collectors.toList()); 510 511 entries.forEach(entry -> { 512 writer.openBlock("{", "},", () -> writePartition(writer, entry)); 513 }); 514 }); 515 } 516 writePartition(GoWriter writer, Partition partition)517 private void writePartition(GoWriter writer, Partition partition) { 518 writer.write("ID: $S,", partition.getId()); 519 Symbol endpointSymbol = SymbolUtils.createValueSymbolBuilder("Endpoint", 520 AwsGoDependency.AWS_ENDPOINTS).build(); 521 writer.openBlock("Defaults: $T{", "},", endpointSymbol, 522 () -> writeEndpoint(writer, partition.getDefaults())); 523 524 writer.addUseImports(AwsGoDependency.REGEXP); 525 writer.write("RegionRegex: regexp.MustCompile($S),", partition.getConfig().expectStringMember("regionRegex") 526 .getValue()); 527 528 Optional<String> optionalPartitionEndpoint = partition.getPartitionEndpoint(); 529 Symbol isRegionalizedValue = SymbolUtils.createValueSymbolBuilder(optionalPartitionEndpoint.isPresent() 530 ? "false" : "true").build(); 531 writer.write("IsRegionalized: $T,", isRegionalizedValue); 532 optionalPartitionEndpoint.ifPresent(s -> writer.write("PartitionEndpoint: $S,", s)); 533 534 Map<StringNode, Node> endpoints = partition.getEndpoints().getMembers(); 535 if (endpoints.size() > 0) { 536 Symbol endpointsSymbol = SymbolUtils.createPointableSymbolBuilder("Endpoints", 537 AwsGoDependency.AWS_ENDPOINTS) 538 .build(); 539 writer.openBlock("Endpoints: $T{", "},", endpointsSymbol, () -> { 540 endpoints.forEach((s, n) -> { 541 writer.openBlock("$S: $T{", "},", s, endpointSymbol, 542 () -> writeEndpoint(writer, n.expectObjectNode())); 543 }); 544 }); 545 } 546 } 547 writeEndpoint(GoWriter writer, ObjectNode node)548 private void writeEndpoint(GoWriter writer, ObjectNode node) { 549 node.getStringMember("hostname").ifPresent(n -> { 550 writer.write("Hostname: $S,", n.getValue()); 551 }); 552 node.getArrayMember("protocols").ifPresent(nodes -> { 553 writer.writeInline("Protocols: []string{"); 554 nodes.forEach(n -> { 555 writer.writeInline("$S, ", n.expectStringNode().getValue()); 556 }); 557 writer.write("},"); 558 }); 559 node.getArrayMember("signatureVersions").ifPresent(nodes -> { 560 writer.writeInline("SignatureVersions: []string{"); 561 nodes.forEach(n -> writer.writeInline("$S, ", n.expectStringNode().getValue())); 562 writer.write("},"); 563 }); 564 node.getMember("credentialScope").ifPresent(n -> { 565 ObjectNode credentialScope = n.expectObjectNode(); 566 Symbol credentialScopeSymbol = SymbolUtils.createValueSymbolBuilder("CredentialScope", 567 AwsGoDependency.AWS_ENDPOINTS) 568 .build(); 569 writer.openBlock("CredentialScope: $T{", "},", credentialScopeSymbol, () -> { 570 credentialScope.getStringMember("region").ifPresent(nn -> { 571 writer.write("Region: $S,", nn.getValue()); 572 }); 573 credentialScope.getStringMember("service").ifPresent(nn -> { 574 writer.write("Service: $S,", nn.getValue()); 575 }); 576 }); 577 }); 578 } 579 580 private static class ResolveConfigField extends ConfigField { 581 private final boolean shared; 582 ResolveConfigField(Builder builder)583 public ResolveConfigField(Builder builder) { 584 super(builder); 585 this.shared = builder.shared; 586 } 587 builder()588 public static Builder builder() { 589 return new Builder(); 590 } 591 isShared()592 public boolean isShared() { 593 return shared; 594 } 595 596 private static class Builder extends ConfigField.Builder { 597 private boolean shared; 598 Builder()599 public Builder() { 600 super(); 601 } 602 603 /** 604 * Set the resolver config field to be shared common parameter 605 * 606 * @param shared whether the resolver config field is shared 607 * @return the builder 608 */ shared(boolean shared)609 public Builder shared(boolean shared) { 610 this.shared = shared; 611 return this; 612 } 613 614 @Override build()615 public ResolveConfigField build() { 616 return new ResolveConfigField(this); 617 } 618 619 @Override name(String name)620 public Builder name(String name) { 621 super.name(name); 622 return this; 623 } 624 625 @Override type(Symbol type)626 public Builder type(Symbol type) { 627 super.type(type); 628 return this; 629 } 630 631 @Override documentation(String documentation)632 public Builder documentation(String documentation) { 633 super.documentation(documentation); 634 return this; 635 } 636 } 637 } 638 639 private final class Partition { 640 private final String id; 641 private final ObjectNode defaults; 642 private final ObjectNode config; 643 private final String dnsSuffix; 644 Partition(ObjectNode config, String partition)645 private Partition(ObjectNode config, String partition) { 646 id = partition; 647 this.config = config; 648 649 // Resolve the partition defaults + the service defaults. 650 ObjectNode serviceDefaults = config.expectObjectMember("defaults").merge(getService() 651 .getObjectMember("defaults") 652 .orElse(Node.objectNode())); 653 654 // Resolve the hostnameTemplate to use for this service in this partition. 655 String hostnameTemplate = serviceDefaults.expectStringMember("hostname").getValue(); 656 hostnameTemplate = hostnameTemplate.replace("{service}", endpointPrefix); 657 hostnameTemplate = hostnameTemplate.replace("{dnsSuffix}", 658 config.expectStringMember("dnsSuffix").getValue()); 659 660 this.defaults = serviceDefaults.withMember("hostname", hostnameTemplate); 661 662 dnsSuffix = config.expectStringMember("dnsSuffix").getValue(); 663 } 664 665 /** 666 * @return the partition defaults merged with the service defaults 667 */ getDefaults()668 ObjectNode getDefaults() { 669 return defaults; 670 } 671 getService()672 ObjectNode getService() { 673 ObjectNode services = config.getObjectMember("services").orElse(Node.objectNode()); 674 return services.getObjectMember(endpointPrefix).orElse(Node.objectNode()); 675 } 676 getEndpoints()677 ObjectNode getEndpoints() { 678 return getService().getObjectMember("endpoints").orElse(Node.objectNode()); 679 } 680 getPartitionEndpoint()681 Optional<String> getPartitionEndpoint() { 682 ObjectNode service = getService(); 683 // Note: regionalized services always use regionalized endpoints. 684 return service.getBooleanMemberOrDefault("isRegionalized", true) 685 ? Optional.empty() 686 : service.getStringMember("partitionEndpoint").map(StringNode::getValue); 687 } 688 getId()689 public String getId() { 690 return id; 691 } 692 getConfig()693 public ObjectNode getConfig() { 694 return config; 695 } 696 } 697 } 698