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