Skip to content

Commit 5b2d183

Browse files
committed
create smithy plugin to generate BDD ruleset
1 parent 519f224 commit 5b2d183

10 files changed

Lines changed: 350 additions & 7 deletions

File tree

tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/endpoint/EndpointProviderHeader.vm

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@
88
#set($epBuiltInClassName = "${metadata.classNamePrefix}BuiltInParameters")
99
#set($exportMacro = "${CppViewHelper.computeExportValue($metadata.classNamePrefix)}")
1010
#set($externMacro = "AWS_${metadata.classNamePrefix.toUpperCase()}_EXTERN")
11+
#if($serviceModel.skipEndpointRulesBlob)
12+
#set($epProviderType = "BDDEndpointProvider")
13+
#else
14+
#set($epProviderType = "DefaultEndpointProvider")
15+
#end
1116
#pragma once
1217
\#include <aws/${metadata.projectName}/${metadata.classNamePrefix}_EXPORTS.h>
1318
#if($serviceModel.hasServiceSpecificClientConfig())
1419
\#include <aws/${metadata.projectName}/${metadata.classNamePrefix}ClientConfiguration.h>
1520
#else
1621
\#include <aws/core/client/GenericClientConfiguration.h>
1722
#end
18-
\#include <aws/core/endpoint/DefaultEndpointProvider.h>
23+
\#include <aws/core/endpoint/${epProviderType}.h>
1924
\#include <aws/core/endpoint/EndpointParameter.h>
2025
\#include <aws/core/utils/memory/stl/AWSString.h>
2126
\#include <aws/core/utils/memory/stl/AWSVector.h>
@@ -33,7 +38,7 @@ using ${metadata.classNamePrefix}ClientConfiguration = Aws::${serviceNamespace}:
3338
#end
3439
using EndpointParameters = Aws::Endpoint::EndpointParameters;
3540
using Aws::Endpoint::EndpointProviderBase;
36-
using Aws::Endpoint::DefaultEndpointProvider;
41+
using Aws::Endpoint::${epProviderType};
3742

3843
#if ($serviceModel.endpointRules)
3944
#if ($serviceModel.clientContextParams)
@@ -92,7 +97,7 @@ using ${metadata.classNamePrefix}EndpointProviderBase =
9297
EndpointProviderBase<${metadata.classNamePrefix}ClientConfiguration, ${epBuiltInClassName}, ${epContextClassName}>;
9398

9499
using ${metadata.classNamePrefix}DefaultEpProviderBase =
95-
DefaultEndpointProvider<${metadata.classNamePrefix}ClientConfiguration, ${epBuiltInClassName}, ${epContextClassName}>;
100+
${epProviderType}<${metadata.classNamePrefix}ClientConfiguration, ${epBuiltInClassName}, ${epContextClassName}>;
96101

97102
#if($serviceModel.hasServiceSpecificClientConfig() || $serviceModel.clientContextParams)
98103
} // namespace Endpoint
@@ -107,7 +112,7 @@ ${externMacro} template class ${exportMacro}
107112
Aws::Endpoint::EndpointProviderBase<${serviceNamespace}::Endpoint::${metadata.classNamePrefix}ClientConfiguration, ${serviceNamespace}::Endpoint::${epBuiltInClassName}, ${serviceNamespace}::Endpoint::${epContextClassName}>;
108113

109114
${externMacro} template class ${exportMacro}
110-
Aws::Endpoint::DefaultEndpointProvider<${serviceNamespace}::Endpoint::${metadata.classNamePrefix}ClientConfiguration, ${serviceNamespace}::Endpoint::${epBuiltInClassName}, ${serviceNamespace}::Endpoint::${epContextClassName}>;
115+
Aws::Endpoint::${epProviderType}<${serviceNamespace}::Endpoint::${metadata.classNamePrefix}ClientConfiguration, ${serviceNamespace}::Endpoint::${epBuiltInClassName}, ${serviceNamespace}::Endpoint::${epContextClassName}>;
111116
} // namespace Endpoint
112117

113118
namespace ${serviceNamespace}

tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/endpoint/EndpointProviderSource.vm

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
#set($endpointPrefix = $metadata.endpointPrefix)
77
#set($epContextClassName = "${metadata.classNamePrefix}ClientContextParameters")
88
#set($epBuiltInClassName = "${metadata.classNamePrefix}BuiltInParameters")
9+
#if($serviceModel.skipEndpointRulesBlob)
10+
#set($epProviderType = "BDDEndpointProvider")
11+
#else
12+
#set($epProviderType = "DefaultEndpointProvider")
13+
#end
914
\#include <aws/${metadata.projectName}/${metadata.classNamePrefix}EndpointProvider.h>
1015
#if ($serviceModel.endpointRules)
1116
\#include <aws/${metadata.projectName}/internal/${metadata.classNamePrefix}EndpointRules.h>
@@ -24,7 +29,7 @@ template class Aws::Endpoint::EndpointProviderBase<${serviceNamespace}::Endpoint
2429
${serviceNamespace}::Endpoint::${epBuiltInClassName},
2530
${serviceNamespace}::Endpoint::${epContextClassName}>;
2631

27-
template class Aws::Endpoint::DefaultEndpointProvider<${serviceNamespace}::Endpoint::${metadata.classNamePrefix}ClientConfiguration,
32+
template class Aws::Endpoint::${epProviderType}<${serviceNamespace}::Endpoint::${metadata.classNamePrefix}ClientConfiguration,
2833
${serviceNamespace}::Endpoint::${epBuiltInClassName},
2934
${serviceNamespace}::Endpoint::${epContextClassName}>;
3035
} // namespace Endpoint

