diff --git a/generated/src/aws-cpp-sdk-s3/include/aws/s3/S3ClientPagination.h b/generated/src/aws-cpp-sdk-s3/include/aws/s3/S3ClientPagination.h new file mode 100644 index 00000000000..9afd5a3e990 --- /dev/null +++ b/generated/src/aws-cpp-sdk-s3/include/aws/s3/S3ClientPagination.h @@ -0,0 +1,26 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace Aws { +namespace S3 { + +using ListBucketsPaginator = + Aws::Utils::Pagination::PagePaginator; +using ListDirectoryBucketsPaginator = + Aws::Utils::Pagination::PagePaginator; +using ListObjectsV2Paginator = + Aws::Utils::Pagination::PagePaginator; +using ListPartsPaginator = Aws::Utils::Pagination::PagePaginator; + +} // namespace S3 +} // namespace Aws diff --git a/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListBucketsPaginationTraits.h b/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListBucketsPaginationTraits.h new file mode 100644 index 00000000000..a3ab75a45a2 --- /dev/null +++ b/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListBucketsPaginationTraits.h @@ -0,0 +1,33 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#pragma once +#include +#include +#include +#include + +namespace Aws { +namespace S3 { +namespace Pagination { + +struct ListBucketsPaginationTraits { + using RequestType = Model::ListBucketsRequest; + using ResultType = Model::ListBucketsResult; + using OutcomeType = Model::ListBucketsOutcome; + using ClientType = S3Client; + + static OutcomeType Invoke(ClientType& client, const RequestType& request) { return client.ListBuckets(request); } + + static bool HasMoreResults(const ResultType& result) { return !result.GetContinuationToken().empty(); } + + static void SetNextRequest(const ResultType& result, RequestType& request) { + request.SetContinuationToken(result.GetContinuationToken()); + } +}; + +} // namespace Pagination +} // namespace S3 +} // namespace Aws diff --git a/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListDirectoryBucketsPaginationTraits.h b/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListDirectoryBucketsPaginationTraits.h new file mode 100644 index 00000000000..f7f6de8b6fc --- /dev/null +++ b/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListDirectoryBucketsPaginationTraits.h @@ -0,0 +1,33 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#pragma once +#include +#include +#include +#include + +namespace Aws { +namespace S3 { +namespace Pagination { + +struct ListDirectoryBucketsPaginationTraits { + using RequestType = Model::ListDirectoryBucketsRequest; + using ResultType = Model::ListDirectoryBucketsResult; + using OutcomeType = Model::ListDirectoryBucketsOutcome; + using ClientType = S3Client; + + static OutcomeType Invoke(ClientType& client, const RequestType& request) { return client.ListDirectoryBuckets(request); } + + static bool HasMoreResults(const ResultType& result) { return !result.GetContinuationToken().empty(); } + + static void SetNextRequest(const ResultType& result, RequestType& request) { + request.SetContinuationToken(result.GetContinuationToken()); + } +}; + +} // namespace Pagination +} // namespace S3 +} // namespace Aws diff --git a/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListObjectsV2PaginationTraits.h b/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListObjectsV2PaginationTraits.h new file mode 100644 index 00000000000..718d6333778 --- /dev/null +++ b/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListObjectsV2PaginationTraits.h @@ -0,0 +1,33 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#pragma once +#include +#include +#include +#include + +namespace Aws { +namespace S3 { +namespace Pagination { + +struct ListObjectsV2PaginationTraits { + using RequestType = Model::ListObjectsV2Request; + using ResultType = Model::ListObjectsV2Result; + using OutcomeType = Model::ListObjectsV2Outcome; + using ClientType = S3Client; + + static OutcomeType Invoke(ClientType& client, const RequestType& request) { return client.ListObjectsV2(request); } + + static bool HasMoreResults(const ResultType& result) { return !result.GetNextContinuationToken().empty(); } + + static void SetNextRequest(const ResultType& result, RequestType& request) { + request.SetContinuationToken(result.GetNextContinuationToken()); + } +}; + +} // namespace Pagination +} // namespace S3 +} // namespace Aws diff --git a/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListPartsPaginationTraits.h b/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListPartsPaginationTraits.h new file mode 100644 index 00000000000..dc6a20a510d --- /dev/null +++ b/generated/src/aws-cpp-sdk-s3/include/aws/s3/model/pagination/ListPartsPaginationTraits.h @@ -0,0 +1,33 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#pragma once +#include +#include +#include +#include + +namespace Aws { +namespace S3 { +namespace Pagination { + +struct ListPartsPaginationTraits { + using RequestType = Model::ListPartsRequest; + using ResultType = Model::ListPartsResult; + using OutcomeType = Model::ListPartsOutcome; + using ClientType = S3Client; + + static OutcomeType Invoke(ClientType& client, const RequestType& request) { return client.ListParts(request); } + + static bool HasMoreResults(const ResultType& result) { return result.GetNextPartNumberMarker() != 0; } + + static void SetNextRequest(const ResultType& result, RequestType& request) { + request.SetPartNumberMarker(result.GetNextPartNumberMarker()); + } +}; + +} // namespace Pagination +} // namespace S3 +} // namespace Aws diff --git a/generated/tests/s3-gen-tests/S3PaginationCompilationTests.cpp b/generated/tests/s3-gen-tests/S3PaginationCompilationTests.cpp new file mode 100644 index 00000000000..9ea1602aebc --- /dev/null +++ b/generated/tests/s3-gen-tests/S3PaginationCompilationTests.cpp @@ -0,0 +1,25 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +// Header compilation test for S3 pagination headers +// This test ensures all generated pagination headers compile successfully + +#include +#include +#include +#include +#include + +#include + +class S3PaginationCompilationTest : public Aws::Testing::AwsCppSdkGTestSuite +{ +}; + +TEST_F(S3PaginationCompilationTest, S3PaginationHeadersCompile) +{ + // Test passes if compilation succeeds + SUCCEED(); +} diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/build.gradle.kts b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/build.gradle.kts new file mode 100644 index 00000000000..d929cca7cbb --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `java-library` +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation("software.amazon.smithy:smithy-model:1.51.0") + implementation("software.amazon.smithy:smithy-codegen-core:1.51.0") + implementation("software.amazon.smithy:smithy-aws-traits:1.51.0") + implementation("software.amazon.smithy:smithy-waiters:1.51.0") + implementation("software.amazon.smithy:smithy-rules-engine:1.51.0") + implementation("software.amazon.smithy:smithy-aws-endpoints:1.51.0") + implementation("software.amazon.smithy:smithy-aws-iam-traits:1.51.0") +} diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ClientCodegenSettings.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ClientCodegenSettings.java new file mode 100644 index 00000000000..f992d3b76df --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ClientCodegenSettings.java @@ -0,0 +1,20 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators; + +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; + +public class ClientCodegenSettings { + private final ShapeId service; + + public ClientCodegenSettings(ObjectNode settings) { + this.service = ShapeId.from(settings.expectStringMember("service").getValue()); + } + + public ShapeId getService() { + return service; + } +} \ No newline at end of file diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CompilationTestParser.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CompilationTestParser.java new file mode 100644 index 00000000000..afcaac07d93 --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CompilationTestParser.java @@ -0,0 +1,45 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators; + +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.model.shapes.*; +import software.amazon.smithy.aws.traits.ServiceTrait; +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import java.util.*; +import java.util.function.Consumer; + +public class CompilationTestParser { + private final ServiceShape service; + private final CppWriterDelegator writerDelegator; + private final String testType; + private final Consumer renderFunction; + private final Map c2jMap; + + public CompilationTestParser(PluginContext context, ServiceShape service, List operations, + String testType, Consumer renderFunction, Map c2jMap) { + this.service = service; + this.writerDelegator = new CppWriterDelegator(context.getFileManifest()); + this.testType = testType; + this.renderFunction = renderFunction; + this.c2jMap = c2jMap; + } + + public void run() { + generateCompilationTest(); + writerDelegator.flushWriters(); + } + + private void generateCompilationTest() { + String serviceName = ServiceNameUtil.getServiceName(service); + String c2jServiceName = ServiceNameUtil.getC2jServiceName(service, c2jMap); + + writerDelegator.useFileWriter( + "generated/tests/" + c2jServiceName + "-gen-tests/" + serviceName + testType + "CompilationTests.cpp", + renderFunction + ); + } +} \ No newline at end of file diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppImportContainer.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppImportContainer.java new file mode 100644 index 00000000000..8c26688ae5f --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppImportContainer.java @@ -0,0 +1,36 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators; + +import software.amazon.smithy.codegen.core.ImportContainer; +import software.amazon.smithy.codegen.core.Symbol; +import java.util.HashSet; +import java.util.Set; + +/** + * CppImportContainer tracks symbols used in code generation. + * For C++, we handle includes manually via writeInclude method. + */ +public class CppImportContainer implements ImportContainer { + private final Set imports = new HashSet<>(); + + @Override + public void importSymbol(Symbol symbol, String alias) { + // Track the symbol's namespace for potential future use + if (symbol.getNamespace() != null && !symbol.getNamespace().isEmpty()) { + imports.add(symbol.getNamespace()); + } + } + + public Set getImports() { + return imports; + } + + @Override + public String toString() { + // Return empty string since we handle includes manually + return ""; + } +} \ No newline at end of file diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriter.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriter.java new file mode 100644 index 00000000000..d3729c6223e --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriter.java @@ -0,0 +1,45 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators; + +import software.amazon.smithy.codegen.core.SymbolWriter; + +public class CppWriter extends SymbolWriter { + + public CppWriter() { + super(new CppImportContainer()); + + // Add custom formatter for symbols + putFormatter('T', (arg, indent) -> { + if (!(arg instanceof software.amazon.smithy.codegen.core.Symbol symbol)) { + throw new software.amazon.smithy.codegen.core.CodegenException("Expected a symbol but got " + arg); + } + + // Record our symbol so we can generate appropriate includes later if needed + getImportContainer().importSymbol(symbol, null); + + // For C++, use namespace::name syntax (e.g. Aws::S3::Model::ListBucketsRequest) + if (symbol.getNamespace() != null && !symbol.getNamespace().isEmpty()) { + return symbol.getNamespace() + "::" + symbol.getName(); + } + return symbol.getName(); + }); + } + + public CppWriter writeInclude(String header) { + write("#include <$L>", header); + return this; + } + + public CppWriter writeNamespaceOpen(String namespace) { + openBlock("namespace $L\n{", namespace); + return this; + } + + public CppWriter writeNamespaceClose(String namespace) { + closeBlock("} // namespace $L", namespace); + return this; + } +} \ No newline at end of file diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java new file mode 100644 index 00000000000..9d42e8b7090 --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java @@ -0,0 +1,37 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators; + +import software.amazon.smithy.build.FileManifest; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class CppWriterDelegator { + private final FileManifest fileManifest; + private final Map writers = new HashMap<>(); + + public CppWriterDelegator(FileManifest fileManifest) { + this.fileManifest = fileManifest; + } + + public void useFileWriter(String filename, Consumer writerConsumer) { + CppWriter writer = writers.computeIfAbsent(filename, k -> new CppWriter()); + writerConsumer.accept(writer); + } + + public void flushWriters() { + writers.forEach((filename, writer) -> { + try { + Path outputPath = fileManifest.getBaseDir().resolve(filename); + java.nio.file.Files.createDirectories(outputPath.getParent()); + java.nio.file.Files.writeString(outputPath, writer.toString()); + } catch (Exception e) { + throw new RuntimeException("Failed to write file: " + filename, e); + } + }); + } +} \ No newline at end of file diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/FeatureParser.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/FeatureParser.java new file mode 100644 index 00000000000..4d70ac642ef --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/FeatureParser.java @@ -0,0 +1,67 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators; + +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.*; +import software.amazon.smithy.aws.traits.ServiceTrait; +import java.util.*; +import java.util.function.Consumer; + +public class FeatureParser { + private final PluginContext context; + private final ServiceShape service; + private final List operations; + private final CppWriterDelegator writerDelegator; + private final Map c2jMap; + private final String featureName; + + public FeatureParser(PluginContext context, ServiceShape service, List operations, String featureName) { + this.context = context; + this.service = service; + this.operations = operations; + this.featureName = featureName; + this.writerDelegator = new CppWriterDelegator(context.getFileManifest()); + + // Initialize c2j map + this.c2jMap = new HashMap<>(); + ObjectNode settings = context.getSettings(); + if (settings.containsMember("c2jMap")) { + Node c2jMapNode = settings.expectMember("c2jMap"); + if (c2jMapNode.isStringNode()) { + String jsonStr = c2jMapNode.expectStringNode().getValue(); + ObjectNode mapNode = Node.parseJsonWithComments(jsonStr).expectObjectNode(); + mapNode.getMembers().forEach((key, value) -> { + this.c2jMap.put(key.getValue(), value.expectStringNode().getValue()); + }); + } + } + } + + public void run(Consumer> generationLogic) { + generationLogic.accept(this); + writerDelegator.flushWriters(); + } + + public void generateClientHeader(String fileName, Consumer generator) { + String serviceName = ServiceNameUtil.getServiceName(service); + String c2jServiceName = ServiceNameUtil.getC2jServiceName(service, c2jMap); + + writerDelegator.useFileWriter( + "include/aws/" + c2jServiceName + "/" + fileName, + generator + ); + } + + public PluginContext getContext() { return context; } + public ServiceShape getService() { return service; } + public List getOperations() { return operations; } + public String getFeatureName() { return featureName; } + public Map getC2jMap() { return c2jMap; } + public String getServiceName() { return ServiceNameUtil.getServiceName(service); } + public String getC2jServiceName() { return ServiceNameUtil.getC2jServiceName(service, c2jMap); } +} \ No newline at end of file diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/OperationData.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/OperationData.java new file mode 100644 index 00000000000..7d4c3bec889 --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/OperationData.java @@ -0,0 +1,32 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators; + +import software.amazon.smithy.model.shapes.*; +import software.amazon.smithy.model.traits.Trait; + +public class OperationData { + private final OperationShape operation; + private final T trait; + private final ServiceShape service; + + public OperationData(OperationShape operation, T trait, ServiceShape service) { + this.operation = operation; + this.trait = trait; + this.service = service; + } + + public OperationShape getOperation() { + return operation; + } + + public T getTrait() { + return trait; + } + + public ServiceShape getService() { + return service; + } +} diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/PaginationCodegenPlugin.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/PaginationCodegenPlugin.java new file mode 100644 index 00000000000..bd64ffd5c9a --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/PaginationCodegenPlugin.java @@ -0,0 +1,68 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators; + +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.traits.PaginatedTrait; +import software.amazon.smithy.model.traits.DeprecatedTrait; +import software.amazon.smithy.model.shapes.*; +import com.amazonaws.util.awsclientsmithygenerator.generators.OperationData; +import com.amazonaws.util.awsclientsmithygenerator.generators.FeatureParser; +import com.amazonaws.util.awsclientsmithygenerator.generators.templates.PaginationTraitsGenerator; +import com.amazonaws.util.awsclientsmithygenerator.generators.templates.PaginationClientHeaderGenerator; +import com.amazonaws.util.awsclientsmithygenerator.generators.templates.PaginationCompilationTestGenerator; +import java.util.List; +import java.util.stream.Collectors; + +public class PaginationCodegenPlugin implements SmithyBuildPlugin { + + @Override + public String getName() { + return "cpp-codegen-pagination-plugin"; + } + + @Override + public void execute(PluginContext context) { + var model = context.getModel(); + + for (ServiceShape service : model.getServiceShapes()) { + // Find paginated operations using TopDownIndex, excluding deprecated operations + List> paginatedOps = TopDownIndex.of(model).getContainedOperations(service).stream() + .filter(op -> op.hasTrait(PaginatedTrait.class)) + .filter(op -> !op.hasTrait(DeprecatedTrait.class)) + .map(op -> new OperationData<>(op, op.expectTrait(PaginatedTrait.class), service)) + .collect(Collectors.toList()); + + if (!paginatedOps.isEmpty()) { + // Generate pagination files + FeatureParser> parser = new FeatureParser<>(context, service, paginatedOps, "Pagination"); + parser.run(featureParser -> { + String serviceName = featureParser.getServiceName(); + + // Generate client pagination header + featureParser.generateClientHeader( + serviceName + "ClientPagination.h", + writer -> new PaginationClientHeaderGenerator(featureParser.getService(), featureParser.getOperations(), featureParser.getC2jMap()).render(writer) + ); + + // Generate pagination traits headers + PaginationTraitsGenerator traitsGenerator = new PaginationTraitsGenerator( + featureParser.getContext(), + featureParser.getService(), + featureParser.getOperations(), + featureParser.getC2jServiceName() + ); + traitsGenerator.write(); + }); + + // Generate compilation test + PaginationCompilationTestGenerator testGenerator = new PaginationCompilationTestGenerator(context, service, paginatedOps, parser.getC2jMap()); + testGenerator.run(); + } + } + } +} diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ServiceNameUtil.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ServiceNameUtil.java new file mode 100644 index 00000000000..94153fc6f0f --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ServiceNameUtil.java @@ -0,0 +1,95 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators; + +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.traits.TitleTrait; +import java.util.Map; +import java.util.Set; + +public final class ServiceNameUtil { + + // Legacy service IDs that need special handling to match what the legacy c2j code generator produced + // This logic is copied from C2jModelToGeneratorModelTransformer.convertMetadata() method + private static final Set LEGACY_SERVICE_IDS = Set.of( + "amp", "appintegrations", "amazonappintegrationservice", "billingconductor", "clouddirectory", "cloudfront", + "cloudsearch", "cloudsearchdomain", "codeartifact", "codestar-notifications", + "config", "databrew", "elasticache", "emr-containers", + "entitlement.marketplace", "events", "evidently", "forecast", "forecastquery", + "grafana", "importexport", "inspector", "lambda", "location", "lookoutvision", + "m2", "migrationhubstrategy", "mobile", "mobileanalytics", "mq", "nimble", + "opensearch", "rbin", "rds-data", "redshift-data", "resiliencehub", "rum", + "sagemaker-a2i-runtime", "sagemaker-edge", "schemas", "sdb", "transcribe", + "transcribe-streaming", "wisdom", "lex", "lexv2-runtime", "lexv2-models", + "marketplace-entitlement", "sagemaker-runtime", "awstransfer", "transcribestreaming", + "dynamodbstreams" + ); + + // TODO: Remove hardcoded mappings once Smithy models include serviceAbbreviation trait + // Smithy sdkId matches C2J serviceId, but C2J prioritizes serviceAbbreviation (which Smithy lacks). + // 18 of 66 services where C2J abbrev != serviceId require these mappings to match C2J output. + // Hardcoded mappings where Smithy sdkId doesn't match C2J serviceAbbreviation + private static final Map SMITHY_TO_C2J_NAMESPACE = Map.ofEntries( + Map.entry("b2bi", "B2BI"), + Map.entry("cloudcontrol", "CloudControlApi"), + Map.entry("ecrpublic", "ECRPublic"), + Map.entry("evs", "EVS"), + Map.entry("finspacedata", "FinSpaceData"), + Map.entry("fis", "FIS"), + Map.entry("identitystore", "IdentityStore"), + Map.entry("inspectorscan", "inspectorscan"), + Map.entry("iotdeviceadvisor", "IoTDeviceAdvisor"), + Map.entry("ivs", "IVS"), + Map.entry("ivsrealtime", "ivsrealtime"), + Map.entry("kinesisvideosignaling", "KinesisVideoSignalingChannels"), + Map.entry("marketplaceagreement", "AgreementService"), + Map.entry("mediapackagev2", "mediapackagev2"), + Map.entry("savingsplans", "SavingsPlans"), + Map.entry("servicecatalogappregistry", "AppRegistry"), + Map.entry("sesv2", "SESV2"), + Map.entry("synthetics", "Synthetics") + ); + + public static String getServiceName(ServiceShape service) { + String serviceId = service.getTrait(ServiceTrait.class) + .map(ServiceTrait::getSdkId) + .orElse(service.getId().getName()); + + // Check hardcoded mappings first + String sanitized = sanitizeServiceAbbreviation(serviceId); + String mapped = SMITHY_TO_C2J_NAMESPACE.get(sanitized.toLowerCase()); + if (mapped != null) { + return mapped; + } + + if (LEGACY_SERVICE_IDS.contains(serviceId.toLowerCase())) { + String title = service.getTrait(TitleTrait.class) + .map(TitleTrait::getValue) + .orElse(null); + if (title != null) { + return sanitizeServiceAbbreviation(title); + } + } + + return sanitized; + } + + // Match C2jModelToGeneratorModelTransformer.sanitizeServiceAbbreviation() exactly + private static String sanitizeServiceAbbreviation(String serviceAbbreviation) { + return serviceAbbreviation.replace(" ", "").replace("-", "").replace("_", "").replace("Amazon", "").replace("AWS", "").replace("/", ""); + } + + public static String getC2jServiceName(ServiceShape service, Map c2jMap) { + String sdkId = service.getTrait(ServiceTrait.class) + .map(ServiceTrait::getSdkId) + .orElse(service.getId().getName()) + .trim() + .toLowerCase() + .replace(" ", "-"); + + return c2jMap != null ? c2jMap.getOrDefault(sdkId, sdkId) : sdkId; + } +} \ No newline at end of file diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java new file mode 100644 index 00000000000..0745a619a92 --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java @@ -0,0 +1,25 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators; + +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.*; +import java.util.HashSet; +import java.util.Set; + +public class ShapeUtil { + + /** + * Returns all shapes referenced by a root shape, recursively. + */ + public static Set getReferences(Model model, Shape root) { + Set refs = new HashSet<>(); + refs.add(root); + for (var member : root.members()) { + refs.addAll(getReferences(model, model.expectShape(member.getTarget()))); + } + return refs; + } +} \ No newline at end of file diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/templates/PaginationClientHeaderGenerator.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/templates/PaginationClientHeaderGenerator.java new file mode 100644 index 00000000000..e8d38763739 --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/templates/PaginationClientHeaderGenerator.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.templates; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.OperationData; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.traits.PaginatedTrait; +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import java.util.List; +import java.util.Map; + +public class PaginationClientHeaderGenerator { + private final ServiceShape service; + private final List> paginatedOps; + private final Map c2jMap; + + public PaginationClientHeaderGenerator(ServiceShape service, List> paginatedOps, Map c2jMap) { + this.service = service; + this.paginatedOps = paginatedOps; + this.c2jMap = c2jMap; + } + + public void render(CppWriter writer) { + String serviceName = ServiceNameUtil.getServiceName(service); + String c2jServiceName = ServiceNameUtil.getC2jServiceName(service, c2jMap); + + // Header and includes + 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"); + + renderIncludes(writer, serviceName, c2jServiceName); + renderNamespaces(writer, serviceName); + } + + private void renderIncludes(CppWriter writer, String serviceName, String c2jServiceName) { + writer.writeInclude("aws/" + c2jServiceName + "/" + serviceName + "Client.h"); + writer.writeInclude("aws/core/utils/pagination/Paginator.h"); + + for (OperationData data : paginatedOps) { + String opName = data.getOperation().getId().getName(); + writer.writeInclude("aws/" + c2jServiceName + "/model/pagination/" + opName + "PaginationTraits.h"); + } + writer.write(""); + } + + private void renderNamespaces(CppWriter writer, String serviceName) { + writer.writeNamespaceOpen("Aws"); + writer.writeNamespaceOpen(serviceName); + writer.write(""); + + for (OperationData data : paginatedOps) { + String opName = data.getOperation().getId().getName(); + writer.write("using $LPaginator = Aws::Utils::Pagination::PagePaginator<$LClient, Model::$LRequest, Pagination::$LPaginationTraits>;", + opName, serviceName, opName, opName); + } + + writer.write(""); + writer.writeNamespaceClose(serviceName); + writer.writeNamespaceClose("Aws"); + } +} \ No newline at end of file diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/templates/PaginationCompilationTestGenerator.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/templates/PaginationCompilationTestGenerator.java new file mode 100644 index 00000000000..69c7ee22dc1 --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/templates/PaginationCompilationTestGenerator.java @@ -0,0 +1,98 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.templates; + +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.model.shapes.*; +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.CompilationTestParser; +import com.amazonaws.util.awsclientsmithygenerator.generators.OperationData; +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import software.amazon.smithy.model.traits.PaginatedTrait; +import java.util.*; + +public class PaginationCompilationTestGenerator { + private final CompilationTestParser> parser; + private final List> allPaginatedOps; + private final Map c2jMap; + + public PaginationCompilationTestGenerator(PluginContext context, ServiceShape service, List> allPaginatedOps, Map c2jMap) { + this.allPaginatedOps = allPaginatedOps; + this.c2jMap = c2jMap; + this.parser = new CompilationTestParser<>( + context, + service, + allPaginatedOps, + "Pagination", + this::render, + c2jMap + ); + } + + public void run() { + parser.run(); + } + + public void render(CppWriter writer) { + // Get service from the first operation since all operations belong to the same service + ServiceShape service = allPaginatedOps.get(0).getService(); + String serviceName = ServiceNameUtil.getServiceName(service); + + writer.write("/**") + .write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.") + .write(" * SPDX-License-Identifier: Apache-2.0.") + .write(" */") + .write("") + .write("// Header compilation test for " + serviceName + " pagination headers") + .write("// This test ensures all generated pagination headers compile successfully") + .write(""); + + // Include all service headers + writeIncludes(writer); + + writer.write("") + .write("#include ") + .write(""); + + writer.openBlock("class " + serviceName + "PaginationCompilationTest : public Aws::Testing::AwsCppSdkGTestSuite\n{", "};", () -> { + // Empty class body + }); + + writer.write(""); + + writer.openBlock("TEST_F(" + serviceName + "PaginationCompilationTest, " + serviceName + "PaginationHeadersCompile)\n{", "}", () -> { + writer.write(" // Test passes if compilation succeeds") + .write(" SUCCEED();"); + }); + } + + private void writeIncludes(CppWriter writer) { + Set clientHeaders = new HashSet<>(); + Set traitHeaders = new HashSet<>(); + + for (OperationData paginationData : allPaginatedOps) { + ServiceShape service = paginationData.getService(); + String serviceName = ServiceNameUtil.getServiceName(service); + String c2jServiceName = ServiceNameUtil.getC2jServiceName(service, c2jMap); + + // Collect unique client headers + clientHeaders.add("aws/" + c2jServiceName + "/" + serviceName + "ClientPagination.h"); + + // Collect unique trait headers + String operationName = paginationData.getOperation().getId().getName(); + traitHeaders.add("aws/" + c2jServiceName + "/model/pagination/" + operationName + "PaginationTraits.h"); + } + + // Write unique client headers + for (String header : clientHeaders) { + writer.writeInclude(header); + } + + // Write unique trait headers + for (String header : traitHeaders) { + writer.writeInclude(header); + } + } +} \ No newline at end of file diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/templates/PaginationTraitsGenerator.java b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/templates/PaginationTraitsGenerator.java new file mode 100644 index 00000000000..c2fd9c7b5aa --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/templates/PaginationTraitsGenerator.java @@ -0,0 +1,264 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.templates; + +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.model.shapes.*; +import software.amazon.smithy.model.traits.PaginatedTrait; +import software.amazon.smithy.aws.traits.ServiceTrait; +import com.amazonaws.util.awsclientsmithygenerator.generators.OperationData; +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import java.util.*; +import java.util.Arrays; + +public class PaginationTraitsGenerator { + private final PluginContext context; + private final ServiceShape service; + private final List> paginatedOps; + private final String c2jServiceName; + + private final CppWriterDelegator writerDelegator; + + public PaginationTraitsGenerator(PluginContext context, ServiceShape service, List> paginatedOps, String c2jServiceName) { + this.context = context; + this.service = service; + this.paginatedOps = paginatedOps; + this.c2jServiceName = c2jServiceName; + this.writerDelegator = new CppWriterDelegator(context.getFileManifest()); + } + + public void write() { + String serviceName = ServiceNameUtil.getServiceName(service); + + for (OperationData data : paginatedOps) { + String fileName = "include/aws/" + c2jServiceName + "/model/pagination/" + data.getOperation().getId().getName() + "PaginationTraits.h"; + + writerDelegator.useFileWriter(fileName, writer -> { + generateTraitsHeader(writer, data, serviceName); + }); + } + + writerDelegator.flushWriters(); + } + + private void generateTraitsHeader(CppWriter writer, OperationData data, String serviceName) { + OperationShape op = data.getOperation(); + PaginatedTrait trait = data.getTrait(); + String opName = op.getId().getName(); + + // Header comment + writer.write("/**") + .write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.") + .write(" * SPDX-License-Identifier: Apache-2.0.") + .write(" */") + .write("") + .write("#pragma once"); + + // Includes - detect suffix like C2J renameShape logic + String resultSuffix = getResultSuffix(opName); + writer.writeInclude("aws/" + c2jServiceName + "/" + serviceName + "_EXPORTS.h") + .writeInclude("aws/" + c2jServiceName + "/model/" + opName + "Request.h") + .writeInclude("aws/" + c2jServiceName + "/model/" + opName + resultSuffix + ".h") + .writeInclude("aws/" + c2jServiceName + "/" + serviceName + "Client.h") + .write(""); + + // Namespaces + writer.writeNamespaceOpen("Aws") + .writeNamespaceOpen(serviceName) + .writeNamespaceOpen("Pagination") + .write(""); + + // Struct definition + writer.openBlock("struct " + opName + "PaginationTraits\n{", "};", () -> { + // Use detected suffix to match C2J renameShape logic + writer.write(" using RequestType = Model::$LRequest;", opName) + .write(" using ResultType = Model::$L$L;", opName, resultSuffix) + .write(" using OutcomeType = Model::$LOutcome;", opName) + .write(" using ClientType = $LClient;", serviceName) + .write(""); + + // Invoke method + writer.openBlock(" static OutcomeType Invoke(ClientType& client, const RequestType& request)\n {", " }", () -> { + writer.write(" return client.$L(request);", opName); + }); + + writer.write(""); + + // HasMoreResults method + writer.openBlock(" static bool HasMoreResults(const ResultType& result)\n {", " }", () -> { + if (trait.getOutputToken().isPresent()) { + String outToken = trait.getOutputToken().get(); + if (outToken.toLowerCase().contains("marker") || outToken.toLowerCase().contains("number")) { + writer.write(" return result.Get$L() != 0;", capitalize(outToken)); + } else { + writer.write(" return !result.Get$L().empty();", capitalize(outToken)); + } + } else { + // TODO: Check how legacy C2J code generation handles service-level pagination + // This is a temporary fix for AccessAnalyzer which uses service-level pagination config. + // We should investigate how the legacy generator in C2jModelToGeneratorModelTransformer + // or other legacy components handle service-level vs operation-level pagination traits + // and implement a more comprehensive solution that matches that behavior. + String serviceLevelOutputToken = getServiceLevelOutputToken(); + if (serviceLevelOutputToken != null) { + if (serviceLevelOutputToken.toLowerCase().contains("marker") || serviceLevelOutputToken.toLowerCase().contains("number")) { + writer.write(" return result.Get$L() != 0;", capitalize(serviceLevelOutputToken)); + } else { + writer.write(" return !result.Get$L().empty();", capitalize(serviceLevelOutputToken)); + } + } else { + writer.write(" return result.GetIsTruncated();"); + } + } + }); + + writer.write(""); + + // SetNextRequest method + writer.openBlock(" static void SetNextRequest(const ResultType& result, RequestType& request)\n {", " }", () -> { + String inToken = null; + String outToken = null; + + if (trait.getInputToken().isPresent() && trait.getOutputToken().isPresent()) { + inToken = trait.getInputToken().get(); + outToken = trait.getOutputToken().get(); + } else { + // TODO: Check how legacy C2J code generation handles service-level pagination + // This is a temporary fix for AccessAnalyzer which uses service-level pagination config. + // We should investigate how the legacy generator in C2jModelToGeneratorModelTransformer + // or other legacy components handle service-level vs operation-level pagination traits + // and implement a more comprehensive solution that matches that behavior. + String serviceLevelInputToken = getServiceLevelInputToken(); + String serviceLevelOutputToken = getServiceLevelOutputToken(); + if (serviceLevelInputToken != null && serviceLevelOutputToken != null) { + inToken = serviceLevelInputToken; + outToken = serviceLevelOutputToken; + } + } + + if (inToken != null && outToken != null) { + writer.write(" request.Set$L(result.Get$L());", capitalize(inToken), capitalize(outToken)); + } else { + // TODO: Check AWS SDK C++ standard for handling null pagination tokens + // Should we throw an exception, log a warning, or silently ignore? + // Verify behavior with other AWS SDK implementations. + writer.write(" (void)result; (void)request; // Unused parameters"); + } + }); + }); + + writer.write("") + .writeNamespaceClose("Pagination") + .writeNamespaceClose(serviceName) + .writeNamespaceClose("Aws"); + } + + // Replicate C2J renameShape conflict detection logic + // TODO: This conflict detection logic may need to be moved to a shared utility class, its also very complicated and can be refactor + // to avoid duplication between C2J and Smithy generators and ensure consistency. + // Consider reusing the already implemented ShapeUtil class for shape name operations. + private String getResultSuffix(String opName) { + // Check if this service uses EC2 protocol which renames Result to Response + if (isEc2Protocol()) { + return "Response"; + } + + // For now, use the simple approach that works: + // If the actual generated file exists, use Result; otherwise use SdkResult + + // Check for known SdkResult cases (where data model conflicts exist) + Set allShapes = context.getModel().toSet(); + boolean hasDataModelConflict = allShapes.stream() + .anyMatch(shape -> { + String shapeName = shape.getId().getName(); + if (shapeName.equals(opName + "Result")) { + // Found a shape with the same name - check if it's a data model + if (shape instanceof StructureShape) { + StructureShape structShape = (StructureShape) shape; + // If it doesn't have NextToken/nextToken, it's likely a data model + Set memberNames = structShape.getAllMembers().keySet(); + // TODO: Sanitize member names for other edge cases (e.g., different casing, underscores, etc.) + boolean hasNextToken = memberNames.contains("NextToken") || memberNames.contains("nextToken"); + return !hasNextToken; + } + } + return false; + }); + + return hasDataModelConflict ? "SdkResult" : "Result"; + } + + // TODO: Delete this method if it's not used - replaced by simpler conflict detection in getResultSuffix + // Replicate the precise conflict detection logic from C2jModelToGeneratorModelTransformer + private boolean hasNamingConflict(String candidateName, String opName) { + // Get all shapes in the model to check for conflicts + Set allShapes = context.getModel().toSet(); + + for (Shape shape : allShapes) { + String shapeName = shape.getId().getName(); + + // Direct exact name conflict - this is the main case + if (candidateName.equals(shapeName)) { + // If this is a structure, check if it's already a suitable operation result + if (shape instanceof StructureShape) { + StructureShape structShape = (StructureShape) shape; + // If the existing shape has pagination tokens, it's already an operation result - no conflict + // Check for various pagination token field names + boolean hasNextToken = structShape.getAllMembers().keySet().stream() + .anyMatch(memberName -> memberName.toLowerCase().contains("nexttoken") || + memberName.toLowerCase().contains("token")); + + if (hasNextToken) { + // This is already an operation result shape, no conflict + return false; + } + + // If it doesn't have pagination tokens, it's a data model - conflict! + return true; + } + return true; + } + } + + return false; + } + + // Remove the hardcoded method - no longer needed + // private boolean isKnownConflictingOperation(String opName) { ... } + + private boolean isEc2Protocol() { + // EC2 protocol services rename all Result shapes to Response + // This matches the logic in Ec2CppClientGenerator.legacyPatchEc2Model + return "ec2".equals(c2jServiceName); + } + + + // TODO: These service-level pagination helper methods are a temporary solution. + // We should investigate how the legacy C2J code generation system handles the precedence + // between operation-level and service-level pagination traits, and implement a solution + // that matches that behavior. This may involve: + // 1. Checking how C2jModelToGeneratorModelTransformer processes pagination + // 2. Understanding the inheritance model for pagination traits + // 3. Implementing proper trait resolution that matches legacy behavior + private String getServiceLevelInputToken() { + // Check if service has service-level pagination configuration + return service.getTrait(software.amazon.smithy.model.traits.PaginatedTrait.class) + .map(trait -> trait.getInputToken().orElse(null)) + .orElse(null); + } + + private String getServiceLevelOutputToken() { + // Check if service has service-level pagination configuration + return service.getTrait(software.amazon.smithy.model.traits.PaginatedTrait.class) + .map(trait -> trait.getOutputToken().orElse(null)) + .orElse(null); + } + + private String capitalize(String str) { + return str.substring(0, 1).toUpperCase() + str.substring(1); + } +} diff --git a/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin new file mode 100644 index 00000000000..f4e06486044 --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin @@ -0,0 +1 @@ +com.amazonaws.util.awsclientsmithygenerator.generators.PaginationCodegenPlugin diff --git a/tools/code-generation/smithy/codegen/cpp-pagination/build.gradle.kts b/tools/code-generation/smithy/codegen/cpp-pagination/build.gradle.kts new file mode 100644 index 00000000000..e6fc1e2a689 --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination/build.gradle.kts @@ -0,0 +1,87 @@ +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.aws.traits.ServiceTrait +import org.gradle.api.logging.Logging +import kotlin.streams.toList + +val logger = Logging.getLogger("MyLogger") + +plugins { + id("java-library") + id("software.amazon.smithy.gradle.smithy-base").version("1.3.0") +} + +repositories { + mavenLocal() + mavenCentral() +} + +buildscript { + dependencies { + classpath(codegen.model) + classpath(codegen.aws.traits) + classpath(codegen.rules.engine) + } +} + +dependencies { + implementation(project(":cpp-pagination-codegen")) + implementation(codegen.aws.traits) + implementation(codegen.aws.cloudformation.traits) + implementation(codegen.aws.iam.traits) + implementation(codegen.aws.endpoints) + implementation(codegen.smoke.test.traits) + implementation(codegen.aws.smoke.test.model) + implementation(codegen.waiters) +} + +tasks.jar { + enabled = false +} + +tasks.register("generate-smithy-build") { + doLast { + val projectionsBuilder = Node.objectNodeBuilder() + val models = project.file("../../api-descriptions") + val filteredServices: String = project.findProperty("servicesFilter")?.toString() ?: "" + val filteredServiceList = filteredServices.split(",").map { it.trim() }.filter { it.isNotEmpty() } + val c2jMapStr: String = project.findProperty("c2jMap")?.toString() ?: "" + + fileTree(models).filter { it.isFile }.files.forEach eachFile@{ file -> + val model = Model.assembler() + .addImport(file.absolutePath) + // Grab the result directly rather than worrying about checking for errors via unwrap. + // All we care about here is the service shape, any unchecked errors will be exposed + // as part of the actual build task done by the smithy gradle plugin. + .assemble().result.get() + val services = model.shapes(ServiceShape::class.java).sorted().toList() + if (services.size != 1) return@eachFile + + val service = services[0] + val serviceTrait = service.getTrait(ServiceTrait::class.java).get() + val sdkId = serviceTrait.sdkId.replace(" ", "-").replace("_", "-").lowercase() + + if (filteredServiceList.isNotEmpty() && sdkId !in filteredServiceList) return@eachFile + + val projectionContents = Node.objectNodeBuilder() + .withMember("imports", Node.fromStrings("${models.absolutePath}${File.separator}${file.name}")) + .withMember("plugins", Node.objectNode() + .withMember("cpp-codegen-pagination-plugin", Node.objectNodeBuilder() + .withMember("c2jMap", Node.from(c2jMapStr)) + .build())) + .build() + + projectionsBuilder.withMember("$sdkId.${service.version.lowercase()}", projectionContents) + } + + val outputDirectoryArg = project.findProperty("outputDirectory")?.toString() ?: "output" + file("smithy-build.json").writeText(Node.prettyPrintJson(Node.objectNodeBuilder() + .withMember("version", "1.0") + .withMember("projections", projectionsBuilder.build()) + .withMember("outputDirectory", outputDirectoryArg) + .build())) + } +} + +tasks["build"].dependsOn(tasks["generate-smithy-build"]) diff --git a/tools/code-generation/smithy/codegen/cpp-pagination/smithy-build.json b/tools/code-generation/smithy/codegen/cpp-pagination/smithy-build.json new file mode 100644 index 00000000000..40328dc60a7 --- /dev/null +++ b/tools/code-generation/smithy/codegen/cpp-pagination/smithy-build.json @@ -0,0 +1,26 @@ +{ + "version": "1.0", + "projections": { + "s3.2006-03-01": { + "imports": [ + "/Users/cailinn/Desktop/workspace/aws-sdk-cpp/tools/code-generation/smithy/api-descriptions/s3.json" + ], + "plugins": { + "cpp-codegen-pagination-plugin": { + "c2jMap": "{\"api-gateway\": \"apigateway\", \"application-auto-scaling\": \"application-autoscaling\", \"app-mesh\": \"appmesh\", \"auto-scaling\": \"autoscaling\", \"auto-scaling-plans\": \"autoscaling-plans\", \"cloudhsm-v2\": \"cloudhsmv2\", \"cloudsearch-domain\": \"cloudsearchdomain\", \"config-service\": \"config\", \"cost-and-usage-report-service\": \"cur\", \"data-pipeline\": \"datapipeline\", \"device-farm\": \"devicefarm\", \"direct-connect\": \"directconnect\", \"dynamodb-streams\": \"dynamodbstreams\", \"elastic-beanstalk\": \"elasticbeanstalk\", \"elastic-load-balancing\": \"elasticloadbalancing\", \"elastic-load-balancing-v2\": \"elasticloadbalancingv2\", \"global-accelerator\": \"globalaccelerator\", \"iot-1click-devices-service\": \"iot1click-devices\", \"iot-1click-projects\": \"iot1click-projects\", \"iot-data-plane\": \"iot-data\", \"iot-events-data\": \"iotevents-data\", \"iot-events\": \"iotevents\", \"iot-jobs-data-plane\": \"iot-jobs-data\", \"iot-wireless\": \"iotwireless\", \"kinesis-analytics\": \"kinesisanalytics\", \"kinesis-analytics-v2\": \"kinesisanalyticsv2\", \"kinesis-video\": \"kinesisvideo\", \"lex-models-v2\": \"lexv2-models\", \"lex-runtime-service\": \"lex\", \"lex-runtime-v2\": \"lexv2-runtime\", \"machine-learning\": \"machinelearning\", \"marketplace-commerce-analytics\": \"marketplacecommerceanalytics\", \"marketplace-entitlement-service\": \"marketplace-entitlement\", \"marketplace-metering\": \"meteringmarketplace\", \"migration-hub\": \"AWSMigrationHub\", \"mturk\": \"mturk-requester\", \"pinpoint-sms-voice\": \"sms-voice\", \"resource-groups-tagging-api\": \"resourcegroupstaggingapi\", \"route-53-domains\": \"route53domains\", \"route-53\": \"route53\", \"s3-control\": \"s3control\", \"sagemaker-runtime\": \"sagemaker-runtime\", \"secrets-manager\": \"secretsmanager\", \"serverlessapplicationrepository\": \"serverlessrepo\", \"service-catalog-appregistry\": \"servicecatalog-appregistry\", \"service-catalog\": \"servicecatalog\", \"transfer\": \"awstransfer\", \"cloudwatch\": \"monitoring\", \"cloudwatch-events\": \"events\", \"storage-gateway\": \"storagegateway\", \"efs\": \"elasticfilesystem\", \"emr\": \"elasticmapreduce\", \"ses\": \"email\", \"cognito-identity-provider\": \"cognito-idp\", \"cost-explorer\": \"ce\", \"application-discovery-service\": \"discovery\", \"database-migration-service\": \"dms\", \"sfn\": \"states\", \"lex-model-building-service\": \"lex-models\", \"cloudwatch-logs\": \"logs\", \"directory-service\": \"ds\", \"elasticsearch-service\": \"es\", \"importexport\": \"importexport\", \"sdb\": \"sdb\", \"transcribe-streaming\": \"transcribestreaming\"}" + } + } + }, + "dynamodb.2012-08-10": { + "imports": [ + "/Users/cailinn/Desktop/workspace/aws-sdk-cpp/tools/code-generation/smithy/api-descriptions/dynamodb.json" + ], + "plugins": { + "cpp-codegen-pagination-plugin": { + "c2jMap": "{\"api-gateway\": \"apigateway\", \"application-auto-scaling\": \"application-autoscaling\", \"app-mesh\": \"appmesh\", \"auto-scaling\": \"autoscaling\", \"auto-scaling-plans\": \"autoscaling-plans\", \"cloudhsm-v2\": \"cloudhsmv2\", \"cloudsearch-domain\": \"cloudsearchdomain\", \"config-service\": \"config\", \"cost-and-usage-report-service\": \"cur\", \"data-pipeline\": \"datapipeline\", \"device-farm\": \"devicefarm\", \"direct-connect\": \"directconnect\", \"dynamodb-streams\": \"dynamodbstreams\", \"elastic-beanstalk\": \"elasticbeanstalk\", \"elastic-load-balancing\": \"elasticloadbalancing\", \"elastic-load-balancing-v2\": \"elasticloadbalancingv2\", \"global-accelerator\": \"globalaccelerator\", \"iot-1click-devices-service\": \"iot1click-devices\", \"iot-1click-projects\": \"iot1click-projects\", \"iot-data-plane\": \"iot-data\", \"iot-events-data\": \"iotevents-data\", \"iot-events\": \"iotevents\", \"iot-jobs-data-plane\": \"iot-jobs-data\", \"iot-wireless\": \"iotwireless\", \"kinesis-analytics\": \"kinesisanalytics\", \"kinesis-analytics-v2\": \"kinesisanalyticsv2\", \"kinesis-video\": \"kinesisvideo\", \"lex-models-v2\": \"lexv2-models\", \"lex-runtime-service\": \"lex\", \"lex-runtime-v2\": \"lexv2-runtime\", \"machine-learning\": \"machinelearning\", \"marketplace-commerce-analytics\": \"marketplacecommerceanalytics\", \"marketplace-entitlement-service\": \"marketplace-entitlement\", \"marketplace-metering\": \"meteringmarketplace\", \"migration-hub\": \"AWSMigrationHub\", \"mturk\": \"mturk-requester\", \"pinpoint-sms-voice\": \"sms-voice\", \"resource-groups-tagging-api\": \"resourcegroupstaggingapi\", \"route-53-domains\": \"route53domains\", \"route-53\": \"route53\", \"s3-control\": \"s3control\", \"sagemaker-runtime\": \"sagemaker-runtime\", \"secrets-manager\": \"secretsmanager\", \"serverlessapplicationrepository\": \"serverlessrepo\", \"service-catalog-appregistry\": \"servicecatalog-appregistry\", \"service-catalog\": \"servicecatalog\", \"transfer\": \"awstransfer\", \"cloudwatch\": \"monitoring\", \"cloudwatch-events\": \"events\", \"storage-gateway\": \"storagegateway\", \"efs\": \"elasticfilesystem\", \"emr\": \"elasticmapreduce\", \"ses\": \"email\", \"cognito-identity-provider\": \"cognito-idp\", \"cost-explorer\": \"ce\", \"application-discovery-service\": \"discovery\", \"database-migration-service\": \"dms\", \"sfn\": \"states\", \"lex-model-building-service\": \"lex-models\", \"cloudwatch-logs\": \"logs\", \"directory-service\": \"ds\", \"elasticsearch-service\": \"es\", \"importexport\": \"importexport\", \"sdb\": \"sdb\", \"transcribe-streaming\": \"transcribestreaming\"}" + } + } + } + }, + "outputDirectory": "output" +} \ No newline at end of file diff --git a/tools/code-generation/smithy/codegen/settings.gradle.kts b/tools/code-generation/smithy/codegen/settings.gradle.kts index bc2b770a273..e14d59a763f 100644 --- a/tools/code-generation/smithy/codegen/settings.gradle.kts +++ b/tools/code-generation/smithy/codegen/settings.gradle.kts @@ -2,6 +2,8 @@ rootProject.name = "smithy_code_generation" include("cpp-smoke-tests") include("cpp-smoke-tests-codegen") +include("cpp-pagination-codegen") +include("cpp-pagination") pluginManagement { repositories { diff --git a/tools/code-generation/smithy/codegen/smithy2c2j_service_map.json b/tools/code-generation/smithy/codegen/smithy2c2j_service_map.json index d4865618ff4..1ff0190e0ac 100644 --- a/tools/code-generation/smithy/codegen/smithy2c2j_service_map.json +++ b/tools/code-generation/smithy/codegen/smithy2c2j_service_map.json @@ -60,7 +60,7 @@ "lex-model-building-service":"lex-models", "cloudwatch-logs":"logs", "directory-service":"ds", - "elasticsearch-service ":"es", + "elasticsearch-service":"es", "importexport":"importexport", "sdb":"sdb", "transcribe-streaming":"transcribestreaming" diff --git a/tools/scripts/codegen/legacy_c2j_cpp_gen.py b/tools/scripts/codegen/legacy_c2j_cpp_gen.py index 7e1df6be35f..ee77eaa90c4 100644 --- a/tools/scripts/codegen/legacy_c2j_cpp_gen.py +++ b/tools/scripts/codegen/legacy_c2j_cpp_gen.py @@ -16,7 +16,6 @@ from concurrent.futures import ProcessPoolExecutor, wait, FIRST_COMPLETED, ALL_COMPLETED from pathlib import Path -from codegen.format_util import format_directories from codegen.include_tests_util import IncludeTestsUtil from codegen.model_utils import ServiceModel @@ -149,12 +148,6 @@ def generate(self, executor: ProcessPoolExecutor, max_workers: int, args: dict) print(f"Code generation done, (re)generated {len(done)} packages.") # Including defaults and partitions - # Format generated client code - generated_clients = [service for service in self.c2j_models.keys()] - if generated_clients: - client_dirs = [f"{self.output_location}/src/aws-cpp-sdk-{client}" for client in generated_clients] - format_directories(client_dirs) - return 0 def _init_common_java_cli(self, diff --git a/tools/scripts/codegen/pagination_gen.py b/tools/scripts/codegen/pagination_gen.py new file mode 100644 index 00000000000..5a70e731971 --- /dev/null +++ b/tools/scripts/codegen/pagination_gen.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0. + +""" +Wrapper for Smithy pagination generator +""" +import json +import os +import shutil +import subprocess +from typing import List + +SMITHY_GENERATOR_LOCATION = "tools/code-generation/smithy/codegen" +SMITHY_TO_C2J_MAP_FILE = "tools/code-generation/smithy/codegen/smithy2c2j_service_map.json" + + +class PaginationGen(object): + """Wrapper for Smithy pagination generator for C++ SDK""" + + def __init__(self, debug: bool, **kwargs): + self.debug = debug + 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()} + + def generate(self, clients_to_build: set): + """Generate pagination APIs for SDK clients""" + smithy_services = [self.c2j_smithy_data.get(service, service) for service in clients_to_build] + if self.debug: + print(f"Generating pagination for: {','.join(smithy_services)}") + + if self._generate_pagination(smithy_services, json.dumps(self.smithy_c2j_data)): + target_dir = os.path.abspath("generated/src") + self._copy_cpp_codegen_contents( + os.path.abspath("tools/code-generation/smithy/codegen"), + "cpp-codegen-pagination-plugin", + target_dir + ) + return 0 + return -1 + + def _generate_pagination(self, smithy_services: List[str], smithy_c2j_data: str): + smithy_codegen_command = [ + "./gradlew", + ":cpp-pagination:build", + "-PservicesFilter=" + ",".join(smithy_services), + "-Pc2jMap=" + smithy_c2j_data + ] + + try: + if self.debug: + print(f"RUNNING: {' '.join(smithy_codegen_command)}\nFROM: {SMITHY_GENERATOR_LOCATION}") + + process = subprocess.run( + smithy_codegen_command, + timeout=6*60, + check=True, + capture_output=True, + text=True, + cwd=SMITHY_GENERATOR_LOCATION + ) + print("Pagination codegen successful!") + if self.debug: + print(process.stdout) + print(f"Pagination generation done, (re)generated {len(smithy_services)} package(s).") + return True + + except subprocess.CalledProcessError as e: + print(f"Command failed: {e.returncode}\nError: {e.stderr}") + return False + + def _copy_cpp_codegen_contents(self, top_level_dir: str, plugin_name: str, target_dir: str): + for root, dirs, files in os.walk(top_level_dir): + if plugin_name in dirs: + source_dir = os.path.join(root, plugin_name) + + # Extract service name from the projection directory + projection_dir = os.path.basename(os.path.dirname(source_dir)) + smithy_service_name = projection_dir.split('.')[0] # e.g., "api-gateway.2017-11-27" -> "api-gateway" + + # Map to c2j service name + c2j_service_name = self.smithy_c2j_data.get(smithy_service_name, smithy_service_name) + + service_target_dir = os.path.join(target_dir, f"aws-cpp-sdk-{c2j_service_name}") + os.makedirs(service_target_dir, exist_ok=True) + + for item in os.listdir(source_dir): + source_item = os.path.join(source_dir, item) + + # Handle test directories separately - move to generated/tests + if item == "generated" and os.path.isdir(source_item): + generated_source = os.path.join(source_item, "tests") + if os.path.exists(generated_source): + test_target_dir = os.path.abspath("generated/tests") + os.makedirs(test_target_dir, exist_ok=True) + shutil.copytree(generated_source, test_target_dir, dirs_exist_ok=True) + if self.debug: + print(f"Copied tests from '{generated_source}' to '{test_target_dir}'") + continue + + target_item = os.path.join(service_target_dir, item) + if os.path.isdir(source_item): + shutil.copytree(source_item, target_item, dirs_exist_ok=True) + else: + shutil.copy2(source_item, target_item) + + if self.debug: + print(f"Copied from '{source_dir}' to '{service_target_dir}'") + + # Cleanup output directory + output_dir = os.path.join(top_level_dir, "cpp-pagination/output") + if os.path.exists(output_dir): + shutil.rmtree(output_dir) + if self.debug: + print(f"Cleaned up '{output_dir}'") + diff --git a/tools/scripts/run_code_generation.py b/tools/scripts/run_code_generation.py index 014ee39ce3d..b88ee081179 100644 --- a/tools/scripts/run_code_generation.py +++ b/tools/scripts/run_code_generation.py @@ -13,6 +13,8 @@ from codegen.model_utils import ModelUtils from codegen.protocol_tests_gen import ProtocolTestsGen from codegen.smoke_tests_gen import SmokeTestsGen +from codegen.pagination_gen import PaginationGen +from codegen.format_util import format_directories def parse_arguments() -> dict: @@ -154,12 +156,26 @@ def main(): # generate code using smithy for all discoverable clients # clients_to_build check is present because user can generate only defaults or partitions or protocol-tests clients_to_build = model_utils.get_clients_to_build() + + if clients_to_build: + pagination_gen = PaginationGen(args["debug"]) + if pagination_gen.generate(clients_to_build) != 0: + print("ERROR: Failed to generate pagination!") + return -1 + if args["generate_smoke_tests"] and clients_to_build: smoke_tests_gen = SmokeTestsGen(args["debug"]) if smoke_tests_gen.generate(clients_to_build) != 0: print("ERROR: Failed to generate smoke test(s)!") return -1 + # Format all generated C++ code at the end + if clients_to_build: + client_dirs = [f"{args['output_location']}/src/aws-cpp-sdk-{client}" for client in clients_to_build] + existing_dirs = [d for d in client_dirs if os.path.exists(d)] + if existing_dirs: + format_directories(existing_dirs) + return 0