From 61b0515b8be8cd9554ef17daa22e937273032657 Mon Sep 17 00:00:00 2001 From: sbiscigl Date: Fri, 24 Jul 2026 16:35:48 -0400 Subject: [PATCH] create smithy plugin to generate BDD ruleset --- .../cpp/endpoint/EndpointProviderHeader.vm | 13 +- .../cpp/endpoint/EndpointProviderSource.vm | 7 +- .../smithy/cpp-codegen/build.gradle.kts | 19 +++ .../generators/ServiceNameUtil.java | 15 ++- .../endpointrules/BddBytecoder.java | 69 ++++++++++ .../EndpointRulesCodegenPlugin.java | 120 ++++++++++++++++++ .../endpointrules/EndpointRulesRenderer.java | 86 +++++++++++++ ...ware.amazon.smithy.build.SmithyBuildPlugin | 1 + tools/scripts/codegen/legacy_c2j_cpp_gen.py | 8 ++ tools/scripts/codegen/smithy_cpp_gen.py | 45 ++++++- tools/scripts/run_code_generation.py | 10 +- 11 files changed, 385 insertions(+), 8 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/BddBytecoder.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/EndpointRulesCodegenPlugin.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/EndpointRulesRenderer.java diff --git a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/endpoint/EndpointProviderHeader.vm b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/endpoint/EndpointProviderHeader.vm index 5c0f530794e..92eb290f166 100644 --- a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/endpoint/EndpointProviderHeader.vm +++ b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/endpoint/EndpointProviderHeader.vm @@ -8,6 +8,11 @@ #set($epBuiltInClassName = "${metadata.classNamePrefix}BuiltInParameters") #set($exportMacro = "${CppViewHelper.computeExportValue($metadata.classNamePrefix)}") #set($externMacro = "AWS_${metadata.classNamePrefix.toUpperCase()}_EXTERN") +#if($serviceModel.skipEndpointRulesBlob) +#set($epProviderType = "BDDEndpointProvider") +#else +#set($epProviderType = "DefaultEndpointProvider") +#end #pragma once \#include #if($serviceModel.hasServiceSpecificClientConfig()) @@ -15,7 +20,7 @@ #else \#include #end -\#include +\#include \#include \#include \#include @@ -33,7 +38,7 @@ using ${metadata.classNamePrefix}ClientConfiguration = Aws::${serviceNamespace}: #end using EndpointParameters = Aws::Endpoint::EndpointParameters; using Aws::Endpoint::EndpointProviderBase; -using Aws::Endpoint::DefaultEndpointProvider; +using Aws::Endpoint::${epProviderType}; #if ($serviceModel.endpointRules) #if ($serviceModel.clientContextParams) @@ -92,7 +97,7 @@ using ${metadata.classNamePrefix}EndpointProviderBase = EndpointProviderBase<${metadata.classNamePrefix}ClientConfiguration, ${epBuiltInClassName}, ${epContextClassName}>; using ${metadata.classNamePrefix}DefaultEpProviderBase = - DefaultEndpointProvider<${metadata.classNamePrefix}ClientConfiguration, ${epBuiltInClassName}, ${epContextClassName}>; + ${epProviderType}<${metadata.classNamePrefix}ClientConfiguration, ${epBuiltInClassName}, ${epContextClassName}>; #if($serviceModel.hasServiceSpecificClientConfig() || $serviceModel.clientContextParams) } // namespace Endpoint @@ -107,7 +112,7 @@ ${externMacro} template class ${exportMacro} Aws::Endpoint::EndpointProviderBase<${serviceNamespace}::Endpoint::${metadata.classNamePrefix}ClientConfiguration, ${serviceNamespace}::Endpoint::${epBuiltInClassName}, ${serviceNamespace}::Endpoint::${epContextClassName}>; ${externMacro} template class ${exportMacro} - Aws::Endpoint::DefaultEndpointProvider<${serviceNamespace}::Endpoint::${metadata.classNamePrefix}ClientConfiguration, ${serviceNamespace}::Endpoint::${epBuiltInClassName}, ${serviceNamespace}::Endpoint::${epContextClassName}>; + Aws::Endpoint::${epProviderType}<${serviceNamespace}::Endpoint::${metadata.classNamePrefix}ClientConfiguration, ${serviceNamespace}::Endpoint::${epBuiltInClassName}, ${serviceNamespace}::Endpoint::${epContextClassName}>; } // namespace Endpoint namespace ${serviceNamespace} diff --git a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/endpoint/EndpointProviderSource.vm b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/endpoint/EndpointProviderSource.vm index 0049c13d201..da352420e8f 100644 --- a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/endpoint/EndpointProviderSource.vm +++ b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/endpoint/EndpointProviderSource.vm @@ -6,6 +6,11 @@ #set($endpointPrefix = $metadata.endpointPrefix) #set($epContextClassName = "${metadata.classNamePrefix}ClientContextParameters") #set($epBuiltInClassName = "${metadata.classNamePrefix}BuiltInParameters") +#if($serviceModel.skipEndpointRulesBlob) +#set($epProviderType = "BDDEndpointProvider") +#else +#set($epProviderType = "DefaultEndpointProvider") +#end \#include #if ($serviceModel.endpointRules) \#include @@ -24,7 +29,7 @@ template class Aws::Endpoint::EndpointProviderBase<${serviceNamespace}::Endpoint ${serviceNamespace}::Endpoint::${epBuiltInClassName}, ${serviceNamespace}::Endpoint::${epContextClassName}>; -template class Aws::Endpoint::DefaultEndpointProvider<${serviceNamespace}::Endpoint::${metadata.classNamePrefix}ClientConfiguration, +template class Aws::Endpoint::${epProviderType}<${serviceNamespace}::Endpoint::${metadata.classNamePrefix}ClientConfiguration, ${serviceNamespace}::Endpoint::${epBuiltInClassName}, ${serviceNamespace}::Endpoint::${epContextClassName}>; } // namespace Endpoint diff --git a/tools/code-generation/smithy/cpp-codegen/build.gradle.kts b/tools/code-generation/smithy/cpp-codegen/build.gradle.kts index 3cf6aa1a728..27f4212fd3e 100644 --- a/tools/code-generation/smithy/cpp-codegen/build.gradle.kts +++ b/tools/code-generation/smithy/cpp-codegen/build.gradle.kts @@ -45,6 +45,9 @@ tasks.register("generate-smithy-build") { val c2jMapStr: String = project.findProperty("c2jMap")?.toString() ?: "{}" val namespaceMappings: String = project.findProperty("namespaceMappings")?.toString() ?: "{}" val generateModels: Boolean = project.findProperty("generateModels")?.toString()?.toBoolean() ?: false + val generateEndpointRules: Boolean = project.findProperty("generateEndpointRules")?.toString()?.toBoolean() ?: false + val bddBytecoderPath: String = project.findProperty("bddBytecoderPath")?.toString() ?: "" + val pythonExecutable: String = project.findProperty("pythonExecutable")?.toString() ?: "python3" fileTree(models).filter { it.isFile }.files.forEach eachFile@{ file -> val model = Model.assembler() @@ -74,6 +77,14 @@ tasks.register("generate-smithy-build") { .withMember("namespaceMappings", Node.from(namespaceMappings)) .build()) } + if (generateEndpointRules) { + pluginsNode = pluginsNode.withMember("smithy-cpp-codegen-endpoint-rules", Node.objectNodeBuilder() + .withMember("c2jMap", Node.from(c2jMapStr)) + .withMember("namespaceMappings", Node.from(namespaceMappings)) + .withMember("bddBytecoderPath", Node.from(bddBytecoderPath)) + .withMember("pythonExecutable", Node.from(pythonExecutable)) + .build()) + } val projectionContents = Node.objectNodeBuilder() .withMember("imports", Node.fromStrings("${models.absolutePath}${File.separator}${file.name}")) @@ -127,6 +138,14 @@ tasks.register("generate-smithy-build") { .withMember("namespaceMappings", Node.from(namespaceMappings)) .build()) } + if (generateEndpointRules) { + s3CrtPluginsNode = s3CrtPluginsNode.withMember("smithy-cpp-codegen-endpoint-rules", Node.objectNodeBuilder() + .withMember("c2jMap", Node.from(c2jMapStr)) + .withMember("namespaceMappings", Node.from(namespaceMappings)) + .withMember("bddBytecoderPath", Node.from(bddBytecoderPath)) + .withMember("pythonExecutable", Node.from(pythonExecutable)) + .build()) + } val s3CrtProjectionContents = Node.objectNodeBuilder() .withMember("imports", Node.fromStrings(s3ModelFile.absolutePath)) .withMember("plugins", s3CrtPluginsNode) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ServiceNameUtil.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ServiceNameUtil.java index 65dc089598e..a47c2a320c3 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ServiceNameUtil.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ServiceNameUtil.java @@ -132,7 +132,7 @@ public static String capitalize(String str) { } // Match C2jModelToGeneratorModelTransformer.sanitizeServiceAbbreviation() exactly - private static String sanitizeServiceAbbreviation(String serviceAbbreviation) { + public static String sanitizeServiceAbbreviation(String serviceAbbreviation) { return serviceAbbreviation.replace(" ", "").replace("-", "").replace("_", "").replace("Amazon", "").replace("AWS", "").replace("/", ""); } @@ -165,6 +165,19 @@ public static String getExportMacro(ServiceShape service, Map se return "AWS_" + serviceName.toUpperCase() + "_API"; } + /** + * Returns the hidden-visibility macro for a service (e.g., "AWS_KINESIS_LOCAL"). + * Follows C2J convention: AWS_{UPPERCASED_SERVICE_NAME}_LOCAL + * + * @param service The service shape to generate the macro for + * @param serviceMap Service ID mappings for namespace overrides (reserved for future consistency with getSmithyServiceName) + * @return The local macro in format AWS_{SERVICE_NAME}_LOCAL + */ + public static String getLocalMacro(ServiceShape service, Map serviceMap) { + String serviceName = getServiceName(service); + return "AWS_" + serviceName.toUpperCase() + "_LOCAL"; + } + public static boolean isS3CrtProjection(ServiceShape service) { String serviceId = service.getTrait(ServiceTrait.class) .map(ServiceTrait::getSdkId) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/BddBytecoder.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/BddBytecoder.java new file mode 100644 index 00000000000..989eb8ce499 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/BddBytecoder.java @@ -0,0 +1,69 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.endpointrules; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Shells out to the in-tree {@code bdd-bytecoder.py} compiler to turn the JSON form of a + * {@code smithy.rules#endpointBdd} trait into the binary BDD bytecode blob consumed by the + * CRT {@code BddEngine}. The bytecoder path and python executable are supplied by the caller + * (threaded in from {@code smithy_cpp_gen.py} as gradle properties) because the gradle working + * directory cannot reach the compiler under {@code crt/} on its own. + */ +public final class BddBytecoder { + + private BddBytecoder() {} + + /** + * @param pythonExecutable python interpreter to run (e.g. "python3"). + * @param bytecoderPath absolute path to bdd-bytecoder.py. + * @param traitJson the endpointBdd trait serialized as JSON (the compiler's input). + * @param serviceLabel used only to label temp files / error messages. + * @return the compiled binary bytecode. + */ + public static byte[] compile(String pythonExecutable, String bytecoderPath, String traitJson, String serviceLabel) { + String tempPrefix = "bdd-" + serviceLabel.replaceAll("[^a-zA-Z0-9._-]", "_") + "-"; + try (TempFile input = new TempFile(tempPrefix, ".json"); + TempFile output = new TempFile(tempPrefix, ".bin")) { + Files.writeString(input.path, traitJson, StandardCharsets.UTF_8); + + Process process = new ProcessBuilder( + pythonExecutable, bytecoderPath, input.path.toString(), output.path.toString()) + .redirectErrorStream(true) + .start(); + String consoleOutput = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new RuntimeException("bdd-bytecoder.py failed for '" + serviceLabel + + "' (exit " + exitCode + "):\n" + consoleOutput); + } + byte[] bytecode = Files.readAllBytes(output.path); + if (bytecode.length == 0) { + throw new RuntimeException("bdd-bytecoder.py produced an empty blob for '" + serviceLabel + "'"); + } + return bytecode; + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Failed to run bdd-bytecoder.py for '" + serviceLabel + "'", e); + } + } + + /** A temp file that deletes itself on close, so {@link #compile} can lean on try-with-resources. */ + private static final class TempFile implements AutoCloseable { + private final Path path; + + TempFile(String prefix, String suffix) throws IOException { + this.path = Files.createTempFile(prefix, suffix); + } + + @Override + public void close() throws IOException { + Files.deleteIfExists(path); + } + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/EndpointRulesCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/EndpointRulesCodegenPlugin.java new file mode 100644 index 00000000000..110c5d0a3ca --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/EndpointRulesCodegenPlugin.java @@ -0,0 +1,120 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.endpointrules; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Generates the {@code EndpointRules.{h,cpp}} pair carrying the compiled BDD bytecode blob. + * + *

