Skip to content

Commit 95c284c

Browse files
committed
create smithy plugin to generate BDD ruleset
1 parent 346e644 commit 95c284c

12 files changed

Lines changed: 1477 additions & 4925 deletions

File tree

generated/src/aws-cpp-sdk-s3/source/S3EndpointRules.cpp

Lines changed: 1126 additions & 4775 deletions
Large diffs are not rendered by default.

generated/tests/s3-gen-tests/S3IncludeTests.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
#include <aws/s3/S3Request.h>
1919
#include <aws/s3/S3ServiceClientModel.h>
2020
#include <aws/s3/S3_EXPORTS.h>
21-
#include <aws/s3/internal/S3EndpointRules.h>
2221
#include <aws/s3/model/AbacStatus.h>
2322
#include <aws/s3/model/AbortIncompleteMultipartUpload.h>
2423
#include <aws/s3/model/AbortMultipartUploadRequest.h>

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)

tools/code-generation/smithy/cpp-codegen/gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=file:///opt/gradle-install.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME
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,122 @@
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.stream.Collectors;
20+
21+
/**
22+
* Generates the {@code <Prefix>EndpointRules.{h,cpp}} pair carrying the compiled BDD bytecode blob.
23+
*
24+
* <p>This is the smithy-side counterpart to the C2J {@code --skip-endpoint-rules-blob} flag: when that
25+
* flag is set the legacy generator deliberately emits nothing for these two files, leaving this plugin
26+
* to write them whole. The emitted ABI is identical to the JSON path (see {@link EndpointRulesRenderer});
27+
* only the blob contents change from ruleset JSON to binary BDD bytecode.
28+
*
29+
* <p>The BDD is read from the {@code smithy.rules#endpointBdd} trait on the service shape and compiled by
30+
* shelling out to the in-tree {@code bdd-bytecoder.py}. The interpreter and compiler paths are supplied via
31+
* the {@code pythonExecutable} and {@code bddBytecoderPath} plugin settings (threaded in from
32+
* {@code smithy_cpp_gen.py}) because gradle's working directory cannot reach the compiler under {@code crt/}.
33+
*/
34+
public class EndpointRulesCodegenPlugin implements SmithyBuildPlugin {
35+
36+
private static final ShapeId ENDPOINT_BDD_TRAIT = ShapeId.from("smithy.rules#endpointBdd");
37+
private static final String DEFAULT_PYTHON = "python3";
38+
39+
@Override
40+
public String getName() {
41+
return "smithy-cpp-codegen-endpoint-rules";
42+
}
43+
44+
@Override
45+
public void execute(PluginContext context) {
46+
Model model = context.getModel();
47+
48+
// Mock projections stand in for legacy services with no Smithy model, hence no BDD to compile.
49+
if (context.getProjectionName().endsWith(".mock")) {
50+
return;
51+
}
52+
53+
ObjectNode settings = context.getSettings();
54+
Map<String, String> serviceMap = parseMapSetting(settings, "c2jMap");
55+
Map<String, String> namespaceMap = parseNamespaceMap(settings);
56+
String pythonExecutable = settings.getStringMemberOrDefault("pythonExecutable", DEFAULT_PYTHON);
57+
String bddBytecoderPath = settings.getStringMember("bddBytecoderPath")
58+
.map(n -> n.getValue())
59+
.orElseThrow(() -> new IllegalStateException(
60+
"endpoint-rules plugin requires the 'bddBytecoderPath' setting (path to bdd-bytecoder.py)"));
61+
62+
CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest());
63+
64+
// Services without an endpointBdd trait keep resolving via the JSON path, so skip them here.
65+
model.getServiceShapes().stream()
66+
.map(service -> ServiceNameUtil.processS3CrtProjection(service, context.getProjectionName()))
67+
.filter(service -> service.hasTrait(ENDPOINT_BDD_TRAIT))
68+
.forEach(service -> generateEndpointRules(
69+
service, serviceMap, namespaceMap, pythonExecutable, bddBytecoderPath, writerDelegator));
70+
71+
writerDelegator.flushWriters();
72+
}
73+
74+
private void generateEndpointRules(ServiceShape service, Map<String, String> serviceMap,
75+
Map<String, String> namespaceMap, String pythonExecutable,
76+
String bddBytecoderPath, CppWriterDelegator writerDelegator) {
77+
String serviceName = ServiceNameUtil.getServiceName(service);
78+
String smithyServiceName = ServiceNameUtil.getSmithyServiceName(service, serviceMap);
79+
String exportMacro = ServiceNameUtil.getExportMacro(service, serviceMap);
80+
String namespace = namespaceMap.getOrDefault(smithyServiceName, serviceName);
81+
82+
String traitJson = Node.printJson(service.findTrait(ENDPOINT_BDD_TRAIT).orElseThrow().toNode());
83+
byte[] bytecode = BddBytecoder.compile(pythonExecutable, bddBytecoderPath, traitJson, smithyServiceName);
84+
85+
writerDelegator.useFileWriter(
86+
"include/aws/" + smithyServiceName + "/internal/" + namespace + "EndpointRules.h",
87+
writer -> EndpointRulesRenderer.renderHeader(writer, namespace, smithyServiceName, exportMacro));
88+
writerDelegator.useFileWriter(
89+
"source/" + namespace + "EndpointRules.cpp",
90+
writer -> EndpointRulesRenderer.renderSource(writer, namespace, smithyServiceName, bytecode));
91+
}
92+
93+
private Map<String, String> parseMapSetting(ObjectNode settings, String key) {
94+
return settings.getMember(key)
95+
.filter(Node::isStringNode)
96+
.map(Node::expectStringNode)
97+
.map(StringNode::getValue)
98+
.map(Node::parseJsonWithComments)
99+
.map(Node::expectObjectNode)
100+
.map(mapNode -> mapNode.getMembers().entrySet().stream()
101+
.collect(Collectors.toMap(
102+
entry -> entry.getKey().getValue(),
103+
entry -> entry.getValue().expectStringNode().getValue())))
104+
.orElse(Map.of());
105+
}
106+
107+
private Map<String, String> parseNamespaceMap(ObjectNode settings) {
108+
return settings.getMember("namespaceMappings")
109+
.map(node -> node.expectStringNode().getValue())
110+
.map(jsonStr -> Node.parseJsonWithComments(jsonStr).expectObjectNode())
111+
.map(node -> node.getMembers().entrySet().stream()
112+
.collect(Collectors.toMap(
113+
entry -> entry.getKey().getValue(),
114+
entry -> sanitize(entry.getValue().expectStringNode().getValue()))))
115+
.orElse(Map.of());
116+
}
117+
118+
private static String sanitize(String s) {
119+
return s.replace(" ", "").replace("-", "").replace("_", "")
120+
.replace("Amazon", "").replace("AWS", "").replace("/", "");
121+
}
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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.CppWriter;
8+
9+
/**
10+
* Renders the {@code <Prefix>EndpointRules} header and source that carry a service's compiled BDD
11+
* bytecode blob. The emitted ABI matches the C2J-generated {@code EndpointRules} class exactly
12+
* (class {@code AWS_<SVC>_LOCAL <Prefix>EndpointRules} exposing {@code RulesBlobStrLen},
13+
* {@code RulesBlobSize} and {@code GetRulesBlob()}) so the core {@code CrtEndpointProvider} consumes
14+
* it unchanged. The only difference from the JSON path is that the blob bytes are binary BDD
15+
* bytecode instead of ruleset JSON.
16+
*/
17+
public final class EndpointRulesRenderer {
18+
19+
// Byte-array lines wrap after this many entries, mirroring the C2J template's 25-per-line layout.
20+
// clang-format re-flows afterwards, so this only affects the pre-format footprint.
21+
private static final int BYTES_PER_LINE = 25;
22+
23+
private EndpointRulesRenderer() {}
24+
25+
/**
26+
* include/aws/&lt;svc&gt;/internal/&lt;Prefix&gt;EndpointRules.h
27+
*/
28+
public static void renderHeader(CppWriter writer, String namespace, String smithyServiceName,
29+
String exportMacro) {
30+
String localMacro = toLocalMacro(exportMacro);
31+
writer.write("/**");
32+
writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.");
33+
writer.write(" * SPDX-License-Identifier: Apache-2.0.");
34+
writer.write(" */");
35+
writer.write("");
36+
writer.write("#pragma once");
37+
writer.write("#include <aws/$1L/$2L_EXPORTS.h>", smithyServiceName, namespace);
38+
writer.write("");
39+
writer.write("#include <cstddef>");
40+
writer.write("");
41+
writer.write("namespace Aws {");
42+
writer.write("namespace $L {", namespace);
43+
writer.write("class $1L $2LEndpointRules {", localMacro, namespace);
44+
writer.write(" public:");
45+
writer.write(" static const size_t RulesBlobStrLen;");
46+
writer.write(" static const size_t RulesBlobSize;");
47+
writer.write("");
48+
writer.write(" static const char* GetRulesBlob();");
49+
writer.write("};");
50+
writer.write("} // namespace $L", namespace);
51+
writer.write("} // namespace Aws");
52+
}
53+
54+
/**
55+
* source/&lt;Prefix&gt;EndpointRules.cpp
56+
*/
57+
public static void renderSource(CppWriter writer, String namespace, String smithyServiceName,
58+
byte[] bytecode) {
59+
writer.write("/**");
60+
writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.");
61+
writer.write(" * SPDX-License-Identifier: Apache-2.0.");
62+
writer.write(" */");
63+
writer.write("");
64+
writer.write("#include <aws/core/utils/memory/stl/AWSArray.h>");
65+
writer.write("#include <aws/$1L/internal/$2LEndpointRules.h>", smithyServiceName, namespace);
66+
writer.write("");
67+
writer.write("namespace Aws {");
68+
writer.write("namespace $L {", namespace);
69+
// Binary bytecode has no NUL terminator; StrLen and Size are the same for the BDD blob.
70+
writer.write("const size_t $1LEndpointRules::RulesBlobStrLen = $2L;", namespace, Integer.toString(bytecode.length));
71+
writer.write("const size_t $1LEndpointRules::RulesBlobSize = $2L;", namespace, Integer.toString(bytecode.length));
72+
writer.write("");
73+
writer.write("using RulesBlobT = Aws::Array<const char, $LEndpointRules::RulesBlobSize>;", namespace);
74+
writer.write("static constexpr RulesBlobT RulesBlob = {");
75+
writeByteArray(writer, bytecode);
76+
writer.write("};");
77+
writer.write("");
78+
writer.write("const char* $LEndpointRules::GetRulesBlob() { return RulesBlob.data(); }", namespace);
79+
writer.write("} // namespace $L", namespace);
80+
writer.write("} // namespace Aws");
81+
}
82+
83+
private static void writeByteArray(CppWriter writer, byte[] bytecode) {
84+
StringBuilder line = new StringBuilder(" {");
85+
for (int i = 0; i < bytecode.length; i++) {
86+
line.append(charLiteral(bytecode[i]));
87+
if (i != bytecode.length - 1) {
88+
line.append(',');
89+
}
90+
if ((i + 1) % BYTES_PER_LINE == 0 && i != bytecode.length - 1) {
91+
writer.write("$L", line.toString());
92+
line.setLength(0);
93+
line.append(" ");
94+
}
95+
}
96+
line.append("}");
97+
writer.write("$L", line.toString());
98+
}
99+
100+
/**
101+
* Emits a byte as a C++ char literal. Bytecode is arbitrary binary, so every byte is written as a
102+
* hex escape ('\xNN'); this is unambiguous, avoids narrowing warnings on values &gt; 0x7F, and never
103+
* needs the printable/escape special-casing the C2J JSON template relied on.
104+
*/
105+
private static String charLiteral(byte b) {
106+
return String.format("'\\x%02x'", b & 0xFF);
107+
}
108+
109+
private static String toLocalMacro(String exportMacro) {
110+
// exportMacro is "AWS_<SVC>_API"; the local (hidden-visibility) macro is "AWS_<SVC>_LOCAL".
111+
if (exportMacro.endsWith("_API")) {
112+
return exportMacro.substring(0, exportMacro.length() - "_API".length()) + "_LOCAL";
113+
}
114+
return exportMacro + "_LOCAL";
115+
}
116+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
com.amazonaws.util.awsclientsmithygenerator.generators.pagination.PaginationCodegenPlugin
22
com.amazonaws.util.awsclientsmithygenerator.generators.waiters.WaiterCodegenPlugin
33
com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelCodegenPlugin
4+
com.amazonaws.util.awsclientsmithygenerator.generators.endpointrules.EndpointRulesCodegenPlugin

0 commit comments

Comments
 (0)