From 3d097d0c370c35398bbba9f3723d3c1bd231e37a Mon Sep 17 00:00:00 2001 From: George Fu Date: Tue, 25 Nov 2025 14:29:51 -0500 Subject: [PATCH] test: generate index test for clients --- README.md | 1 + private/my-local-model-schema/package.json | 1 + .../test/index-objects.spec.mjs | 17 ++ .../my-local-model-schema/test/index-types.ts | 15 ++ private/smithy-rpcv2-cbor-schema/package.json | 1 + .../test/index-objects.spec.mjs | 48 +++++ .../test/index-types.ts | 42 +++++ private/smithy-rpcv2-cbor/package.json | 1 + .../test/index-objects.spec.mjs | 48 +++++ private/smithy-rpcv2-cbor/test/index-types.ts | 42 +++++ .../typescript/codegen/CodegenUtils.java | 1 + .../codegen/DirectedTypeScriptCodegen.java | 19 ++ .../PackageApiValidationGenerator.java | 169 ++++++++++++++++++ .../codegen/PackageJsonGenerator.java | 7 + .../codegen/TypeScriptSettings.java | 32 +++- .../codegen/knowledge/ServiceClosure.java | 137 ++++++++++++++ .../typescript/codegen/base-package.json | 1 + .../smithy-build.json | 9 +- 18 files changed, 582 insertions(+), 9 deletions(-) create mode 100644 private/my-local-model-schema/test/index-objects.spec.mjs create mode 100644 private/my-local-model-schema/test/index-types.ts create mode 100644 private/smithy-rpcv2-cbor-schema/test/index-objects.spec.mjs create mode 100644 private/smithy-rpcv2-cbor-schema/test/index-types.ts create mode 100644 private/smithy-rpcv2-cbor/test/index-objects.spec.mjs create mode 100644 private/smithy-rpcv2-cbor/test/index-types.ts create mode 100644 smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/PackageApiValidationGenerator.java create mode 100644 smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/knowledge/ServiceClosure.java diff --git a/README.md b/README.md index 76415b46485..7ba8d7fe93d 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,7 @@ By default, the Smithy TypeScript code generators provide the code generation fr | `useLegacyAuth` | No | **NOT RECOMMENDED, AVAILABLE ONLY FOR BACKWARD COMPATIBILITY CONCERNS.** Flag that enables using legacy auth. When in doubt, use the default identity and auth behavior (not configuring `useLegacyAuth`) as the golden path. | | `serviceProtocolPriority` | No | Map of service `ShapeId` strings to lists of protocol `ShapeId` strings. Used to override protocol selection behavior. | | `defaultProtocolPriority` | No | List of protocol `ShapeId` strings. Lower precedence than `serviceProtocolPriority` but applies to all services. | +| `generateIndexTests` | No | Default=`false`. Whether to generate a set of tests that does a basic validation of the export surface of the generated client package. The tests can be run with the script `test:index` in the generated package. | #### `typescript-client-codegen` plugin artifacts diff --git a/private/my-local-model-schema/package.json b/private/my-local-model-schema/package.json index 3247678475b..efe78cc81a4 100644 --- a/private/my-local-model-schema/package.json +++ b/private/my-local-model-schema/package.json @@ -8,6 +8,7 @@ "build:es": "tsc -p tsconfig.es.json", "build:types": "tsc -p tsconfig.types.json", "build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4", + "test:index": "tsc --noEmit ./test/index-types.ts && node ./test/index-objects.spec.mjs", "clean": "rimraf ./dist-* && rimraf *.tsbuildinfo || exit 0", "prepack": "yarn run clean && yarn run build" }, diff --git a/private/my-local-model-schema/test/index-objects.spec.mjs b/private/my-local-model-schema/test/index-objects.spec.mjs new file mode 100644 index 00000000000..5f97cf676a2 --- /dev/null +++ b/private/my-local-model-schema/test/index-objects.spec.mjs @@ -0,0 +1,17 @@ +import { + GetNumbersCommand, + TradeEventStreamCommand, + XYZService, + XYZServiceClient, + XYZServiceServiceException, +} from "../dist-cjs/index.js"; +import assert from "node:assert"; +// clients +assert(typeof XYZServiceClient === "function"); +assert(typeof XYZService === "function"); +// commands +assert(typeof GetNumbersCommand === "function"); +assert(typeof TradeEventStreamCommand === "function"); +// errors +assert(XYZServiceServiceException.prototype instanceof Error); +console.log(`XYZService index test passed.`); diff --git a/private/my-local-model-schema/test/index-types.ts b/private/my-local-model-schema/test/index-types.ts new file mode 100644 index 00000000000..fb1c8cf64b6 --- /dev/null +++ b/private/my-local-model-schema/test/index-types.ts @@ -0,0 +1,15 @@ +// smithy-typescript generated code +export type { + XYZServiceClient, + XYZService, + GetNumbersCommand, + TradeEventStreamCommand, + Alpha, + GetNumbersRequest, + GetNumbersResponse, + TradeEvents, + TradeEventStreamRequest, + TradeEventStreamResponse, + Unit, + XYZServiceServiceException, +} from "../dist-types/index.d"; diff --git a/private/smithy-rpcv2-cbor-schema/package.json b/private/smithy-rpcv2-cbor-schema/package.json index a9f0df152dd..4c79bf25101 100644 --- a/private/smithy-rpcv2-cbor-schema/package.json +++ b/private/smithy-rpcv2-cbor-schema/package.json @@ -8,6 +8,7 @@ "build:es": "tsc -p tsconfig.es.json", "build:types": "tsc -p tsconfig.types.json", "build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4", + "test:index": "tsc --noEmit ./test/index-types.ts && node ./test/index-objects.spec.mjs", "clean": "rimraf ./dist-* && rimraf *.tsbuildinfo || exit 0", "prepack": "yarn run clean && yarn run build", "test": "yarn g:vitest run --passWithNoTests" diff --git a/private/smithy-rpcv2-cbor-schema/test/index-objects.spec.mjs b/private/smithy-rpcv2-cbor-schema/test/index-objects.spec.mjs new file mode 100644 index 00000000000..a2728f1470e --- /dev/null +++ b/private/smithy-rpcv2-cbor-schema/test/index-objects.spec.mjs @@ -0,0 +1,48 @@ +import { + EmptyInputOutputCommand, + Float16Command, + FooEnum, + FractionalSecondsCommand, + GreetingWithErrorsCommand, + IntegerEnum, + NoInputOutputCommand, + OperationWithDefaultsCommand, + OptionalInputOutputCommand, + RecursiveShapesCommand, + RpcV2CborDenseMapsCommand, + RpcV2CborListsCommand, + RpcV2CborSparseMapsCommand, + RpcV2Protocol, + RpcV2ProtocolClient, + RpcV2ProtocolServiceException, + SimpleScalarPropertiesCommand, + SparseNullsOperationCommand, + TestEnum, + TestIntEnum, +} from "../dist-cjs/index.js"; +import assert from "node:assert"; +// clients +assert(typeof RpcV2ProtocolClient === "function"); +assert(typeof RpcV2Protocol === "function"); +// commands +assert(typeof EmptyInputOutputCommand === "function"); +assert(typeof Float16Command === "function"); +assert(typeof FractionalSecondsCommand === "function"); +assert(typeof GreetingWithErrorsCommand === "function"); +assert(typeof NoInputOutputCommand === "function"); +assert(typeof OperationWithDefaultsCommand === "function"); +assert(typeof OptionalInputOutputCommand === "function"); +assert(typeof RecursiveShapesCommand === "function"); +assert(typeof RpcV2CborDenseMapsCommand === "function"); +assert(typeof RpcV2CborListsCommand === "function"); +assert(typeof RpcV2CborSparseMapsCommand === "function"); +assert(typeof SimpleScalarPropertiesCommand === "function"); +assert(typeof SparseNullsOperationCommand === "function"); +// enums +assert(typeof TestEnum === "object"); +assert(typeof TestIntEnum === "object"); +assert(typeof FooEnum === "object"); +assert(typeof IntegerEnum === "object"); +// errors +assert(RpcV2ProtocolServiceException.prototype instanceof Error); +console.log(`RpcV2Protocol index test passed.`); diff --git a/private/smithy-rpcv2-cbor-schema/test/index-types.ts b/private/smithy-rpcv2-cbor-schema/test/index-types.ts new file mode 100644 index 00000000000..eec35b87e24 --- /dev/null +++ b/private/smithy-rpcv2-cbor-schema/test/index-types.ts @@ -0,0 +1,42 @@ +// smithy-typescript generated code +export type { + RpcV2ProtocolClient, + RpcV2Protocol, + EmptyInputOutputCommand, + Float16Command, + FractionalSecondsCommand, + GreetingWithErrorsCommand, + NoInputOutputCommand, + OperationWithDefaultsCommand, + OptionalInputOutputCommand, + RecursiveShapesCommand, + RpcV2CborDenseMapsCommand, + RpcV2CborListsCommand, + RpcV2CborSparseMapsCommand, + SimpleScalarPropertiesCommand, + SparseNullsOperationCommand, + TestEnum, + TestIntEnum, + FooEnum, + IntegerEnum, + ClientOptionalDefaults, + Defaults, + EmptyStructure, + Float16Output, + FractionalSecondsOutput, + GreetingWithErrorsOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + RecursiveShapesInputOutput, + RecursiveShapesInputOutputNested1, + RecursiveShapesInputOutputNested2, + RpcV2CborDenseMapsInputOutput, + RpcV2CborListInputOutput, + RpcV2CborSparseMapsInputOutput, + SimpleScalarStructure, + SimpleStructure, + SparseNullsOperationInputOutput, + StructureListMember, + GreetingStruct, + RpcV2ProtocolServiceException, +} from "../dist-types/index.d"; diff --git a/private/smithy-rpcv2-cbor/package.json b/private/smithy-rpcv2-cbor/package.json index 5e467ae21b7..44ab873329a 100644 --- a/private/smithy-rpcv2-cbor/package.json +++ b/private/smithy-rpcv2-cbor/package.json @@ -8,6 +8,7 @@ "build:es": "tsc -p tsconfig.es.json", "build:types": "tsc -p tsconfig.types.json", "build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4", + "test:index": "tsc --noEmit ./test/index-types.ts && node ./test/index-objects.spec.mjs", "clean": "rimraf ./dist-* && rimraf *.tsbuildinfo || exit 0", "prepack": "yarn run clean && yarn run build", "merged": "echo \"this is merged from user configuration.\"", diff --git a/private/smithy-rpcv2-cbor/test/index-objects.spec.mjs b/private/smithy-rpcv2-cbor/test/index-objects.spec.mjs new file mode 100644 index 00000000000..a2728f1470e --- /dev/null +++ b/private/smithy-rpcv2-cbor/test/index-objects.spec.mjs @@ -0,0 +1,48 @@ +import { + EmptyInputOutputCommand, + Float16Command, + FooEnum, + FractionalSecondsCommand, + GreetingWithErrorsCommand, + IntegerEnum, + NoInputOutputCommand, + OperationWithDefaultsCommand, + OptionalInputOutputCommand, + RecursiveShapesCommand, + RpcV2CborDenseMapsCommand, + RpcV2CborListsCommand, + RpcV2CborSparseMapsCommand, + RpcV2Protocol, + RpcV2ProtocolClient, + RpcV2ProtocolServiceException, + SimpleScalarPropertiesCommand, + SparseNullsOperationCommand, + TestEnum, + TestIntEnum, +} from "../dist-cjs/index.js"; +import assert from "node:assert"; +// clients +assert(typeof RpcV2ProtocolClient === "function"); +assert(typeof RpcV2Protocol === "function"); +// commands +assert(typeof EmptyInputOutputCommand === "function"); +assert(typeof Float16Command === "function"); +assert(typeof FractionalSecondsCommand === "function"); +assert(typeof GreetingWithErrorsCommand === "function"); +assert(typeof NoInputOutputCommand === "function"); +assert(typeof OperationWithDefaultsCommand === "function"); +assert(typeof OptionalInputOutputCommand === "function"); +assert(typeof RecursiveShapesCommand === "function"); +assert(typeof RpcV2CborDenseMapsCommand === "function"); +assert(typeof RpcV2CborListsCommand === "function"); +assert(typeof RpcV2CborSparseMapsCommand === "function"); +assert(typeof SimpleScalarPropertiesCommand === "function"); +assert(typeof SparseNullsOperationCommand === "function"); +// enums +assert(typeof TestEnum === "object"); +assert(typeof TestIntEnum === "object"); +assert(typeof FooEnum === "object"); +assert(typeof IntegerEnum === "object"); +// errors +assert(RpcV2ProtocolServiceException.prototype instanceof Error); +console.log(`RpcV2Protocol index test passed.`); diff --git a/private/smithy-rpcv2-cbor/test/index-types.ts b/private/smithy-rpcv2-cbor/test/index-types.ts new file mode 100644 index 00000000000..eec35b87e24 --- /dev/null +++ b/private/smithy-rpcv2-cbor/test/index-types.ts @@ -0,0 +1,42 @@ +// smithy-typescript generated code +export type { + RpcV2ProtocolClient, + RpcV2Protocol, + EmptyInputOutputCommand, + Float16Command, + FractionalSecondsCommand, + GreetingWithErrorsCommand, + NoInputOutputCommand, + OperationWithDefaultsCommand, + OptionalInputOutputCommand, + RecursiveShapesCommand, + RpcV2CborDenseMapsCommand, + RpcV2CborListsCommand, + RpcV2CborSparseMapsCommand, + SimpleScalarPropertiesCommand, + SparseNullsOperationCommand, + TestEnum, + TestIntEnum, + FooEnum, + IntegerEnum, + ClientOptionalDefaults, + Defaults, + EmptyStructure, + Float16Output, + FractionalSecondsOutput, + GreetingWithErrorsOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + RecursiveShapesInputOutput, + RecursiveShapesInputOutputNested1, + RecursiveShapesInputOutputNested2, + RpcV2CborDenseMapsInputOutput, + RpcV2CborListInputOutput, + RpcV2CborSparseMapsInputOutput, + SimpleScalarStructure, + SimpleStructure, + SparseNullsOperationInputOutput, + StructureListMember, + GreetingStruct, + RpcV2ProtocolServiceException, +} from "../dist-types/index.d"; diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenUtils.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenUtils.java index b9447fa5799..73a642fabac 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenUtils.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenUtils.java @@ -41,6 +41,7 @@ public final class CodegenUtils { public static final String SOURCE_FOLDER = "src"; + public static final String TEST_FOLDER = "test"; private CodegenUtils() {} diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java index 58546db6bda..9cc60f16b5a 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java @@ -504,6 +504,25 @@ public void customizeBeforeIntegrations( ); }); + if (directive.settings().generateClient() && directive.settings().generateIndexTests()) { + writerFactory.accept(Paths.get(CodegenUtils.TEST_FOLDER, "index-types.ts").toString(), writer -> { + new PackageApiValidationGenerator( + writer, + directive.settings(), + directive.model(), + directive.symbolProvider() + ).writeTypeIndexTest(); + }); + writerFactory.accept(Paths.get(CodegenUtils.TEST_FOLDER, "index-objects.spec.mjs").toString(), writer -> { + new PackageApiValidationGenerator( + writer, + directive.settings(), + directive.model(), + directive.symbolProvider() + ).writeRuntimeIndexTest(); + }); + } + if (directive.settings().generateServerSdk()) { // Generate index for server IndexGenerator.writeServerIndex( diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/PackageApiValidationGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/PackageApiValidationGenerator.java new file mode 100644 index 00000000000..6a6ee544074 --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/PackageApiValidationGenerator.java @@ -0,0 +1,169 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.typescript.codegen; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Set; +import java.util.TreeSet; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.typescript.codegen.knowledge.ServiceClosure; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * This class generates a runnable pair of test files + * that demonstrates the exportable components of the generated client + * are accounted for. + */ +@SmithyInternalApi +public final class PackageApiValidationGenerator { + private final TypeScriptWriter writer; + private final TypeScriptSettings settings; + private final Model model; + private final SymbolProvider symbolProvider; + private final ServiceClosure serviceClosure; + + public PackageApiValidationGenerator( + TypeScriptWriter writer, + TypeScriptSettings settings, + Model model, + SymbolProvider symbolProvider + ) { + this.writer = writer; + this.settings = settings; + this.model = model; + this.symbolProvider = symbolProvider; + serviceClosure = ServiceClosure.of(model, settings.getService(model)); + } + + /** + * Code written by this method is types-only TypeScript. + */ + public void writeTypeIndexTest() { + writer.openBlock(""" + export type {""", + """ + } from "../dist-types/index.d\"""", + () -> { + // exportable types include: + + // the barebones client + String aggregateClientName = CodegenUtils.getServiceName(settings, model, symbolProvider); + writer.write("$L", aggregateClientName + "Client,"); + + // the aggregate client + writer.write(aggregateClientName + ","); + + // all commands + Set containedOperations = TopDownIndex.of(model).getContainedOperations( + settings.getService() + ); + for (OperationShape operation : containedOperations) { + writer.write("$L,", symbolProvider.toSymbol(operation).getName()); + } + + // enums + TreeSet enumShapes = serviceClosure.getEnums(); + for (Shape enumShape : enumShapes) { + writer.write("$L,", symbolProvider.toSymbol(enumShape).getName()); + } + + // structure & union types & modeled errors + TreeSet structuralShapes = serviceClosure.getStructuralNonErrorShapes(); + for (Shape structuralShape : structuralShapes) { + writer.write("$L,", symbolProvider.toSymbol(structuralShape).getName()); + } + + TreeSet errorShapes = serviceClosure.getErrorShapes(); + for (Shape errorShape : errorShapes) { + writer.write("$L,", symbolProvider.toSymbol(errorShape).getName()); + } + + // synthetic base exception + writer.write("$L,", aggregateClientName + "ServiceException"); + } + ); + } + + /** + * Code written by this method is pure JavaScript (CJS). + */ + public void writeRuntimeIndexTest() { + writer.write(""" + import assert from "node:assert";"""); + // runtime components include: + + Path cjsIndex = Paths.get("./dist-cjs/index.js"); + + // the barebones client + String aggregateClientName = CodegenUtils.getServiceName(settings, model, symbolProvider); + writer.addRelativeImport(aggregateClientName + "Client", null, cjsIndex); + writer.addRelativeImport(aggregateClientName, null, cjsIndex); + + // the aggregate client + writer.write("// clients"); + writer.write(""" + assert(typeof $L === "function")""", + aggregateClientName + "Client" + ); + writer.write(""" + assert(typeof $L === "function")""", + aggregateClientName + ); + + // all commands + writer.write("// commands"); + Set containedOperations = TopDownIndex.of(model).getContainedOperations( + settings.getService() + ); + for (OperationShape operation : containedOperations) { + Symbol operationSymbol = symbolProvider.toSymbol(operation); + writer.addRelativeImport(operationSymbol.getName(), null, cjsIndex); + writer.write(""" + assert(typeof $L === "function")""", + operationSymbol.getName() + ); + } + + // enums + TreeSet enumShapes = serviceClosure.getEnums(); + if (!enumShapes.isEmpty()) { + writer.write("// enums"); + } + for (Shape enumShape : enumShapes) { + Symbol enumSymbol = symbolProvider.toSymbol(enumShape); + writer.addRelativeImport(enumSymbol.getName(), null, cjsIndex); + writer.write(""" + assert(typeof $L === "object");""", + enumSymbol.getName() + ); + } + + String baseExceptionName = aggregateClientName + "ServiceException"; + + // modeled errors and synthetic base error + writer.write("// errors"); + TreeSet errors = serviceClosure.getErrorShapes(); + for (Shape error : errors) { + Symbol errorSymbol = symbolProvider.toSymbol(error); + writer.addRelativeImport(errorSymbol.getName(), null, cjsIndex); + writer.write( + "assert($L.prototype instanceof $L)", + errorSymbol.getName(), + baseExceptionName + ); + } + writer.addRelativeImport(baseExceptionName, null, cjsIndex); + writer.write("assert($L.prototype instanceof Error)", baseExceptionName); + + writer.write("console.log(`$L index test passed.`);", aggregateClientName); + } +} diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/PackageJsonGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/PackageJsonGenerator.java index 766aace2289..b73e5c7ce3b 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/PackageJsonGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/PackageJsonGenerator.java @@ -119,6 +119,13 @@ static void writePackageJson( node = node.withMember("private", true); } + if (!settings.generateIndexTests()) { + node = node.withMember( + "scripts", + node.getObjectMember("scripts").get().withoutMember("test:index") + ); + } + // Expand template parameters. String template = Node.prettyPrintJson(node); template = template.replace("${package}", settings.getPackageName()); diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java index bdcfbc87b7a..bd1c3e19b7a 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java @@ -67,6 +67,7 @@ public final class TypeScriptSettings { private static final String CREATE_DEFAULT_README = "createDefaultReadme"; private static final String USE_LEGACY_AUTH = "useLegacyAuth"; private static final String GENERATE_TYPEDOC = "generateTypeDoc"; + private static final String GENERATE_INDEX_TESTS = "generateIndexTests"; private static final String SERVICE_PROTOCOL_PRIORITY = "serviceProtocolPriority"; private static final String DEFAULT_PROTOCOL_PRIORITY = "defaultProtocolPriority"; @@ -90,6 +91,7 @@ public final class TypeScriptSettings { private ProtocolPriorityConfig protocolPriorityConfig = new ProtocolPriorityConfig(null, null); private String bigNumberMode = "native"; private boolean generateSchemas = false; + private boolean generateIndexTests = false; @Deprecated public static TypeScriptSettings from(Model model, ObjectNode config) { @@ -152,6 +154,10 @@ public static TypeScriptSettings from(Model model, ObjectNode config, ArtifactTy config.getBooleanMemberOrDefault("generateSchemas", false) ); + settings.setGenerateIndexTests( + config.getBooleanMemberOrDefault("generateIndexTests", false) + ); + return settings; } @@ -262,6 +268,14 @@ public boolean generateSchemas() { return generateSchemas; } + public void setGenerateIndexTests(boolean generateIndexTests) { + this.generateIndexTests = generateIndexTests; + } + + public boolean generateIndexTests() { + return generateIndexTests; + } + /** * Gets a chunk of custom properties to merge into the generated * package.json file. @@ -558,13 +572,19 @@ public void setProtocolPriority(ProtocolPriorityConfig protocolPriorityConfig) { */ public enum ArtifactType { CLIENT(SymbolVisitor::new, - Arrays.asList(PACKAGE, PACKAGE_DESCRIPTION, PACKAGE_JSON, PACKAGE_VERSION, PACKAGE_MANAGER, - SERVICE, PROTOCOL, PRIVATE, REQUIRED_MEMBER_MODE, - CREATE_DEFAULT_README, USE_LEGACY_AUTH, GENERATE_TYPEDOC)), + Arrays.asList( + PACKAGE, PACKAGE_DESCRIPTION, PACKAGE_JSON, PACKAGE_VERSION, PACKAGE_MANAGER, + SERVICE, PROTOCOL, PRIVATE, REQUIRED_MEMBER_MODE, + CREATE_DEFAULT_README, USE_LEGACY_AUTH, GENERATE_TYPEDOC, + GENERATE_INDEX_TESTS + )), SSDK((m, s) -> new ServerSymbolVisitor(m, new SymbolVisitor(m, s)), - Arrays.asList(PACKAGE, PACKAGE_DESCRIPTION, PACKAGE_JSON, PACKAGE_VERSION, PACKAGE_MANAGER, - SERVICE, PROTOCOL, PRIVATE, REQUIRED_MEMBER_MODE, - DISABLE_DEFAULT_VALIDATION, CREATE_DEFAULT_README, GENERATE_TYPEDOC)); + Arrays.asList( + PACKAGE, PACKAGE_DESCRIPTION, PACKAGE_JSON, PACKAGE_VERSION, PACKAGE_MANAGER, + SERVICE, PROTOCOL, PRIVATE, REQUIRED_MEMBER_MODE, + DISABLE_DEFAULT_VALIDATION, CREATE_DEFAULT_README, GENERATE_TYPEDOC, + GENERATE_INDEX_TESTS + )); private final BiFunction symbolProviderFactory; private final List configProperties; diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/knowledge/ServiceClosure.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/knowledge/ServiceClosure.java new file mode 100644 index 00000000000..91616f73580 --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/knowledge/ServiceClosure.java @@ -0,0 +1,137 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.typescript.codegen.knowledge; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.KnowledgeIndex; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.model.traits.ErrorTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Retrieves shapes in the service operation closure. + */ +@SmithyInternalApi +public final class ServiceClosure implements KnowledgeIndex { + private static final Map BY_SERVICE = new ConcurrentHashMap<>(); + private final Model model; + + private final TreeSet structures = new TreeSet<>(); + private final TreeSet errors = new TreeSet<>(); + private final TreeSet enums = new TreeSet<>(); + + private final Set scanned = new HashSet<>(); + + private ServiceClosure( + Model model + ) { + this.model = model; + } + + public static ServiceClosure of(Model model, ServiceShape service) { + if (BY_SERVICE.containsKey(service.getId())) { + return BY_SERVICE.get(service.getId()); + } + TopDownIndex topDown = TopDownIndex.of(model); + ServiceClosure instance = new ServiceClosure(model); + Set containedOperations = topDown.getContainedOperations(service); + instance.scan(containedOperations); + instance.scanned.clear(); + + BY_SERVICE.put(service.getId(), instance); + + return instance; + } + + public TreeSet getStructuralNonErrorShapes() { + return structures; + } + + public TreeSet getErrorShapes() { + return errors; + } + + public TreeSet getEnums() { + return enums; + } + + private void scan(Shape shape) { + scan(Collections.singletonList(shape)); + } + + private void scan(Set shapes) { + scan(new ArrayList<>(shapes)); + } + + private void scan(Collection shapes) { + for (Shape shape : shapes) { + if (scanned.contains(shape)) { + continue; + } + scanned.add(shape); + if (shape.isMemberShape()) { + MemberShape memberShape = (MemberShape) shape; + shape = model.expectShape(memberShape.getTarget()); + } + + if (shape.isStructureShape() || shape.isUnionShape()) { + if (shape.hasTrait(ErrorTrait.class)) { + errors.add(shape); + } else { + structures.add(shape); + } + + if (shape instanceof StructureShape structureShape) { + structureShape.getAllMembers().values().forEach(this::scan); + } else if (shape instanceof UnionShape unionShape) { + unionShape.getAllMembers().values().forEach(this::scan); + } + } + + if (shape.isEnumShape() || shape.isIntEnumShape() || shape.hasTrait(EnumTrait.class)) { + enums.add(shape); + } + + if (shape.isListShape()) { + ListShape listShape = (ListShape) shape; + scan(listShape.getMember()); + } + if (shape.isMapShape()) { + MapShape mapShape = (MapShape) shape; + scan(mapShape.getKey()); + scan(mapShape.getValue()); + } + + if (shape.isOperationShape()) { + OperationShape operation = (OperationShape) shape; + if (operation.getInput().isPresent()) { + scan(model.expectShape(operation.getInputShape())); + } + if (operation.getOutput().isPresent()) { + scan(model.expectShape(operation.getOutputShape())); + } + } + } + } +} diff --git a/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/base-package.json b/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/base-package.json index 2c6bdc551a4..e1bdd334877 100644 --- a/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/base-package.json +++ b/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/base-package.json @@ -8,6 +8,7 @@ "build:es": "tsc -p tsconfig.es.json", "build:types": "tsc -p tsconfig.types.json", "build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4", + "test:index": "tsc --noEmit ./test/index-types.ts && node ./test/index-objects.spec.mjs", "clean": "rimraf ./dist-* && rimraf *.tsbuildinfo || exit 0", "prepack": "${packageManager} run clean && ${packageManager} run build" }, diff --git a/smithy-typescript-protocol-test-codegen/smithy-build.json b/smithy-typescript-protocol-test-codegen/smithy-build.json index dff313fd3cb..7d93617be71 100644 --- a/smithy-typescript-protocol-test-codegen/smithy-build.json +++ b/smithy-typescript-protocol-test-codegen/smithy-build.json @@ -24,7 +24,8 @@ }, "license": "Apache-2.0" }, - "private": true + "private": true, + "generateIndexTests": true } } }, @@ -51,7 +52,8 @@ "license": "Apache-2.0" }, "private": true, - "generateSchemas": true + "generateSchemas": true, + "generateIndexTests": true } } }, @@ -94,7 +96,8 @@ "private": true }, "bigNumberMode": "native", - "generateSchemas": true + "generateSchemas": true, + "generateIndexTests": true } } }