This is the smithy-side counterpart to the C2J {@code --skip-endpoint-rules-blob} flag: when that + * flag is set the legacy generator deliberately emits nothing for these two files, leaving this plugin + * to write them whole. The emitted ABI is identical to the JSON path (see {@link EndpointRulesRenderer}); + * only the blob contents change from ruleset JSON to binary BDD bytecode. + * + *

The BDD is read from the {@code smithy.rules#endpointBdd} trait on the service shape and compiled by + * shelling out to the in-tree {@code bdd-bytecoder.py}. The interpreter and compiler paths are supplied via + * the {@code pythonExecutable} and {@code bddBytecoderPath} plugin settings (threaded in from + * {@code smithy_cpp_gen.py}) because gradle's working directory cannot reach the compiler under {@code crt/}. + */ +public class EndpointRulesCodegenPlugin implements SmithyBuildPlugin { + + private static final ShapeId ENDPOINT_BDD_TRAIT = ShapeId.from("smithy.rules#endpointBdd"); + private static final String DEFAULT_PYTHON = "python3"; + + @Override + public String getName() { + return "smithy-cpp-codegen-endpoint-rules"; + } + + @Override + public void execute(PluginContext context) { + Model model = context.getModel(); + + // Mock projections stand in for legacy services with no Smithy model, hence no BDD to compile. + if (context.getProjectionName().endsWith(".mock")) { + return; + } + + ObjectNode settings = context.getSettings(); + Map serviceMap = parseMapSetting(settings, "c2jMap"); + Map namespaceMap = parseNamespaceMap(settings); + String pythonExecutable = settings.getStringMemberOrDefault("pythonExecutable", DEFAULT_PYTHON); + String bddBytecoderPath = settings.getStringMember("bddBytecoderPath") + .map(n -> n.getValue()) + .orElseThrow(() -> new IllegalStateException( + "endpoint-rules plugin requires the 'bddBytecoderPath' setting (path to bdd-bytecoder.py)")); + + CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest()); + + // Services without an endpointBdd trait keep resolving via the JSON path, so skip them here. + model.getServiceShapes().stream() + .map(service -> ServiceNameUtil.processS3CrtProjection(service, context.getProjectionName())) + .filter(service -> service.hasTrait(ENDPOINT_BDD_TRAIT)) + .forEach(service -> generateEndpointRules( + service, serviceMap, namespaceMap, pythonExecutable, bddBytecoderPath, writerDelegator)); + + writerDelegator.flushWriters(); + } + + private void generateEndpointRules(ServiceShape service, Map serviceMap, + Map namespaceMap, String pythonExecutable, + String bddBytecoderPath, CppWriterDelegator writerDelegator) { + String smithyServiceName = ServiceNameUtil.getSmithyServiceName(service, serviceMap); + String localMacro = ServiceNameUtil.getLocalMacro(service, serviceMap); + String namespace = namespaceMap.getOrDefault(smithyServiceName, ServiceNameUtil.getServiceName(service)); + String classPrefix = Optional.ofNullable(namespaceMap.get(smithyServiceName)) + .map(ServiceNameUtil::capitalize) + .orElse(ServiceNameUtil.getServiceNameUpperCamel(service)); + + String traitJson = Node.printJson(service.findTrait(ENDPOINT_BDD_TRAIT).orElseThrow().toNode()); + byte[] bytecode = BddBytecoder.compile(pythonExecutable, bddBytecoderPath, traitJson, smithyServiceName); + + writerDelegator.useFileWriter( + "include/aws/" + smithyServiceName + "/internal/" + classPrefix + "EndpointRules.h", + writer -> EndpointRulesRenderer.renderHeader(writer, namespace, classPrefix, smithyServiceName, localMacro)); + writerDelegator.useFileWriter( + "source/" + classPrefix + "EndpointRules.cpp", + writer -> EndpointRulesRenderer.renderSource(writer, namespace, classPrefix, smithyServiceName, bytecode)); + } + + private Map parseMapSetting(ObjectNode settings, String key) { + return settings.getMember(key) + .filter(Node::isStringNode) + .map(Node::expectStringNode) + .map(StringNode::getValue) + .map(Node::parseJsonWithComments) + .map(Node::expectObjectNode) + .map(mapNode -> mapNode.getMembers().entrySet().stream() + .collect(Collectors.toMap( + entry -> entry.getKey().getValue(), + entry -> entry.getValue().expectStringNode().getValue()))) + .orElse(Map.of()); + } + + private Map parseNamespaceMap(ObjectNode settings) { + return settings.getMember("namespaceMappings") + .map(node -> node.expectStringNode().getValue()) + .map(jsonStr -> Node.parseJsonWithComments(jsonStr).expectObjectNode()) + .map(node -> node.getMembers().entrySet().stream() + .collect(Collectors.toMap( + entry -> entry.getKey().getValue(), + entry -> ServiceNameUtil.sanitizeServiceAbbreviation(entry.getValue().expectStringNode().getValue())))) + .orElse(Map.of()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/EndpointRulesRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/EndpointRulesRenderer.java new file mode 100644 index 00000000000..4f68ff2b822 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/endpointrules/EndpointRulesRenderer.java @@ -0,0 +1,86 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.endpointrules; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; + +public final class EndpointRulesRenderer { + + private static final int BYTES_PER_LINE = 25; + + private EndpointRulesRenderer() {} + + public static void renderHeader(CppWriter writer, String namespace, String classPrefix, + String smithyServiceName, String localMacro) { + writer.write("/**"); + writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); + writer.write(" * SPDX-License-Identifier: Apache-2.0."); + writer.write(" */"); + writer.write(""); + writer.write("#pragma once"); + writer.write("#include ", smithyServiceName, classPrefix); + writer.write(""); + writer.write("#include "); + writer.write(""); + writer.write("namespace Aws {"); + writer.write("namespace $L {", namespace); + writer.write("class $1L $2LEndpointRules {", localMacro, classPrefix); + writer.write(" public:"); + writer.write(" static const size_t RulesBlobStrLen;"); + writer.write(" static const size_t RulesBlobSize;"); + writer.write(""); + writer.write(" static const char* GetRulesBlob();"); + writer.write("};"); + writer.write("} // namespace $L", namespace); + writer.write("} // namespace Aws"); + } + + public static void renderSource(CppWriter writer, String namespace, String classPrefix, + String smithyServiceName, byte[] bytecode) { + writer.write("/**"); + writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); + writer.write(" * SPDX-License-Identifier: Apache-2.0."); + writer.write(" */"); + writer.write(""); + writer.write("#include "); + writer.write("#include ", smithyServiceName, classPrefix); + writer.write(""); + writer.write("namespace Aws {"); + writer.write("namespace $L {", namespace); + writer.write("const size_t $1LEndpointRules::RulesBlobStrLen = $2L;", classPrefix, Integer.toString(bytecode.length)); + writer.write("const size_t $1LEndpointRules::RulesBlobSize = $2L;", classPrefix, Integer.toString(bytecode.length)); + writer.write(""); + writer.write("using RulesBlobT = Aws::Array;", classPrefix); + writer.write("static constexpr RulesBlobT RulesBlob = {"); + writeByteArray(writer, bytecode); + writer.write("};"); + writer.write(""); + writer.write("const char* $LEndpointRules::GetRulesBlob() { return RulesBlob.data(); }", classPrefix); + writer.write("} // namespace $L", namespace); + writer.write("} // namespace Aws"); + } + + private static void writeByteArray(CppWriter writer, byte[] bytecode) { + StringBuilder line = new StringBuilder(" {"); + for (int i = 0; i < bytecode.length; i++) { + line.append(charLiteral(bytecode[i])); + if (i != bytecode.length - 1) { + line.append(','); + } + if ((i + 1) % BYTES_PER_LINE == 0 && i != bytecode.length - 1) { + writer.write("$L", line.toString()); + line.setLength(0); + line.append(" "); + } + } + line.append("}"); + writer.write("$L", line.toString()); + } + + private static String charLiteral(byte b) { + return String.format("'\\x%02x'", b & 0xFF); + } + +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin index 1f36b05b7f2..85ed5c1ae39 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin @@ -1,3 +1,4 @@ com.amazonaws.util.awsclientsmithygenerator.generators.pagination.PaginationCodegenPlugin com.amazonaws.util.awsclientsmithygenerator.generators.waiters.WaiterCodegenPlugin com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelCodegenPlugin +com.amazonaws.util.awsclientsmithygenerator.generators.endpointrules.EndpointRulesCodegenPlugin diff --git a/tools/scripts/codegen/legacy_c2j_cpp_gen.py b/tools/scripts/codegen/legacy_c2j_cpp_gen.py index 634d1d7ce50..0a9d765329d 100644 --- a/tools/scripts/codegen/legacy_c2j_cpp_gen.py +++ b/tools/scripts/codegen/legacy_c2j_cpp_gen.py @@ -19,6 +19,7 @@ from codegen.include_tests_util import IncludeTestsUtil from codegen.model_utils import ServiceModel +from codegen.smithy_cpp_gen import bdd_endpoint_services SMITHY_SUPPORTED_CLIENTS = [ "dynamodb", @@ -60,6 +61,8 @@ def __init__(self, args: dict, c2j_models: dict, skip_model_services: set = None self.debug = args.get("debug", False) self.c2j_models = c2j_models self.skip_model_services = skip_model_services or set() + self.use_smithy_bdd_endpoints = args.get("use_smithy_bdd_endpoints", False) + self.bdd_endpoint_services = bdd_endpoint_services() if self.use_smithy_bdd_endpoints else set() generator_location = args["path_to_generator"] or DEFAULT_GENERATOR_LOCATION generator_location = str(Path(generator_location).absolute()) @@ -198,6 +201,11 @@ def _init_common_java_cli(self, if self.debug: print(f" Skipping C2J model generation for {service_name} (using Smithy models)") + if service_name in self.bdd_endpoint_services: + run_command += ["--skip-endpoint-rules-blob"] + if self.debug: + print(f" Skipping C2J endpoint-rules blob for {service_name} (using Smithy BDD endpoints)") + for key, val in kwargs.items(): run_command += [f"--{key}", val] diff --git a/tools/scripts/codegen/smithy_cpp_gen.py b/tools/scripts/codegen/smithy_cpp_gen.py index 7dfc8b77c44..29e0fdc3b31 100644 --- a/tools/scripts/codegen/smithy_cpp_gen.py +++ b/tools/scripts/codegen/smithy_cpp_gen.py @@ -10,21 +10,56 @@ import os import shutil import subprocess +import sys +from pathlib import Path from typing import List SMITHY_GENERATOR_LOCATION = "tools/code-generation/smithy/cpp-codegen" SMITHY_TO_C2J_MAP_FILE = "tools/code-generation/smithy/cpp-codegen/smithy2c2j_service_map.json" SMITHY_NAMPESPACE_MAPPING_FILE = "tools/code-generation/smithy/mapping/smithy-namespace-mapping.json" +SMITHY_API_DESCRIPTIONS_DIR = "tools/code-generation/smithy/api-descriptions" +BDD_BYTECODER_FILE = "crt/aws-crt-cpp/crt/aws-c-sdkutils/compiler/bdd-bytecoder.py" +ENDPOINT_BDD_TRAIT = "smithy.rules#endpointBdd" + + +def bdd_endpoint_services(): + """C2J service names whose Smithy model carries the endpointBdd trait on its service shape, + i.e. the services the Smithy generator will emit a BDD blob for. C2J must skip its JSON + endpoint-rules blob for exactly this set; services outside it (legacy/mock, or models lacking + the trait) keep the JSON path. This MUST stay in sync with EndpointRulesCodegenPlugin's + service.hasTrait(ENDPOINT_BDD_TRAIT) check — divergence causes a missing GetRulesBlob() symbol.""" + with open(os.path.abspath(SMITHY_TO_C2J_MAP_FILE), 'r') as file: + smithy_to_c2j = json.load(file) + descriptions_dir = Path(os.path.abspath(SMITHY_API_DESCRIPTIONS_DIR)) + services = { + smithy_to_c2j.get(model.stem, model.stem) + for model in descriptions_dir.glob("*.json") + if _has_bdd_service_shape(model) + } + if "s3" in services: + services.add("s3-crt") + return services + + +def _has_bdd_service_shape(model_path): + """True if any service shape in the Smithy model carries the endpointBdd trait.""" + shapes = json.loads(model_path.read_text()).get("shapes", {}).values() + return any( + shape.get("type") == "service" and ENDPOINT_BDD_TRAIT in shape.get("traits", {}) + for shape in shapes + ) class SmithyCppGen(object): """Wrapper for Smithy C++ code generator for C++ SDK""" def __init__(self, debug: bool, use_smithy_models: bool = False, - smithy_model_services: set = None, **kwargs): + smithy_model_services: set = None, use_smithy_bdd_endpoints: bool = False, + **kwargs): self.debug = debug self.use_smithy_models = use_smithy_models self.smithy_model_services = smithy_model_services or set() + self.use_smithy_bdd_endpoints = use_smithy_bdd_endpoints with open(os.path.abspath(SMITHY_TO_C2J_MAP_FILE), 'r') as file: self.smithy_c2j_data = json.load(file) self.c2j_smithy_data = {value: key for key, value in self.smithy_c2j_data.items()} @@ -44,6 +79,10 @@ def generate(self, clients_to_build: set): plugins_to_copy.append("smithy-cpp-codegen-models") if self.debug: print(f"Including Smithy model files for: {sorted(self.smithy_model_services)}") + if self.use_smithy_bdd_endpoints: + plugins_to_copy.append("smithy-cpp-codegen-endpoint-rules") + if self.debug: + print("Including Smithy-generated BDD endpoint-rules blob files") self._copy_cpp_codegen_contents( os.path.abspath("tools/code-generation/smithy/cpp-codegen"), plugins_to_copy, @@ -62,6 +101,10 @@ def _generate_pagination(self, smithy_services: List[str], smithy_c2j_data: str) ] if self.use_smithy_models: smithy_codegen_command.append("-PgenerateModels=true") + if self.use_smithy_bdd_endpoints: + smithy_codegen_command.append("-PgenerateEndpointRules=true") + smithy_codegen_command.append("-PbddBytecoderPath=" + os.path.abspath(BDD_BYTECODER_FILE)) + smithy_codegen_command.append("-PpythonExecutable=" + sys.executable) try: if self.debug: diff --git a/tools/scripts/run_code_generation.py b/tools/scripts/run_code_generation.py index 01f86ecfbb9..7d65f7abaf6 100644 --- a/tools/scripts/run_code_generation.py +++ b/tools/scripts/run_code_generation.py @@ -81,6 +81,12 @@ def parse_arguments() -> dict: help="Comma-separated list of services to generate models from Smithy. " "Only effective with --use-smithy-models. " "Defaults to all services in --client_list if omitted.") + parser.add_argument("--use-smithy-bdd-endpoints", + help="Use Smithy-generated BDD bytecode for endpoint resolution instead of the C2J " + "JSON ruleset. C2J skips the JSON endpoint-rules blob and emits a BDD-backed " + "provider; Smithy codegen produces the compiled bytecode blob. When omitted, " + "endpoint resolution keeps using the C2J JSON path.", + action="store_true") args = vars(parser.parse_args()) arg_map = {"debug": args.get("debug", False)} @@ -142,6 +148,7 @@ def parse_arguments() -> dict: arg_map["generate_protocol_tests"] = args.get("generate_protocol_tests", None) arg_map["generate_install_tests"] = args.get("generate_install_tests", None) arg_map["use_smithy_models"] = args.get("use_smithy_models", False) + arg_map["use_smithy_bdd_endpoints"] = args.get("use_smithy_bdd_endpoints", False) smithy_model_services_raw = args.get("smithy_model_services", None) if smithy_model_services_raw: arg_map["smithy_model_services"] = set(smithy_model_services_raw.replace(";", ",").split(",")) @@ -205,7 +212,8 @@ def main(): if clients_to_build: smithy_cpp_gen = SmithyCppGen(args["debug"], use_smithy_models=bool(smithy_model_services), - smithy_model_services=smithy_model_services) + smithy_model_services=smithy_model_services, + use_smithy_bdd_endpoints=args.get("use_smithy_bdd_endpoints", False)) if smithy_cpp_gen.generate(clients_to_build) != 0: print("ERROR: Failed to generate Smithy code!") return -1