tools/code-generation/smithy/cpp-codegen/build.gradle.kts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ tasks.register("generate-smithy-build") {
4545
val c2jMapStr: String = project.findProperty("c2jMap")?.toString() ?: "{}"
4646
val namespaceMappings: String = project.findProperty("namespaceMappings")?.toString() ?: "{}"
4747
val generateModels: Boolean = project.findProperty("generateModels")?.toString()?.toBoolean() ?: false
48+
val generateEndpointRules: Boolean = project.findProperty("generateEndpointRules")?.toString()?.toBoolean() ?: false
49+
val bddBytecoderPath: String = project.findProperty("bddBytecoderPath")?.toString() ?: ""
50+
val pythonExecutable: String = project.findProperty("pythonExecutable")?.toString() ?: "python3"
4851

4952
fileTree(models).filter { it.isFile }.files.forEach eachFile@{ file ->
5053
val model = Model.assembler()
@@ -74,6 +77,14 @@ tasks.register("generate-smithy-build") {
7477
.withMember("namespaceMappings", Node.from(namespaceMappings))
7578
.build())
7679
}
80+
if (generateEndpointRules) {
81+
pluginsNode = pluginsNode.withMember("smithy-cpp-codegen-endpoint-rules", Node.objectNodeBuilder()
82+
.withMember("c2jMap", Node.from(c2jMapStr))
83+
.withMember("namespaceMappings", Node.from(namespaceMappings))
84+
.withMember("bddBytecoderPath", Node.from(bddBytecoderPath))
85+
.withMember("pythonExecutable", Node.from(pythonExecutable))
86+
.build())
87+
}
7788

7889
val projectionContents = Node.objectNodeBuilder()
7990
.withMember("imports", Node.fromStrings("${models.absolutePath}${File.separator}${file.name}"))
@@ -127,6 +138,14 @@ tasks.register("generate-smithy-build") {
127138
.withMember("namespaceMappings", Node.from(namespaceMappings))
128139
.build())
129140
}
141+
if (generateEndpointRules) {
142+
s3CrtPluginsNode = s3CrtPluginsNode.withMember("smithy-cpp-codegen-endpoint-rules", Node.objectNodeBuilder()
143+
.withMember("c2jMap", Node.from(c2jMapStr))
144+
.withMember("namespaceMappings", Node.from(namespaceMappings))
145+
.withMember("bddBytecoderPath", Node.from(bddBytecoderPath))
146+
.withMember("pythonExecutable", Node.from(pythonExecutable))
147+
.build())
148+
}
130149
val s3CrtProjectionContents = Node.objectNodeBuilder()
131150
.withMember("imports", Node.fromStrings(s3ModelFile.absolutePath))
132151
.withMember("plugins", s3CrtPluginsNode)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
package com.amazonaws.util.awsclientsmithygenerator.generators.endpointrules;
6+
7+
import java.io.IOException;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
12+
/**
13+
* Shells out to the in-tree {@code bdd-bytecoder.py} compiler to turn the JSON form of a
14+
* {@code smithy.rules#endpointBdd} trait into the binary BDD bytecode blob consumed by the
15+
* CRT {@code BddEngine}. The bytecoder path and python executable are supplied by the caller
16+
* (threaded in from {@code smithy_cpp_gen.py} as gradle properties) because the gradle working
17+
* directory cannot reach the compiler under {@code crt/} on its own.
18+
*/
19+
public final class BddBytecoder {
20+
21+
private BddBytecoder() {}
22+
23+
/**
24+
* @param pythonExecutable python interpreter to run (e.g. "python3").
25+
* @param bytecoderPath absolute path to bdd-bytecoder.py.
26+
* @param traitJson the endpointBdd trait serialized as JSON (the compiler's input).
27+
* @param serviceLabel used only to label temp files / error messages.
28+
* @return the compiled binary bytecode.
29+
*/
30+
public static byte[] compile(String pythonExecutable, String bytecoderPath, String traitJson, String serviceLabel) {
31+
String tempPrefix = "bdd-" + serviceLabel.replaceAll("[^a-zA-Z0-9._-]", "_") + "-";
32+
try (TempFile input = new TempFile(tempPrefix, ".json");
33+
TempFile output = new TempFile(tempPrefix, ".bin")) {
34+
Files.writeString(input.path, traitJson, StandardCharsets.UTF_8);
35+
36+
Process process = new ProcessBuilder(
37+
pythonExecutable, bytecoderPath, input.path.toString(), output.path.toString())
38+
.redirectErrorStream(true)
39+
.start();
40+
String consoleOutput = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
41+
int exitCode = process.waitFor();
42+
if (exitCode != 0) {
43+
throw new RuntimeException("bdd-bytecoder.py failed for '" + serviceLabel
44+
+ "' (exit " + exitCode + "):\n" + consoleOutput);
45+
}
46+
byte[] bytecode = Files.readAllBytes(output.path);
47+
if (bytecode.length == 0) {
48+
throw new RuntimeException("bdd-bytecoder.py produced an empty blob for '" + serviceLabel + "'");
49+
}
50+
return bytecode;
51+
} catch (IOException | InterruptedException e) {
52+
throw new RuntimeException("Failed to run bdd-bytecoder.py for '" + serviceLabel + "'", e);
53+
}
54+
}
55+
56+
/** A temp file that deletes itself on close, so {@link #compile} can lean on try-with-resources. */
57+
private static final class TempFile implements AutoCloseable {
58+
private final Path path;
59+
60+
TempFile(String prefix, String suffix) throws IOException {
61+
this.path = Files.createTempFile(prefix, suffix);
62+
}
63+
64+
@Override
65+
public void close() throws IOException {
66+
Files.deleteIfExists(path);
67+
}
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
package com.amazonaws.util.awsclientsmithygenerator.generators.endpointrules;
6+
7+
import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator;
8+
import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil;
9+
import software.amazon.smithy.build.PluginContext;
10+
import software.amazon.smithy.build.SmithyBuildPlugin;
11+
import software.amazon.smithy.model.Model;
12+
import software.amazon.smithy.model.node.Node;
13+
import software.amazon.smithy.model.node.ObjectNode;
14+
import software.amazon.smithy.model.node.StringNode;
15+
import software.amazon.smithy.model.shapes.ServiceShape;
16+
import software.amazon.smithy.model.shapes.ShapeId;
17+
18+
import java.util.Map;
19+
import java.util.Optional;
20+
import java.util.stream.Collectors;
21+
22+
/**
23+
* Generates the {@code <Prefix>EndpointRules.{h,cpp}} pair carrying the compiled BDD bytecode blob.
24+
*
25+
* <p>This is the smithy-side counterpart to the C2J {@code --skip-endpoint-rules-blob} flag: when that
26+
* flag is set the legacy generator deliberately emits nothing for these two files, leaving this plugin
27+
* to write them whole. The emitted ABI is identical to the JSON path (see {@link EndpointRulesRenderer});
28+
* only the blob contents change from ruleset JSON to binary BDD bytecode.
29+
*
30+
* <p>The BDD is read from the {@code smithy.rules#endpointBdd} trait on the service shape and compiled by
31+
* shelling out to the in-tree {@code bdd-bytecoder.py}. The interpreter and compiler paths are supplied via
32+
* the {@code pythonExecutable} and {@code bddBytecoderPath} plugin settings (threaded in from
33+
* {@code smithy_cpp_gen.py}) because gradle's working directory cannot reach the compiler under {@code crt/}.
34+
*/
35+
public class EndpointRulesCodegenPlugin implements SmithyBuildPlugin {
36+
37+
private static final ShapeId ENDPOINT_BDD_TRAIT = ShapeId.from("smithy.rules#endpointBdd");
38+
private static final String DEFAULT_PYTHON = "python3";
39+
40+
@Override
41+
public String getName() {
42+
return "smithy-cpp-codegen-endpoint-rules";
43+
}
44+
45+
@Override
46+
public void execute(PluginContext context) {
47+
Model model = context.getModel();
48+
49+
// Mock projections stand in for legacy services with no Smithy model, hence no BDD to compile.
50+
if (context.getProjectionName().endsWith(".mock")) {
51+
return;
52+
}
53+
54+
ObjectNode settings = context.getSettings();
55+
Map<String, String> serviceMap = parseMapSetting(settings, "c2jMap");
56+
Map<String, String> namespaceMap = parseNamespaceMap(settings);
57+
String pythonExecutable = settings.getStringMemberOrDefault("pythonExecutable", DEFAULT_PYTHON);
58+
String bddBytecoderPath = settings.getStringMember("bddBytecoderPath")
59+
.map(n -> n.getValue())
60+
.orElseThrow(() -> new IllegalStateException(
61+
"endpoint-rules plugin requires the 'bddBytecoderPath' setting (path to bdd-bytecoder.py)"));
62+
63+
CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest());
64+
65+
// Services without an endpointBdd trait keep resolving via the JSON path, so skip them here.
66+
model.getServiceShapes().stream()
67+
.map(service -> ServiceNameUtil.processS3CrtProjection(service, context.getProjectionName()))
68+
.filter(service -> service.hasTrait(ENDPOINT_BDD_TRAIT))
69+
.forEach(service -> generateEndpointRules(
70+
service, serviceMap, namespaceMap, pythonExecutable, bddBytecoderPath, writerDelegator));
71+
72+
writerDelegator.flushWriters();
73+
}
74+
75+
private void generateEndpointRules(ServiceShape service, Map<String, String> serviceMap,
76+
Map<String, String> namespaceMap, String pythonExecutable,
77+
String bddBytecoderPath, CppWriterDelegator writerDelegator) {
78+
String smithyServiceName = ServiceNameUtil.getSmithyServiceName(service, serviceMap);
79+
String exportMacro = ServiceNameUtil.getExportMacro(service, serviceMap);
80+
String namespace = namespaceMap.getOrDefault(smithyServiceName, ServiceNameUtil.getServiceName(service));
81+
String classPrefix = Optional.ofNullable(namespaceMap.get(smithyServiceName))
82+
.map(ServiceNameUtil::capitalize)
83+
.orElse(ServiceNameUtil.getServiceNameUpperCamel(service));
84+
85+
String traitJson = Node.printJson(service.findTrait(ENDPOINT_BDD_TRAIT).orElseThrow().toNode());
86+
byte[] bytecode = BddBytecoder.compile(pythonExecutable, bddBytecoderPath, traitJson, smithyServiceName);
87+
88+
writerDelegator.useFileWriter(
89+
"include/aws/" + smithyServiceName + "/internal/" + classPrefix + "EndpointRules.h",
90+
writer -> EndpointRulesRenderer.renderHeader(writer, namespace, classPrefix, smithyServiceName, exportMacro));
91+
writerDelegator.useFileWriter(
92+
"source/" + classPrefix + "EndpointRules.cpp",
93+
writer -> EndpointRulesRenderer.renderSource(writer, namespace, classPrefix, smithyServiceName, bytecode));
94+
}
95+
96+
private Map<String, String> parseMapSetting(ObjectNode settings, String key) {
97+
return settings.getMember(key)
98+
.filter(Node::isStringNode)
99+
.map(Node::expectStringNode)
100+
.map(StringNode::getValue)
101+
.map(Node::parseJsonWithComments)
102+
.map(Node::expectObjectNode)
103+
.map(mapNode -> mapNode.getMembers().entrySet().stream()
104+
.collect(Collectors.toMap(
105+
entry -> entry.getKey().getValue(),
106+
entry -> entry.getValue().expectStringNode().getValue())))
107+
.orElse(Map.of());
108+
}
109+
110+
private Map<String, String> parseNamespaceMap(ObjectNode settings) {
111+
return settings.getMember("namespaceMappings")
112+
.map(node -> node.expectStringNode().getValue())
113+
.map(jsonStr -> Node.parseJsonWithComments(jsonStr).expectObjectNode())
114+
.map(node -> node.getMembers().entrySet().stream()
115+
.collect(Collectors.toMap(
116+
entry -> entry.getKey().getValue(),
117+
entry -> sanitize(entry.getValue().expectStringNode().getValue()))))
118+
.orElse(Map.of());
119+
}
120+
121+
private static String sanitize(String s) {
122+
return s.replace(" ", "").replace("-", "").replace("_", "")
123+
.replace("Amazon", "").replace("AWS", "").replace("/", "");
124+
}
125+
}

0 commit comments

Comments
 (0)