Skip to content

Commit d2acbaa

Browse files
mtdowlingAllanZhengYP
authored andcommitted
feat: start endpoint resolver generation
1 parent a655418 commit d2acbaa

File tree

4 files changed

+5928
-0
lines changed

4 files changed

+5928
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2019 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+
16+
package software.amazon.smithy.aws.typescript.codegen;
17+
18+
import java.util.function.BiConsumer;
19+
import java.util.function.Consumer;
20+
import software.amazon.smithy.codegen.core.SymbolProvider;
21+
import software.amazon.smithy.model.Model;
22+
import software.amazon.smithy.typescript.codegen.TypeScriptSettings;
23+
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
24+
import software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration;
25+
26+
/**
27+
* Generates an endpoint resolver from endpoints.json.
28+
*/
29+
public final class AwsEndpointGeneratorIntegration implements TypeScriptIntegration {
30+
@Override
31+
public void writeAdditionalFiles(
32+
TypeScriptSettings settings,
33+
Model model,
34+
SymbolProvider symbolProvider,
35+
BiConsumer<String, Consumer<TypeScriptWriter>> writerFactory
36+
) {
37+
writerFactory.accept("endpoints.ts", writer -> {
38+
new EndpointGenerator(settings.getService(model), writer).run();
39+
});
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/*
2+
* Copyright 2019 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+
16+
package software.amazon.smithy.aws.typescript.codegen;
17+
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
import java.util.Set;
22+
import java.util.TreeMap;
23+
import software.amazon.smithy.aws.traits.ServiceTrait;
24+
import software.amazon.smithy.codegen.core.CodegenException;
25+
import software.amazon.smithy.model.node.Node;
26+
import software.amazon.smithy.model.node.ObjectNode;
27+
import software.amazon.smithy.model.node.StringNode;
28+
import software.amazon.smithy.model.shapes.ServiceShape;
29+
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
30+
import software.amazon.smithy.utils.CaseUtils;
31+
import software.amazon.smithy.utils.IoUtils;
32+
import software.amazon.smithy.utils.OptionalUtils;
33+
import software.amazon.smithy.utils.StringUtils;
34+
35+
/**
36+
* Writes out a file that resolves endpoints using endpoints.json, but the
37+
* created resolver resolves endpoints for a single service.
38+
*/
39+
final class EndpointGenerator implements Runnable {
40+
41+
private static final int VERSION = 3;
42+
43+
private final TypeScriptWriter writer;
44+
private final ObjectNode endpointData;
45+
private final String endpointPrefix;
46+
private final Map<String, Partition> partitions = new TreeMap<>();
47+
private final Map<String, ObjectNode> endpoints = new TreeMap<>();
48+
49+
EndpointGenerator(ServiceShape service, TypeScriptWriter writer) {
50+
this.writer = writer;
51+
endpointPrefix = getEndpointPrefix(service);
52+
endpointData = Node.parse(IoUtils.readUtf8Resource(getClass(), "endpoints.json")).expectObjectNode();
53+
validateVersion();
54+
loadPartitions();
55+
loadServiceEndpoints();
56+
}
57+
58+
private void validateVersion() {
59+
int version = endpointData.expectNumberMember("version").getValue().intValue();
60+
if (version != VERSION) {
61+
throw new CodegenException("Invalid endpoints.json version. Expected version 3, found " + version);
62+
}
63+
}
64+
65+
// TODO: This needs to be updated to better figure out the endpoint prefix.
66+
// Smithy doesn't currently expose this value, but the endpoints file is keyed off of it.
67+
private String getEndpointPrefix(ServiceShape service) {
68+
return service.getTrait(ServiceTrait.class)
69+
.orElseThrow(() -> new CodegenException("No service trait found on " + service.getId()))
70+
.getArnNamespace();
71+
}
72+
73+
private void loadPartitions() {
74+
List<ObjectNode> partitionObjects = endpointData
75+
.expectArrayMember("partitions")
76+
.getElementsAs(Node::expectObjectNode);
77+
78+
for (ObjectNode partition : partitionObjects) {
79+
String partitionName = partition.expectStringMember("partition").getValue();
80+
partitions.put(partitionName, new Partition(partition, partitionName));
81+
}
82+
}
83+
84+
private void loadServiceEndpoints() {
85+
for (Partition partition : partitions.values()) {
86+
String dnsSuffix = partition.dnsSuffix;
87+
ObjectNode serviceData = partition.getService();
88+
ObjectNode endpointMap = serviceData.getObjectMember("endpoints").orElse(Node.objectNode());
89+
90+
for (Map.Entry<String, Node> entry : endpointMap.getStringMap().entrySet()) {
91+
// Merge the endpoint settings into the resolved service settings.
92+
ObjectNode config = partition.getDefaults().merge(entry.getValue().expectObjectNode());
93+
// Resolve the hostname.
94+
String hostName = config.expectStringMember("hostname").getValue();
95+
hostName = hostName.replace("{dnsSuffix}", dnsSuffix);
96+
hostName = hostName.replace("{service}", endpointPrefix);
97+
hostName = hostName.replace("{region}", entry.getKey());
98+
config = config.withMember("hostname", hostName);
99+
endpoints.put(entry.getKey(), config);
100+
}
101+
}
102+
}
103+
104+
@Override
105+
public void run() {
106+
writePartitionTemplates();
107+
writePartitionRegions();
108+
writeEndpointProviderFunction();
109+
}
110+
111+
private void writePartitionTemplates() {
112+
writer.write("// Partition default templates");
113+
partitions.values().forEach(partition -> {
114+
writer.write("const $L = $S;", partition.templateVariableName, partition.templateValue);
115+
});
116+
writer.write("");
117+
}
118+
119+
private void writePartitionRegions() {
120+
writer.write("// Partition regions");
121+
partitions.values().forEach(partition -> {
122+
writer.openBlock("const $L = new Set([", "]);", partition.regionVariableName, () -> {
123+
for (String region : partition.getAllRegions()) {
124+
writer.write("$S,", region);
125+
}
126+
});
127+
});
128+
writer.write("");
129+
}
130+
131+
private void writeEndpointProviderFunction() {
132+
writer.write("export let defaultEndpointProvider: EndpointProvider;");
133+
writer.openBlock("defaultEndpointProvider = function(\n"
134+
+ " region: string,\n"
135+
+ " options?: EndpointOptions\n"
136+
+ ") {", "}", () -> {
137+
writer.openBlock("switch (region) {", "}", () -> {
138+
writer.write("// First, try to match exact region names.");
139+
for (Map.Entry<String, ObjectNode> entry : endpoints.entrySet()) {
140+
writer.write("case $S:", entry.getKey()).indent();
141+
writeEndpointSpecificResolver(entry.getValue());
142+
writer.dedent();
143+
}
144+
writer.write("// Next, try to match partition endpoints.");
145+
writer.write("default:").indent();
146+
partitions.values().forEach(partition -> {
147+
writer.openBlock("if (region in $L) {", "}", partition.regionVariableName, () -> {
148+
writePartitionEndpointResolver(partition);
149+
});
150+
});
151+
// Default to using the AWS partition resolver.
152+
writer.write("// Finally, assume it's an AWS partition endpoint.");
153+
writePartitionEndpointResolver(partitions.get("aws"));
154+
writer.dedent();
155+
});
156+
});
157+
}
158+
159+
private void writePartitionEndpointResolver(Partition partition) {
160+
OptionalUtils.ifPresentOrElse(
161+
partition.getPartitionEndpoint(),
162+
name -> writer.write("return defaultEndpointProvider($S);", name),
163+
() -> {
164+
writer.openBlock("return {", "};", () -> {
165+
String template = partition.templateVariableName;
166+
writer.write("hostname: $L.replace(\"{region}\", region),", template);
167+
writeAdditionalEndpointSettings(partition.getDefaults());
168+
});
169+
}
170+
);
171+
}
172+
173+
private void writeEndpointSpecificResolver(ObjectNode resolved) {
174+
String hostname = resolved.expectStringMember("hostname").getValue();
175+
writer.openBlock("return {", "};", () -> {
176+
writer.write("hostname: $S,", hostname);
177+
writeAdditionalEndpointSettings(resolved);
178+
});
179+
}
180+
181+
// Write credential scope settings into the resolved endpoint object.
182+
private void writeAdditionalEndpointSettings(ObjectNode settings) {
183+
settings.getObjectMember("credentialScope").ifPresent(scope -> {
184+
scope.getStringMember("region").ifPresent(signingRegion -> {
185+
writer.write("signingRegion: $S,", signingRegion);
186+
});
187+
scope.getStringMember("service").ifPresent(signingService -> {
188+
writer.write("signingService: $S,", signingService);
189+
});
190+
});
191+
}
192+
193+
private final class Partition {
194+
final ObjectNode defaults;
195+
final String regionVariableName;
196+
final String templateVariableName;
197+
final String templateValue;
198+
final String dnsSuffix;
199+
private final ObjectNode config;
200+
201+
private Partition(ObjectNode config, String partition) {
202+
this.config = config;
203+
// Resolve the partition defaults + the service defaults.
204+
ObjectNode partitionDefaults = config.expectObjectMember("defaults");
205+
defaults = partitionDefaults.merge(getService().getObjectMember("defaults").orElse(Node.objectNode()));
206+
207+
// Resolve the template to use for this service in this partition.
208+
String template = defaults.expectStringMember("hostname").getValue();
209+
template = template.replace("{service}", endpointPrefix);
210+
template = template.replace("{dnsSuffix}", config.expectStringMember("dnsSuffix").getValue());
211+
templateValue = template;
212+
213+
// Compute the template and regions variable names.
214+
String snakePartition = StringUtils.upperCase(CaseUtils.toSnakeCase(partition));
215+
templateVariableName = snakePartition + "_TEMPLATE";
216+
regionVariableName = snakePartition + "_REGIONS";
217+
218+
dnsSuffix = config.expectStringMember("dnsSuffix").getValue();
219+
}
220+
221+
ObjectNode getDefaults() {
222+
return defaults;
223+
}
224+
225+
ObjectNode getService() {
226+
ObjectNode services = config.getObjectMember("services").orElse(Node.objectNode());
227+
return services.getObjectMember(endpointPrefix).orElse(Node.objectNode());
228+
}
229+
230+
Set<String> getAllRegions() {
231+
return config.getObjectMember("regions").orElse(Node.objectNode()).getStringMap().keySet();
232+
}
233+
234+
Optional<String> getPartitionEndpoint() {
235+
ObjectNode service = getService();
236+
// Note: regionalized services always use regionalized endpoints.
237+
return service.getBooleanMemberOrDefault("isRegionalized", true)
238+
? Optional.empty()
239+
: service.getStringMember("partitionEndpoint").map(StringNode::getValue);
240+
}
241+
}
242+
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
software.amazon.smithy.aws.typescript.codegen.AddAwsRuntimeConfig
22
software.amazon.smithy.aws.typescript.codegen.AddBuiltinPlugins
33
software.amazon.smithy.aws.typescript.codegen.AddProtocols
4+
software.amazon.smithy.aws.typescript.codegen.AwsEndpointGeneratorIntegration
45
software.amazon.smithy.aws.typescript.codegen.AwsServiceIdIntegration

0 commit comments

Comments
 (0)