Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@
#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 <aws/${metadata.projectName}/${metadata.classNamePrefix}_EXPORTS.h>
#if($serviceModel.hasServiceSpecificClientConfig())
\#include <aws/${metadata.projectName}/${metadata.classNamePrefix}ClientConfiguration.h>
#else
\#include <aws/core/client/GenericClientConfiguration.h>
#end
\#include <aws/core/endpoint/DefaultEndpointProvider.h>
\#include <aws/core/endpoint/${epProviderType}.h>
\#include <aws/core/endpoint/EndpointParameter.h>
\#include <aws/core/utils/memory/stl/AWSString.h>
\#include <aws/core/utils/memory/stl/AWSVector.h>
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <aws/${metadata.projectName}/${metadata.classNamePrefix}EndpointProvider.h>
#if ($serviceModel.endpointRules)
\#include <aws/${metadata.projectName}/internal/${metadata.classNamePrefix}EndpointRules.h>
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions tools/code-generation/smithy/cpp-codegen/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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}"))
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* 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 <Prefix>EndpointRules.{h,cpp}} pair carrying the compiled BDD bytecode blob.
*
* <p>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.
*
* <p>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<String, String> serviceMap = parseMapSetting(settings, "c2jMap");
Map<String, String> 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<String, String> serviceMap,
Map<String, String> namespaceMap, String pythonExecutable,
String bddBytecoderPath, CppWriterDelegator writerDelegator) {
String smithyServiceName = ServiceNameUtil.getSmithyServiceName(service, serviceMap);
String exportMacro = ServiceNameUtil.getExportMacro(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, exportMacro));
writerDelegator.useFileWriter(
"source/" + classPrefix + "EndpointRules.cpp",
writer -> EndpointRulesRenderer.renderSource(writer, namespace, classPrefix, smithyServiceName, bytecode));
}

private Map<String, String> 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<String, String> 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 -> sanitize(entry.getValue().expectStringNode().getValue()))))
.orElse(Map.of());
}

private static String sanitize(String s) {
return s.replace(" ", "").replace("-", "").replace("_", "")
.replace("Amazon", "").replace("AWS", "").replace("/", "");
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're duplicating functionality here. ServiceNameUtilJava.sanitizeServiceAbbreviation does this already

}
Loading
Loading