diff --git a/Makefile b/Makefile index e147f85b2dd..e33741ef220 100644 --- a/Makefile +++ b/Makefile @@ -22,8 +22,8 @@ generate-protocol-tests: yarn turbo run build -F="./private/*" --only test-protocols: - (cd ./private/smithy-rpcv2-cbor && npx vitest run --globals) - (cd ./private/smithy-rpcv2-cbor-schema && npx vitest run --globals) + (cd ./private/smithy-rpcv2-cbor && npx vitest run --globals && yarn test:index) + (cd ./private/smithy-rpcv2-cbor-schema && npx vitest run --globals && yarn test:index) test-unit: yarn g:vitest run -c vitest.config.mts diff --git a/private/my-local-model-schema/test/index-objects.spec.mjs b/private/my-local-model-schema/test/index-objects.spec.mjs index 5f97cf676a2..e0dc28e52e4 100644 --- a/private/my-local-model-schema/test/index-objects.spec.mjs +++ b/private/my-local-model-schema/test/index-objects.spec.mjs @@ -1,9 +1,14 @@ import { + CodedThrottlingError, GetNumbersCommand, + HaltError, + MysteryThrottlingError, + RetryableError, TradeEventStreamCommand, XYZService, XYZServiceClient, XYZServiceServiceException, + XYZServiceSyntheticServiceException, } from "../dist-cjs/index.js"; import assert from "node:assert"; // clients @@ -13,5 +18,10 @@ assert(typeof XYZService === "function"); assert(typeof GetNumbersCommand === "function"); assert(typeof TradeEventStreamCommand === "function"); // errors -assert(XYZServiceServiceException.prototype instanceof Error); +assert(CodedThrottlingError.prototype instanceof XYZServiceSyntheticServiceException); +assert(HaltError.prototype instanceof XYZServiceSyntheticServiceException); +assert(MysteryThrottlingError.prototype instanceof XYZServiceSyntheticServiceException); +assert(RetryableError.prototype instanceof XYZServiceSyntheticServiceException); +assert(XYZServiceServiceException.prototype instanceof XYZServiceSyntheticServiceException); +assert(XYZServiceSyntheticServiceException.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 index f205e37982c..1c4ee56ab81 100644 --- a/private/my-local-model-schema/test/index-types.ts +++ b/private/my-local-model-schema/test/index-types.ts @@ -14,6 +14,10 @@ export type { TradeEvents, TradeEventStreamRequest, TradeEventStreamResponse, - Unit, + CodedThrottlingError, + HaltError, + MysteryThrottlingError, + RetryableError, XYZServiceServiceException, + XYZServiceSyntheticServiceException, } from "../dist-types/index.d"; diff --git a/private/smithy-rpcv2-cbor-schema/test/index-objects.spec.mjs b/private/smithy-rpcv2-cbor-schema/test/index-objects.spec.mjs index a2728f1470e..18cec8efef8 100644 --- a/private/smithy-rpcv2-cbor-schema/test/index-objects.spec.mjs +++ b/private/smithy-rpcv2-cbor-schema/test/index-objects.spec.mjs @@ -1,10 +1,12 @@ import { + ComplexError, EmptyInputOutputCommand, Float16Command, FooEnum, FractionalSecondsCommand, GreetingWithErrorsCommand, IntegerEnum, + InvalidGreeting, NoInputOutputCommand, OperationWithDefaultsCommand, OptionalInputOutputCommand, @@ -19,6 +21,7 @@ import { SparseNullsOperationCommand, TestEnum, TestIntEnum, + ValidationException, } from "../dist-cjs/index.js"; import assert from "node:assert"; // clients @@ -44,5 +47,8 @@ assert(typeof TestIntEnum === "object"); assert(typeof FooEnum === "object"); assert(typeof IntegerEnum === "object"); // errors +assert(ValidationException.prototype instanceof RpcV2ProtocolServiceException); +assert(ComplexError.prototype instanceof RpcV2ProtocolServiceException); +assert(InvalidGreeting.prototype instanceof RpcV2ProtocolServiceException); 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 index 544bebe7141..ae80f5a65e0 100644 --- a/private/smithy-rpcv2-cbor-schema/test/index-types.ts +++ b/private/smithy-rpcv2-cbor-schema/test/index-types.ts @@ -45,7 +45,9 @@ export type { TestIntEnum, FooEnum, IntegerEnum, + ValidationExceptionField, ClientOptionalDefaults, + ComplexNestedErrorData, Defaults, EmptyStructure, Float16Output, @@ -64,5 +66,8 @@ export type { SparseNullsOperationInputOutput, StructureListMember, GreetingStruct, + ValidationException, + ComplexError, + InvalidGreeting, RpcV2ProtocolServiceException, } from "../dist-types/index.d"; diff --git a/private/smithy-rpcv2-cbor/test/index-objects.spec.mjs b/private/smithy-rpcv2-cbor/test/index-objects.spec.mjs index a2728f1470e..18cec8efef8 100644 --- a/private/smithy-rpcv2-cbor/test/index-objects.spec.mjs +++ b/private/smithy-rpcv2-cbor/test/index-objects.spec.mjs @@ -1,10 +1,12 @@ import { + ComplexError, EmptyInputOutputCommand, Float16Command, FooEnum, FractionalSecondsCommand, GreetingWithErrorsCommand, IntegerEnum, + InvalidGreeting, NoInputOutputCommand, OperationWithDefaultsCommand, OptionalInputOutputCommand, @@ -19,6 +21,7 @@ import { SparseNullsOperationCommand, TestEnum, TestIntEnum, + ValidationException, } from "../dist-cjs/index.js"; import assert from "node:assert"; // clients @@ -44,5 +47,8 @@ assert(typeof TestIntEnum === "object"); assert(typeof FooEnum === "object"); assert(typeof IntegerEnum === "object"); // errors +assert(ValidationException.prototype instanceof RpcV2ProtocolServiceException); +assert(ComplexError.prototype instanceof RpcV2ProtocolServiceException); +assert(InvalidGreeting.prototype instanceof RpcV2ProtocolServiceException); 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 index 544bebe7141..ae80f5a65e0 100644 --- a/private/smithy-rpcv2-cbor/test/index-types.ts +++ b/private/smithy-rpcv2-cbor/test/index-types.ts @@ -45,7 +45,9 @@ export type { TestIntEnum, FooEnum, IntegerEnum, + ValidationExceptionField, ClientOptionalDefaults, + ComplexNestedErrorData, Defaults, EmptyStructure, Float16Output, @@ -64,5 +66,8 @@ export type { SparseNullsOperationInputOutput, StructureListMember, GreetingStruct, + ValidationException, + ComplexError, + InvalidGreeting, RpcV2ProtocolServiceException, } from "../dist-types/index.d"; 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 index 925fabc1d1f..5fd1f5db525 100644 --- 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 @@ -30,7 +30,7 @@ public final class PackageApiValidationGenerator { private final TypeScriptSettings settings; private final Model model; private final SymbolProvider symbolProvider; - private final ServiceClosure serviceClosure; + private final ServiceClosure closure; public PackageApiValidationGenerator( TypeScriptWriter writer, @@ -42,7 +42,7 @@ public PackageApiValidationGenerator( this.settings = settings; this.model = model; this.symbolProvider = symbolProvider; - serviceClosure = ServiceClosure.of(model, settings.getService(model)); + closure = ServiceClosure.of(model, settings.getService(model)); } /** @@ -75,31 +75,32 @@ public void writeTypeIndexTest() { } // enums - TreeSet enumShapes = serviceClosure.getEnums(); + TreeSet enumShapes = closure.getEnums(); for (Shape enumShape : enumShapes) { writer.write("$L,", symbolProvider.toSymbol(enumShape).getName()); } // structure & union types & modeled errors - TreeSet structuralShapes = serviceClosure.getStructuralNonErrorShapes(); + TreeSet structuralShapes = closure.getStructuralNonErrorShapes(); for (Shape structuralShape : structuralShapes) { writer.write("$L,", symbolProvider.toSymbol(structuralShape).getName()); } - TreeSet errorShapes = serviceClosure.getErrorShapes(); + TreeSet errorShapes = closure.getErrorShapes(); for (Shape errorShape : errorShapes) { writer.write("$L,", symbolProvider.toSymbol(errorShape).getName()); } // synthetic base exception - writer.write("$L,", aggregateClientName + "ServiceException"); + String baseExceptionName = CodegenUtils.getSyntheticBaseExceptionName(aggregateClientName, model); + writer.write("$L,", baseExceptionName); // waiters - serviceClosure.getWaiterNames().forEach(waiter -> { + closure.getWaiterNames().forEach(waiter -> { writer.write("$L,", waiter); }); // paginators - serviceClosure.getPaginatorNames().forEach(paginator -> { + closure.getPaginatorNames().forEach(paginator -> { writer.write("$L,", paginator); }); } @@ -150,7 +151,7 @@ public void writeRuntimeIndexTest() { // string shapes with enum trait do not generate anything if // any enum value does not have a name. - TreeSet enumShapes = serviceClosure.getEnums().stream() + TreeSet enumShapes = closure.getEnums().stream() .filter(shape -> shape .getTrait(EnumTrait.class) .map(EnumTrait::hasNames) @@ -169,11 +170,11 @@ public void writeRuntimeIndexTest() { ); } - String baseExceptionName = aggregateClientName + "ServiceException"; + String baseExceptionName = CodegenUtils.getSyntheticBaseExceptionName(aggregateClientName, model); // modeled errors and synthetic base error writer.write("// errors"); - TreeSet errors = serviceClosure.getErrorShapes(); + TreeSet errors = closure.getErrorShapes(); for (Shape error : errors) { Symbol errorSymbol = symbolProvider.toSymbol(error); writer.addRelativeImport(errorSymbol.getName(), null, cjsIndex); @@ -187,7 +188,7 @@ public void writeRuntimeIndexTest() { writer.write("assert($L.prototype instanceof Error);", baseExceptionName); // waiters & paginators - TreeSet waiterNames = serviceClosure.getWaiterNames(); + TreeSet waiterNames = closure.getWaiterNames(); if (!waiterNames.isEmpty()) { writer.write("// waiters"); } @@ -198,7 +199,7 @@ public void writeRuntimeIndexTest() { waiter ); }); - TreeSet paginatorNames = serviceClosure.getPaginatorNames(); + TreeSet paginatorNames = closure.getPaginatorNames(); if (!paginatorNames.isEmpty()) { writer.write("// paginators"); } 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 index 3b231b88508..26339de4fa8 100644 --- 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 @@ -9,13 +9,12 @@ 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.CollectionShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; @@ -28,6 +27,7 @@ import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.ErrorTrait; import software.amazon.smithy.model.traits.PaginatedTrait; +import software.amazon.smithy.typescript.codegen.schema.SchemaReferenceIndex; import software.amazon.smithy.utils.SmithyInternalApi; import software.amazon.smithy.waiters.WaitableTrait; @@ -36,40 +36,86 @@ */ @SmithyInternalApi public final class ServiceClosure implements KnowledgeIndex { - private static final Map BY_SERVICE = new ConcurrentHashMap<>(); + private static final ShapeId UNIT = ShapeId.from("smithy.api#Unit"); private final Model model; + private final ServiceShape service; + private final SchemaReferenceIndex elision; + + /** + * For API testing & schemas. + */ private final TreeSet operations = new TreeSet<>(); - private final TreeSet structures = new TreeSet<>(); + /** + * Note: also contains unions. + * For API testing. + */ + private final TreeSet structuralInterfaces = new TreeSet<>(); + /** + * For API testing. + */ private final TreeSet errors = new TreeSet<>(); + /** + * For API testing. + */ private final TreeSet enums = new TreeSet<>(); - private final Set scanned = new HashSet<>(); + /** + * For schemas. + */ + private final TreeSet structureShapes = new TreeSet<>(); + /** + * For schemas. + */ + private final TreeSet collectionShapes = new TreeSet<>(); + /** + * For schemas. + */ + private final TreeSet mapShapes = new TreeSet<>(); + /** + * For schemas. + */ + private final TreeSet unionShapes = new TreeSet<>(); + /** + * For schemas. + */ + private final TreeSet simpleShapes = new TreeSet<>(); + /** + * Used to deconflict schema variable names. + * Iteration determinism is desired (ordered set). + */ + private final TreeSet existsAsSchema = new TreeSet<>(); + /** + * For schemas. + */ + private final Set requiresNamingDeconfliction = new HashSet<>(); + + /** + * Used temporarily during initial traversal. + */ + private final Set scanned = new HashSet<>(); private ServiceClosure( - Model model + Model model, + ServiceShape service ) { this.model = model; - } - - public static ServiceClosure of(Model model, ServiceShape service) { - if (BY_SERVICE.containsKey(service.getId())) { - return BY_SERVICE.get(service.getId()); - } + this.service = service; + elision = SchemaReferenceIndex.of(model); TopDownIndex topDown = TopDownIndex.of(model); - ServiceClosure instance = new ServiceClosure(model); Set containedOperations = topDown.getContainedOperations(service); - instance.operations.addAll(containedOperations); - instance.scan(containedOperations); - instance.scanned.clear(); - - BY_SERVICE.put(service.getId(), instance); + operations.addAll(containedOperations); + scan(containedOperations); + scanned.clear(); + deconflictSchemaVarNames(); + } - return instance; + public static ServiceClosure of(Model model, ServiceShape service) { + return model.getKnowledge(ServiceClosure.class, (Model m) -> new ServiceClosure(m, service)); } public TreeSet getStructuralNonErrorShapes() { - return structures; + return structuralInterfaces; } public TreeSet getErrorShapes() { @@ -103,6 +149,50 @@ public TreeSet getPaginatorNames() { return paginators; } + public Set getRequiresNamingDeconfliction() { + return requiresNamingDeconfliction; + } + + public TreeSet getSimpleShapes() { + return simpleShapes; + } + + public TreeSet getStructureShapes() { + return structureShapes; + } + + public TreeSet getUnionShapes() { + return unionShapes; + } + + public TreeSet getMapShapes() { + return mapShapes; + } + + public TreeSet getCollectionShapes() { + return collectionShapes; + } + + public TreeSet getOperationShapes() { + return operations; + } + + /** + * Since we use the short names for schema objects, in rare cases there may be a + * naming conflict due to shapes with the same short name in different namespaces. + * These shapes will have their variable names deconflicted with a suffix. + */ + private void deconflictSchemaVarNames() { + Set observedShapeNames = new HashSet<>(); + for (Shape shape : existsAsSchema) { + if (observedShapeNames.contains(shape.getId().getName())) { + requiresNamingDeconfliction.add(shape); + } else { + observedShapeNames.add(shape.getId().getName()); + } + } + } + private void scan(Shape shape) { scan(Collections.singletonList(shape)); } @@ -113,50 +203,87 @@ private void scan(Set shapes) { private void scan(Collection shapes) { for (Shape shape : shapes) { - if (scanned.contains(shape)) { + if (scanned.contains(shape.getId())) { continue; } - scanned.add(shape); + scanned.add(shape.getId()); + 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); + switch (shape.getType()) { + case LIST -> { + ListShape listShape = (ListShape) shape; + collectionShapes.add(listShape); + existsAsSchema.add(listShape); + scan(listShape.getMember()); } - - if (shape instanceof StructureShape structureShape) { - structureShape.getAllMembers().values().forEach(this::scan); - } else if (shape instanceof UnionShape unionShape) { - unionShape.getAllMembers().values().forEach(this::scan); + case SET -> { + var setShape = shape.asSetShape().get(); + collectionShapes.add(setShape); + existsAsSchema.add(setShape); + scan(setShape.getMember()); } - } + case MAP -> { + MapShape mapShape = (MapShape) shape; + mapShapes.add(mapShape); + existsAsSchema.add(mapShape); + scan(mapShape.getKey()); + scan(mapShape.getValue()); + } + case STRUCTURE, UNION -> { + if (shape.isStructureShape()) { + structureShapes.add(shape.asStructureShape().get()); + } else if (shape.isUnionShape()) { + unionShapes.add(shape.asUnionShape().get()); + } + existsAsSchema.add(shape); - if (shape.isEnumShape() || shape.isIntEnumShape() || shape.hasTrait(EnumTrait.class)) { - enums.add(shape); - } + if (shape.hasTrait(ErrorTrait.class)) { + errors.add(shape); + } else if (!shape.getId().equals(UNIT)) { + structuralInterfaces.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 instanceof StructureShape structureShape) { + structureShape.getAllMembers().values().forEach(this::scan); + } else if (shape instanceof UnionShape unionShape) { + unionShape.getAllMembers().values().forEach(this::scan); + } + } + case OPERATION -> { + OperationShape operation = (OperationShape) shape; + if (operation.getInput().isPresent()) { + scan(model.expectShape(operation.getInputShape())); + } else { + scan(model.expectShape(UNIT)); + } + if (operation.getOutput().isPresent()) { + scan(model.expectShape(operation.getOutputShape())); + } else { + scan(model.expectShape(UNIT)); + } + operation.getErrors(service).forEach(error -> { + scan(model.expectShape(error)); + }); + operations.add(operation); + existsAsSchema.add(operation); + } + case BYTE, INT_ENUM, SHORT, INTEGER, LONG, FLOAT, DOUBLE, BIG_INTEGER, BIG_DECIMAL, BOOLEAN, STRING, + TIMESTAMP, DOCUMENT, ENUM, BLOB -> { + if (shape.isEnumShape() || shape.isIntEnumShape() || shape.hasTrait(EnumTrait.class)) { + enums.add(shape); + } - if (shape.isOperationShape()) { - OperationShape operation = (OperationShape) shape; - if (operation.getInput().isPresent()) { - scan(model.expectShape(operation.getInputShape())); + if (elision.traits.hasSchemaTraits(shape)) { + existsAsSchema.add(shape); + } + simpleShapes.add(shape); } - if (operation.getOutput().isPresent()) { - scan(model.expectShape(operation.getOutputShape())); + default -> { + // ... } } } diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaGenerator.java index 33b95e9f65e..e08bacbcd47 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaGenerator.java @@ -6,18 +6,13 @@ package software.amazon.smithy.typescript.codegen.schema; import java.nio.file.Paths; -import java.util.HashSet; import java.util.Objects; import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Collectors; import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.codegen.core.ReservedWords; import software.amazon.smithy.codegen.core.ReservedWordsBuilder; 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.CollectionShape; import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; @@ -36,6 +31,7 @@ import software.amazon.smithy.typescript.codegen.TypeScriptDependency; import software.amazon.smithy.typescript.codegen.TypeScriptSettings; import software.amazon.smithy.typescript.codegen.TypeScriptWriter; +import software.amazon.smithy.typescript.codegen.knowledge.ServiceClosure; import software.amazon.smithy.typescript.codegen.util.StringStore; import software.amazon.smithy.utils.SmithyInternalApi; @@ -52,24 +48,7 @@ public class SchemaGenerator implements Runnable { private final FileManifest fileManifest; private final StringStore store = new StringStore(); private final TypeScriptWriter writer = new TypeScriptWriter(""); - - /** - * Avoids infinite recursion when navigating shape graph. - */ - private final Set loadShapesVisited = new HashSet<>(); - - private final Set structureShapes = new TreeSet<>(); - private final Set collectionShapes = new TreeSet<>(); - private final Set mapShapes = new TreeSet<>(); - private final Set unionShapes = new TreeSet<>(); - private final Set operationShapes = new TreeSet<>(); - private final Set simpleShapes = new TreeSet<>(); - - /** - * Used to deconflict schema variable names. - */ - private final Set existsAsSchema = new HashSet<>(); - private final Set requiresNamingDeconfliction = new HashSet<>(); + private final ServiceClosure closure; private final ReservedWords reservedWords = new ReservedWordsBuilder() .loadWords(Objects.requireNonNull(TypeScriptClientCodegenPlugin.class.getResource("reserved-words.txt"))) @@ -80,6 +59,7 @@ public SchemaGenerator(Model model, TypeScriptSettings settings, SymbolProvider symbolProvider) { this.model = model; this.fileManifest = fileManifest; + closure = ServiceClosure.of(model, settings.getService(model)); elision = SchemaReferenceIndex.of(model); this.settings = settings; this.symbolProvider = symbolProvider; @@ -96,33 +76,14 @@ public void run() { if (!SchemaGenerationAllowlist.allows(service.getId(), settings)) { return; } - for (OperationShape operation : TopDownIndex.of(model).getContainedOperations(service)) { - if (operation.getInput().isPresent()) { - loadShapes(model.expectShape(operation.getInput().get())); - } else { - loadShapes(model.expectShape(ShapeId.from("smithy.api#Unit"))); - } - if (operation.getOutput().isPresent()) { - loadShapes(model.expectShape(operation.getOutput().get())); - } else { - loadShapes(model.expectShape(ShapeId.from("smithy.api#Unit"))); - } - operation.getErrors().forEach(error -> { - loadShapes(model.expectShape(error)); - }); - operationShapes.add(operation); - existsAsSchema.add(operation); - } } - deconflictSchemaVarNames(); - - simpleShapes.forEach(this::writeSimpleSchema); - structureShapes.forEach(this::writeStructureSchema); + closure.getSimpleShapes().forEach(this::writeSimpleSchema); + closure.getStructureShapes().forEach(this::writeStructureSchema); writeBaseError(); - collectionShapes.forEach(this::writeListSchema); - mapShapes.forEach(this::writeMapSchema); - unionShapes.forEach(this::writeUnionSchema); - operationShapes.forEach(this::writeOperationSchema); + closure.getCollectionShapes().forEach(this::writeListSchema); + closure.getMapShapes().forEach(this::writeMapSchema); + closure.getUnionShapes().forEach(this::writeUnionSchema); + closure.getOperationShapes().forEach(this::writeOperationSchema); String stringConstants = store.flushVariableDeclarationCode(); @@ -135,82 +96,6 @@ public void run() { } } - /** - * Identifies repeated strings among the schemas to use in StringStore. - */ - private void loadShapes(Shape shape) { - String absoluteName = shape.getId().toString(); - - if (shape.isMemberShape()) { - loadShapes(model.expectShape(shape.asMemberShape().get().getTarget())); - return; - } - - if (loadShapesVisited.contains(absoluteName)) { - return; - } - - loadShapesVisited.add(absoluteName); - - switch (shape.getType()) { - case LIST -> { - collectionShapes.add(shape.asListShape().get()); - existsAsSchema.add(shape); - } - case SET -> { - collectionShapes.add(shape.asSetShape().get()); - existsAsSchema.add(shape); - } - case MAP -> { - mapShapes.add(shape.asMapShape().get()); - existsAsSchema.add(shape); - } - case STRUCTURE -> { - structureShapes.add(shape.asStructureShape().get()); - existsAsSchema.add(shape); - } - case UNION -> { - unionShapes.add(shape.asUnionShape().get()); - existsAsSchema.add(shape); - } - case BYTE, INT_ENUM, SHORT, INTEGER, LONG, FLOAT, DOUBLE, BIG_INTEGER, BIG_DECIMAL, BOOLEAN, STRING, - TIMESTAMP, DOCUMENT, ENUM, BLOB -> { - if (elision.traits.hasSchemaTraits(shape)) { - existsAsSchema.add(shape); - } - simpleShapes.add(shape); - } - default -> { - // ... - } - } - - Set memberTargetShapes = shape.getAllMembers().values().stream() - .map(MemberShape::getTarget) - .map(model::expectShape) - .collect(Collectors.toSet()); - - for (Shape memberTargetShape : memberTargetShapes) { - loadShapes(memberTargetShape); - } - } - - /** - * Since we use the short names for schema objects, in rare cases there may be a - * naming conflict due to shapes with the same short name in different namespaces. - * These shapes will have their variable names deconflicted with a suffix. - */ - private void deconflictSchemaVarNames() { - Set observedShapeNames = new HashSet<>(); - for (Shape shape : existsAsSchema) { - if (observedShapeNames.contains(shape.getId().getName())) { - requiresNamingDeconfliction.add(shape); - } else { - observedShapeNames.add(shape.getId().getName()); - } - } - } - /** * @return variable name of the shape's schema, with deconfliction for multiple namespaces with the same * unqualified name. @@ -220,7 +105,7 @@ private String getShapeVariableName(Shape shape) { return "__Unit"; } String symbolName = reservedWords.escape(shape.getId().getName()); - if (requiresNamingDeconfliction.contains(shape)) { + if (closure.getRequiresNamingDeconfliction().contains(shape)) { symbolName += "_" + store.var(shape.getId().getNamespace(), "n"); } return symbolName; diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaReferenceIndex.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaReferenceIndex.java index 92ac13076aa..6c11f24a99a 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaReferenceIndex.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaReferenceIndex.java @@ -18,7 +18,7 @@ * Can determine whether a Schema can be defined by a sentinel value. */ @SmithyInternalApi -final class SchemaReferenceIndex implements KnowledgeIndex { +public final class SchemaReferenceIndex implements KnowledgeIndex { public final SchemaTraitFilterIndex traits; private final Model model; diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaTraitFilterIndex.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaTraitFilterIndex.java index a9d5d6c6832..f74dc915fb8 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaTraitFilterIndex.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/SchemaTraitFilterIndex.java @@ -47,7 +47,7 @@ import software.amazon.smithy.utils.SmithyInternalApi; @SmithyInternalApi -final class SchemaTraitFilterIndex implements KnowledgeIndex { +public final class SchemaTraitFilterIndex implements KnowledgeIndex { private static final Set EXCLUDED_TRAITS = SetUtils.of( // excluded due to special schema handling. TimestampFormatTrait.ID @@ -132,6 +132,8 @@ public boolean includeTrait(ShapeId traitShapeId) { } /** + * This operation is cached on call. + * * @param shape - structure or member, usually. * @return whether it has at least 1 trait that is needed in a schema. */ diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/ShapeGroupingIndex.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/ShapeGroupingIndex.java deleted file mode 100644 index 98d92bee201..00000000000 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/schema/ShapeGroupingIndex.java +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.typescript.codegen.schema; - -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; -import java.util.stream.Stream; -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.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.utils.SmithyInternalApi; - -/** - * Creates schema groupings. - * E.g. used to create disjoint sets of schemas to assist with tree-shaking. - */ -@SmithyInternalApi -public class ShapeGroupingIndex implements KnowledgeIndex { - public static final String FILENAME_PREFIX = "schemas"; - - /** - * The maximum number of operations to place into one group (file). - */ - public static final int MAX_OPERATIONS_GROUP_SIZE = 12; - - /** - * Instances of this class for specific models. - */ - private static final Map INSTANCES = new ConcurrentHashMap<>(); - - /** - * Shapes mapped to operations that use them. - */ - private final Map> shapeToOperationDependents = new HashMap<>(); - - /** - * Shapes mapped to the largest logical grouping of operations making use of the shape. - */ - private final Map> shapeToOperationalGroup = new HashMap<>(); - - /** - * Hashed combined operation names to their numeric group id. - */ - private final Map opGroups = new HashMap<>(); - - /** - * Combined operation names mapped to a readable group name. - */ - private final Map groupNames = new HashMap<>(); - - /** - * Last group assigned by increasing number. - */ - private int lastGroup = 0; - - /** - * Contextual model. - */ - private Model model; - - public static ShapeGroupingIndex of(Model model) { - return INSTANCES.computeIfAbsent(model, k -> { - ShapeGroupingIndex shapeTreeOrganizer = new ShapeGroupingIndex(); - shapeTreeOrganizer.loadModel(model); - return shapeTreeOrganizer; - }); - } - - /** - * @return the group name (filename) of the schema group for the given shape. - */ - public String getGroup(ShapeId id) { - /* - As of the introduction of static schemas, we don't need to bucket schemas into - different files anymore. - todo: remove usage of this class. - */ - return getBaseGroup(); - - /* - if (!shapeToOperationalGroup.containsKey(id)) { - return getBaseGroup(); - } - TreeSet operations = shapeToOperationalGroup.get(id); - return hashOperationSet(operations); - */ - } - - /** - * @return whether shape is in the base group. - */ - public boolean isBaseGroup(Shape shape) { - return getGroup(shape.getId()).equals(getBaseGroup()); - } - - /** - * @return whether two shapes are in different groups. - */ - public boolean different(Shape a, Shape b) { - return !Objects.equals( - getGroup(a.getId()), - getGroup(b.getId()) - ); - } - - /** - * Initialize for given model. - */ - private void loadModel(Model model) { - if (this.model != null) { - throw new IllegalArgumentException("Model has already been loaded"); - } - this.model = model; - for (ServiceShape service : model.getServiceShapes()) { - for (OperationShape operation : TopDownIndex.of(model).getContainedOperations(service)) { - readOperationClosure(operation, new HashSet<>()); - } - } - - // restack operational groups. - for (Map.Entry> entry : shapeToOperationDependents.entrySet()) { - ShapeId shapeId = entry.getKey(); - TreeSet dependentOperations = entry.getValue(); - - shapeToOperationalGroup.put(shapeId, - shapeToOperationDependents.values() - .stream() - .filter(group -> group.size() < MAX_OPERATIONS_GROUP_SIZE) - .filter(group -> group.containsAll(dependentOperations)) - .max(Comparator.comparing(TreeSet::size)) - .orElse(dependentOperations) - ); - } - - // precompute group allocations. - shapeToOperationalGroup.keySet() - .forEach(this::getGroup); - } - - /** - * @return a string hash identifying the group that this set of operations is assigned to. - */ - private String hashOperationSet(TreeSet operations) { - if (operations.size() > MAX_OPERATIONS_GROUP_SIZE) { - return getBaseGroup(); - } - String key = joinOperationNames(operations); - if (opGroups.containsKey(key) && groupNames.containsKey(key)) { - return FILENAME_PREFIX - + "_" + opGroups.get(key) - + "_" + groupNames.get(key); - } else { - opGroups.put(key, ++lastGroup); - groupNames.put(key, nominateGroupName(operations)); - } - return FILENAME_PREFIX - + "_" + lastGroup - + "_" + groupNames.get(key); - } - - /** - * Determines a name for the group of operations - * based on the most commonly observed name or structure. - * Uses "longest common phrase" algorithm. - */ - private String nominateGroupName(TreeSet operations) { - if (operations.size() == 1) { - return operations.iterator().next().getName(); - } - - Set names = operations.stream().map(ShapeId::getName).collect(Collectors.toSet()); - int minLength = 3; - - Stream phrases = names.stream() - .flatMap(operationName -> names.stream() - .filter(otherOperationName -> !otherOperationName.equals(operationName)) - .flatMap(other -> { - Set wordPhrases = new HashSet<>(); - - // expensive, but cached. - for (int i = 0; i < operationName.length(); ++i) { - for (int j = i + 1; j <= operationName.length(); ++j) { - String candidate = operationName.substring(i, j); - - if (candidate.length() >= minLength && other.contains(candidate)) { - boolean validNounPhrase = isValidNounPhrase(operationName, i, j); - if (validNounPhrase) { - wordPhrases.add(candidate); - } - } - } - } - - return wordPhrases.stream(); - }) - ); - - return phrases - .collect(Collectors.groupingBy(s -> s, Collectors.counting())) - .entrySet() - .stream() - .max(Map.Entry.comparingByValue()) - .map(Map.Entry::getKey) - .orElse(""); - } - - /** - * The substring from i to j starts with a capital letter and ends - * with the string or at another capital letter or number. - */ - private static boolean isValidNounPhrase(String name, int i, int j) { - String candidate = name.substring(i, j); - - boolean capitalInitialChar = candidate.substring(0, 1).matches("[A-Z]"); - boolean endsWord = name.length() == j - || name.substring(j, j + 1).matches("[A-Z0-9]"); - - return capitalInitialChar && endsWord; - } - - private String getBaseGroup() { - return FILENAME_PREFIX + "_0"; - } - - /** - * Make known that a shape id is used within a certain operation. - */ - private void register(ShapeId operationId, ShapeId shapeId) { - shapeToOperationDependents.computeIfAbsent(shapeId, k -> new TreeSet<>()).add(operationId); - } - - /** - * Explore the set of shapes in the closure of an operation. - */ - private void readOperationClosure(OperationShape op, Set visited) { - registerShapes(op, op, visited); - op.getInput().ifPresent(inputShape -> { - registerShapes(op, model.expectShape(inputShape), visited); - }); - op.getOutput().ifPresent(outputShape -> { - registerShapes(op, model.expectShape(outputShape), visited); - }); - op.getErrors().forEach(error -> { - registerShapes(op, model.expectShape(error), visited); - }); - } - - /** - * Registers knowledge of the shape in the context of an operation. - * Recurses on referenced shapes in the input shape. - */ - private void registerShapes(OperationShape op, Shape shape, Set visited) { - if (shape.isMemberShape()) { - registerShapes(op, model.expectShape(shape.asMemberShape().get().getTarget()), visited); - return; - } - if (visited.contains(shape)) { - return; - } - visited.add(shape); - register(op.getId(), shape.getId()); - - Set memberTargetShapes = shape.getAllMembers().values().stream() - .map(MemberShape::getTarget) - .map(model::expectShape) - .collect(Collectors.toSet()); - - for (Shape memberTargetShape : memberTargetShapes) { - registerShapes(op, memberTargetShape, visited); - } - } - - /** - * Used to create a hash of a set of operations. - */ - private String joinOperationNames(TreeSet operations) { - return operations.stream().map(ShapeId::getName).collect(Collectors.joining(",")); - } -}