diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenVisitor.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenVisitor.java index 67c90495a9f..d9a6f25ff82 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenVisitor.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenVisitor.java @@ -25,6 +25,7 @@ import java.util.ServiceLoader; import java.util.Set; import java.util.TreeSet; +import java.util.UUID; import java.util.logging.Logger; import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.build.PluginContext; @@ -32,9 +33,13 @@ import software.amazon.smithy.codegen.core.SymbolDependency; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.codegen.core.TopologicalIndex; +import software.amazon.smithy.codegen.core.trace.ArtifactDefinitions; +import software.amazon.smithy.codegen.core.trace.TraceMetadata; +import software.amazon.smithy.codegen.core.trace.TracingSymbolProvider; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.neighbor.Walker; +import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; @@ -93,6 +98,7 @@ class CodegenVisitor extends ShapeVisitor.Default { runtimePlugins.add(runtimePlugin); }); }); + // Sort the integrations in specified order. integrations.sort(Comparator.comparingInt(TypeScriptIntegration::getOrder)); @@ -119,13 +125,44 @@ class CodegenVisitor extends ShapeVisitor.Default { for (TypeScriptIntegration integration : integrations) { resolvedProvider = integration.decorateSymbolProvider(settings, model, resolvedProvider); } - symbolProvider = SymbolProvider.cache(resolvedProvider); // Resolve the nullable protocol generator and application protocol. protocolGenerator = resolveProtocolGenerator(integrations, service, settings); applicationProtocol = protocolGenerator == null ? ApplicationProtocol.createDefaultHttpApplicationProtocol() : protocolGenerator.getApplicationProtocol(); + // Make the symbol provider a cachingSymbolProvider. + SymbolProvider cachedProvider = SymbolProvider.cache(resolvedProvider); + // Defining Definitions for TraceFile Generation. + ArtifactDefinitions artifactDefinitions = ArtifactDefinitions.builder() + .addType(TypeScriptShapeLinkProvider.FIELD_TYPE, + "Field declaration (includes enum constants)") + .addType(TypeScriptShapeLinkProvider.METHOD_TYPE, "Method declaration") + .addType(TypeScriptShapeLinkProvider.TYPE_TYPE, + "Class, interface (including annotation type), or enum declaration") + .addTag(TypeScriptShapeLinkProvider.SERVICE_TAG, "Service client") + .addTag(TypeScriptShapeLinkProvider.REQUEST_TAG, "AWS SDK request type") + .addTag(TypeScriptShapeLinkProvider.RESPONSE_TAG, "AWS SDK response type") + .addTag(TypeScriptShapeLinkProvider.SERIALIZER_TAG, "Command serializer") + .addTag(TypeScriptShapeLinkProvider.DESERIALIZER_TAG, "Command deserializer") + .build(); + + String serviceId = service.getId().getName(); + TraceMetadata artifactMetadata = TraceMetadata.builder() + .setTimestampAsNow() + .id(serviceId) + .version(UUID.randomUUID().toString()) + .type("TypeScript") + .build(); + + + // Decorate the symbol provider using the trace file generator. + symbolProvider = TracingSymbolProvider.builder() + .symbolProvider(cachedProvider) + .metadata(artifactMetadata) + .artifactDefinitions(artifactDefinitions) + .shapeLinkCreator(new TypeScriptShapeLinkProvider()) + .build(); writers = new TypeScriptDelegator(settings, model, fileManifest, symbolProvider, integrations); } @@ -212,6 +249,13 @@ void execute() { settings, model, protocol, symbolProvider, writers, protocolGenerator).run(); } + // Write the TraceFile. + TracingSymbolProvider traceProvider = (TracingSymbolProvider) symbolProvider; + String traceName = symbolProvider.toSymbol(service).getName().replace("Client", "") + .toLowerCase() + ".trace.json"; + fileManifest.writeFile(traceName, + Node.prettyPrintJson(traceProvider.buildTraceFile().toNode())); + // Write each pending writer. LOGGER.fine("Flushing TypeScript writers"); List dependencies = writers.getDependencies(); @@ -428,7 +472,7 @@ private void generateCommands(ServiceShape shape) { TopDownIndex topDownIndex = TopDownIndex.of(model); Set containedOperations = new TreeSet<>(topDownIndex.getContainedOperations(shape)); for (OperationShape operation : containedOperations) { - // Right now this only generates stubs + // Right now this only generates stubs. if (settings.generateClient()) { writers.useShapeWriter(operation, commandWriter -> new CommandGenerator( settings, model, operation, symbolProvider, commandWriter, diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/SymbolVisitor.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/SymbolVisitor.java index 3f68cbc1faf..6b63e6290d9 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/SymbolVisitor.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/SymbolVisitor.java @@ -250,6 +250,7 @@ public Symbol operationShape(OperationShape shape) { // Add input and output type symbols (XCommandInput / XCommandOutput). builder.putProperty("inputType", intermediate.toBuilder().name(commandName + "Input").build()); builder.putProperty("outputType", intermediate.toBuilder().name(commandName + "Output").build()); + return builder.build(); } @@ -327,14 +328,20 @@ private Symbol.Builder addSmithyUseImport(Symbol.Builder builder, String name, S @Override public Symbol unionShape(UnionShape shape) { - return createObjectSymbolBuilder(shape).build(); + return createObjectSymbolBuilder(shape) + .putProperty("SymbolProvider", this) + .build(); } @Override public Symbol memberShape(MemberShape shape) { Shape targetShape = model.getShape(shape.getTarget()) .orElseThrow(() -> new CodegenException("Shape not found: " + shape.getTarget())); - Symbol targetSymbol = toSymbol(targetShape); + Symbol targetSymbol = toSymbol(targetShape) + .toBuilder() + .putProperty("model", model) + .putProperty("SymbolProvider", this) + .build(); if (targetSymbol.getProperties().containsKey(EnumTrait.class.getName())) { return createMemberSymbolWithEnumTarget(targetSymbol); @@ -383,14 +390,20 @@ private Symbol.Builder createObjectSymbolBuilder(Shape shape) { } private Symbol.Builder createSymbolBuilder(Shape shape, String typeName) { - return Symbol.builder().putProperty("shape", shape).name(typeName); + return Symbol.builder() + .putProperty("shape", shape) + .putProperty("traceFileNamespace", moduleNameDelegator.formatModuleName(shape, null)) + .putProperty("traceFileNamespaceDelimiter", "/") + .name(typeName); } private Symbol.Builder createSymbolBuilder(Shape shape, String typeName, String namespace) { return Symbol.builder() .putProperty("shape", shape) .name(typeName) - .namespace(namespace, "/"); + .namespace(namespace, "/") + .putProperty("traceFileNamespace", moduleNameDelegator.formatModuleName(shape, null)) + .putProperty("traceFileNamespaceDelimiter", "/"); } private Symbol.Builder createGeneratedSymbolBuilder(Shape shape, String typeName, String namespace) { diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptShapeLinkProvider.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptShapeLinkProvider.java new file mode 100644 index 00000000000..a3578e599cf --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptShapeLinkProvider.java @@ -0,0 +1,428 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.typescript.codegen; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.trace.ShapeLink; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.StringUtils; + +/** + * BiFunction for TraceFile generation that defines a mapping from shapes and symbols to ShapeLinks. + */ +final class TypeScriptShapeLinkProvider implements BiFunction> { + static final String BASE_PACKAGE = "software.amazon.awssdk.services"; + // Definitions types. + static final String FIELD_TYPE = "FIELD"; + static final String METHOD_TYPE = "METHOD"; + static final String TYPE_TYPE = "TYPE"; + // Definitions tags. + static final String SERVICE_TAG = "service"; + static final String REQUEST_TAG = "request"; + static final String RESPONSE_TAG = "response"; + static final String SERIALIZER_TAG = "serializer"; + static final String DESERIALIZER_TAG = "deserializer"; + + private Map cachedParentMap = new HashMap<>(); + private String serviceSymbolName = null; + + TypeScriptShapeLinkProvider() { + } + + /** + * Maps a Shape and a Symbol to a List of ShapeLinks. Mappings depend + * on the ShapeType and the EnumTrait. All classes, interfaces, types + * interface and class fields, and interface and class methods associated + * with each Smithy Shape are included. Interface and class + * fields that are not associated with a Smithy Shape, but are still + * generated are not included. For example, the fields of the ClientDefaults + * interface in the (ServiceName)Client.ts file are not included because + * they are not associated with a Smithy Shape. + * + * @param shape Smithy shape for ShapeLink mapping. + * @param symbol Smithy symbol for ShapeLink mapping. + * @return ShapeLink that contains a mapping + */ + @Override + public List apply(Shape shape, Symbol symbol) { + switch (shape.getType()) { + case OPERATION: + return operationMapping(symbol, shape); + case UNION: + return unionMapping(symbol, shape); + case STRUCTURE: + return structureMapping(symbol, shape); + case STRING: + return stringMapping(symbol, shape); + case SERVICE: + return serviceMapping(symbol); + case MEMBER: + return memberMapping(symbol, shape); + default: + return ListUtils.copyOf(new ArrayList()); + } + } + + private List stringMapping(Symbol symbol, Shape shape) { + if (shape.getTrait(EnumTrait.class).isPresent()) { + cachedParentMap.put(shape.getId(), symbol); + return ListUtils.of(ShapeLink.builder() + .file(getFile(symbol)) + .id(getBaseIdWithName(symbol).toString()) + .type(TYPE_TYPE) + .build()); + } + return ListUtils.copyOf(new ArrayList<>()); + } + + private List unionMapping(Symbol symbol, Shape shape) { + cachedParentMap.put(shape.getId(), symbol); + List shapeLinkList = new ArrayList<>(); + String file = getFile(symbol); + TypeScriptTracingId baseId = getBaseIdWithName(symbol); + + // Get the SymbolProvider. + SymbolProvider symbolProvider = (SymbolProvider) symbol.expectProperty("SymbolProvider"); + + // Type has a matching namespace artifact that is not included. + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder() + .build() + .toString()) + .type(TYPE_TYPE) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().appendToPackageName("Visitor") + .build() + .toString()) + .type(TYPE_TYPE) + .build()); + + for (MemberShape memberShape: shape.asUnionShape().get().getAllMembers().values()) { + String fieldName = symbolProvider.toMemberName(memberShape); + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().appendToPackageName("Visitor") + .fieldName(fieldName) + .build() + .toString()) + .type(FIELD_TYPE) + .build()); + } + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().methodName("visit") + .build() + .toString()) + .type(METHOD_TYPE) + .build()); + + return ListUtils.copyOf(shapeLinkList); + } + + private List structureMapping(Symbol symbol, Shape shape) { + cachedParentMap.put(shape.getId(), symbol); + List shapeLinkList = new ArrayList<>(); + String type = TYPE_TYPE; + String file = getFile(symbol); + TypeScriptTracingId baseId = getBaseIdWithName(symbol); + + // Interface has a matching namespace artifact that was not included. + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().build().toString()) + .type(type) + .build()); + + // Adding isa constant. + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().methodName("isa").build().toString()) + .type(type) + .build()); + + // Adding filterSensitiveLog constant. + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().methodName("filterSensitiveLog").build().toString()) + .type(type) + .build()); + + return ListUtils.copyOf(shapeLinkList); + } + + private List memberMapping(Symbol symbol, Shape shape) { + List shapeLinkList = new ArrayList<>(); + String file = getFile(symbol); + + // Get the model property. + Model model = (Model) symbol.expectProperty("model"); + SymbolProvider symbolProvider = (SymbolProvider) symbol.expectProperty("SymbolProvider"); + // Get the member's parentShape and symbol. + Shape parentShape = model.expectShape(shape.getId().withoutMember()); + Symbol parentSymbol = cachedParentMap.get(shape.getId().withoutMember()); + + if (parentShape.isUnionShape()) { + String memberName = StringUtils.capitalize(symbolProvider.toMemberName((MemberShape) shape)) + "Member"; + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(getBaseIdWithoutName(symbol).toBuilder() + .appendToPackageName(parentSymbol.getName()) + .appendToPackageName(memberName) + .build() + .toString()) + .type(TYPE_TYPE) + .build()); + } else { + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(getBaseIdWithoutName(symbol).toBuilder() + .appendToPackageName(shape.getId().asRelativeReference()) + .build() + .toString()) + .type(FIELD_TYPE) + .build()); + } + return ListUtils.copyOf(shapeLinkList); + } + + private List operationMapping(Symbol symbol, Shape shape) { + cachedParentMap.put(shape.getId(), symbol); + List shapeLinkList = new ArrayList<>(); + + String file = getFile(symbol); + TypeScriptTracingId baseId = getBaseIdWithName(symbol); + TypeScriptTracingId baseIdWithoutName = getBaseIdWithoutName(symbol); + + String inputType = ((Symbol) symbol.expectProperty("inputType")).getName(); + String outputType = ((Symbol) symbol.expectProperty("outputType")).getName(); + + // Tracing CommandGenerator. + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toString()) + .type(TYPE_TYPE) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseIdWithoutName.toBuilder().appendToPackageName(inputType).build().toString()) + .type(TYPE_TYPE) + .tags(Collections.singletonList(REQUEST_TAG)) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseIdWithoutName.toBuilder().appendToPackageName(outputType).build().toString()) + .type(TYPE_TYPE) + .tags(Collections.singletonList(RESPONSE_TAG)) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().methodName("constructor") + .build() + .toString()) + .type(METHOD_TYPE) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().methodName("serialize") + .build() + .toString()) + .type(METHOD_TYPE) + .tags(new ArrayList<>(Collections.singletonList(SERIALIZER_TAG))) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().methodName("deserialize") + .build() + .toString()) + .type(METHOD_TYPE) + .tags(new ArrayList<>(Collections.singletonList(DESERIALIZER_TAG))) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().methodName("resolveMiddleware") + .build() + .toString()) + .type(METHOD_TYPE) + .build()); + + // Tracing Unstructured Service Generator. + String methodName = StringUtils.uncapitalize( + symbol.getName().replaceAll("Command$", "") + ); + + // Different file without client at the end of service name. + file = "./" + serviceSymbolName + ".ts"; + + baseId = TypeScriptTracingId.builder().packageName(BASE_PACKAGE) + .appendToPackageName(serviceSymbolName) + .methodName(methodName) + .build(); + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder() + .build() + .toString()) + .type(METHOD_TYPE) + .build()); + + return ListUtils.copyOf(shapeLinkList); + } + + private List serviceMapping(Symbol symbol) { + List shapeLinkList = new ArrayList<>(); + String type = TYPE_TYPE; + String file = getFile(symbol); + TypeScriptTracingId baseId = getBaseIdWithoutName(symbol); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().appendToPackageName("ServiceInputTypes").build().toString()) + .type(type) + .tags(new ArrayList<>(Collections.singletonList(REQUEST_TAG))) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().appendToPackageName("ServiceOutputTypes").build().toString()) + .type(type) + .tags(new ArrayList<>(Collections.singletonList(RESPONSE_TAG))) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().appendToPackageName("ClientDefaults").build().toString()) + .type(type) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().appendToPackageName(symbol.getName() + "Config").build().toString()) + .type(type) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().appendToPackageName(symbol.getName() + "ResolvedConfig").build().toString()) + .type(type) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().appendToPackageName(symbol.getName()).build().toString()) + .tags(new ArrayList<>(Collections.singletonList(SERVICE_TAG))) + .type(type) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().appendToPackageName(symbol.getName()) + .fieldName("config") + .build() + .toString()) + .type(type) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().appendToPackageName(symbol.getName()) + .methodName("constructor") + .build() + .toString()) + .type(METHOD_TYPE) + .build()); + + shapeLinkList.add(ShapeLink.builder() + .file(file) + .id(baseId.toBuilder().appendToPackageName(symbol.getName()).methodName("destroy").build().toString()) + .type(METHOD_TYPE) + .build()); + + // Additional ShapeLink from unstructured ServiceGenerator. + String fileName = symbol.getName().replaceAll("Client", ""); + serviceSymbolName = fileName; + shapeLinkList.add(ShapeLink.builder() + .file("./" + fileName + ".ts") + .id(TypeScriptTracingId.builder() + .packageName(BASE_PACKAGE) + .appendToPackageName(fileName) + .build() + .toString()) + .type(TYPE_TYPE) + .build()); + + return ListUtils.copyOf(shapeLinkList); + } + + private TypeScriptTracingId getBaseIdWithName(Symbol symbol) { + return getBaseIdWithoutName(symbol).toBuilder() + .appendToPackageName(symbol.getName()) + .build(); + } + + private TypeScriptTracingId getBaseIdWithoutName(Symbol symbol) { + return TypeScriptTracingId.builder() + .packageName(BASE_PACKAGE) + .appendToPackageName(getPackageNameAddition(symbol)) + .build(); + } + + private String getFile(Symbol symbol) { + String file = symbol.getDefinitionFile(); + if (file.equals("")) { + file = null; + } + return file; + } + + private String getPackageNameAddition(Symbol symbol) { + String namespace = symbol.getNamespace(); + if (namespace.length() == 0) { + namespace = symbol.expectProperty("traceFileNamespace").toString(); + } + + String namespaceDelimiter = symbol.getNamespaceDelimiter(); + if (namespaceDelimiter.length() == 0) { + namespaceDelimiter = symbol.expectProperty("traceFileNamespaceDelimiter").toString(); + } + + return namespace.replace(".", "") + .replace(namespaceDelimiter, ".") + .replaceFirst("\\.", ""); + } +} diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptTracingId.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptTracingId.java new file mode 100644 index 00000000000..f88428b8e79 --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptTracingId.java @@ -0,0 +1,135 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.typescript.codegen; + +import java.util.Objects; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Class that represents ShapeLink ids for TypeScript. IDs are similar to Java naming conventions: + * packageName.filename.ClassName#methodName or packageName.filename.ClassName$fieldName. ClassName + * is the name of an interface, type, class or namespace that is exported in a TypeScript file. + */ +public class TypeScriptTracingId implements ToSmithyBuilder { + private String packageName; + private String methodName; + private String fieldName; + + public TypeScriptTracingId(Builder builder) { + packageName = builder.packageName; + methodName = builder.methodName; + fieldName = builder.fieldName; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Parses TypeScriptTracingId from a string. + * @param tracingId Id as a string to parse. + * @return TypeScriptTracingId representation of the string. + */ + public static TypeScriptTracingId fromString(String tracingId) { + String fieldName = null; + String methodName = null; + String packageName; + + String[] fieldSplit = tracingId.split("\\$"); + String[] methodSplit = tracingId.split("#"); + + if (fieldSplit.length > 1) { + fieldName = fieldSplit[1]; + packageName = fieldSplit[0]; + } else if (methodSplit.length > 1) { + methodName = methodSplit[1]; + packageName = methodSplit[0]; + } else { + packageName = tracingId; + } + + return builder().packageName(packageName).fieldName(fieldName).methodName(methodName).build(); + } + + public String toString() { + StringBuilder builder = new StringBuilder() + .append(packageName); + + if (Objects.nonNull(methodName)) { + builder.append("#").append(methodName); + } else if (Objects.nonNull(fieldName)) { + builder.append("$").append(fieldName); + } + + return builder.toString(); + } + + public String getPackageName() { + return packageName; + } + + public String getMethodName() { + return methodName; + } + + public String getFieldName() { + return fieldName; + } + + public Builder toBuilder() { + return builder() + .packageName(packageName) + .methodName(methodName); + } + + /** + * Builder for TypeScriptTracingId. + */ + public static final class Builder implements SmithyBuilder { + private String packageName = ""; + private String methodName; + private String fieldName; + + public TypeScriptTracingId build() { + return new TypeScriptTracingId(this); + } + + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + public Builder methodName(String methodName) { + this.methodName = methodName; + return this; + } + + public Builder fieldName(String fieldName) { + this.fieldName = fieldName; + return this; + } + + public Builder appendToPackageName(String packageAddition) { + if (this.packageName.length() == 0) { + this.packageName = packageAddition; + } else { + this.packageName += "." + packageAddition; + } + return this; + } + } +} diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/TypeScriptTracingIdTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/TypeScriptTracingIdTest.java new file mode 100644 index 00000000000..d6bcfe5efed --- /dev/null +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/TypeScriptTracingIdTest.java @@ -0,0 +1,62 @@ +package software.amazon.smithy.typescript.codegen; + +import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + + +public class TypeScriptTracingIdTest { + @Test + public void parsesTracingIdFromString() { + TypeScriptTracingId methodTracingId = TypeScriptTracingId.fromString("foo.Bar#method"); + TypeScriptTracingId fieldTracingId = TypeScriptTracingId.fromString("foo.Bar$field"); + TypeScriptTracingId classTracingId = TypeScriptTracingId.fromString("foo.Bar"); + + TypeScriptTracingId expectedMethodId = TypeScriptTracingId.builder() + .appendToPackageName("foo.Bar") + .methodName("method") + .build(); + + TypeScriptTracingId expectedFieldId = TypeScriptTracingId.builder() + .appendToPackageName("foo.Bar") + .fieldName("field") + .build(); + + TypeScriptTracingId expectedClassId = TypeScriptTracingId.builder() + .appendToPackageName("foo.Bar") + .build(); + + assertThat(methodTracingId.getFieldName(), equalTo(expectedMethodId.getFieldName())); + assertThat(methodTracingId.getMethodName(), equalTo(expectedMethodId.getMethodName())); + assertThat(methodTracingId.getPackageName(), equalTo(expectedMethodId.getPackageName())); + + assertThat(fieldTracingId.getFieldName(), equalTo(expectedFieldId.getFieldName())); + assertThat(fieldTracingId.getMethodName(), equalTo(expectedFieldId.getMethodName())); + assertThat(fieldTracingId.getPackageName(), equalTo(expectedFieldId.getPackageName())); + + assertThat(classTracingId.getFieldName(), equalTo(expectedClassId.getFieldName())); + assertThat(classTracingId.getMethodName(), equalTo(expectedClassId.getMethodName())); + assertThat(classTracingId.getPackageName(), equalTo(expectedClassId.getPackageName())); + } + + @Test + public void convertsTracingIdToString() { + TypeScriptTracingId methodId = TypeScriptTracingId.builder() + .appendToPackageName("foo.Bar") + .methodName("method") + .build(); + + TypeScriptTracingId fieldId = TypeScriptTracingId.builder() + .appendToPackageName("foo.Bar") + .fieldName("field") + .build(); + + TypeScriptTracingId classId = TypeScriptTracingId.builder() + .appendToPackageName("foo.Bar") + .build(); + + assertThat(methodId.toString(), equalTo("foo.Bar#method")); + assertThat(fieldId.toString(), equalTo("foo.Bar$field")); + assertThat(classId.toString(), equalTo("foo.Bar")); + } +} diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/TypeScriptTracingTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/TypeScriptTracingTest.java new file mode 100644 index 00000000000..a18d94389a9 --- /dev/null +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/TypeScriptTracingTest.java @@ -0,0 +1,258 @@ +package software.amazon.smithy.typescript.codegen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.codegen.core.trace.ShapeLink; +import software.amazon.smithy.codegen.core.trace.TraceFile; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.traits.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.utils.ListUtils; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; + + +public class TypeScriptTracingTest { + @Test + public void includesAllExportStatementsInCommands() { + String modelFile = "output-structure.smithy"; + String tsFileName = "/commands/GetFooCommand.ts"; + String baseId = "software.amazon.awssdk.services.commands.GetFooCommand."; + String shapeId = "smithy.example#GetFoo"; + + testFromExports(modelFile, tsFileName, baseId, ListUtils.of(shapeId)); + } + + @Test + public void includesAllExportStatementsInModels() { + String modelFile = "output-structure.smithy"; + String tsFileName = "/models/models_0.ts"; + String baseId = "software.amazon.awssdk.services.models.models_0."; + List shapeIds = Arrays.asList("smithy.example#GetFooInput", "smithy.example#GetFooOutput", + "smithy.example#GetFooError"); + + testFromExports(modelFile, tsFileName, baseId, shapeIds); + } + + @Test + public void includesAllExportStatementsInService() { + String modelFile = "output-structure.smithy"; + String tsFileName = "/Example.ts"; + String baseId = "software.amazon.awssdk.services."; + List shapeIds = Collections.singletonList("smithy.example#Example"); + + testFromExports(modelFile, tsFileName, baseId, shapeIds); + } + + @Test + public void includesAllExportStatementsInServiceClient() { + String modelFile = "output-structure.smithy"; + String tsFileName = "/ExampleClient.ts"; + String baseId = "software.amazon.awssdk.services.ExampleClient."; + List shapeIds = Collections.singletonList("smithy.example#Example"); + + testFromExports(modelFile, tsFileName, baseId, shapeIds); + } + + @Test + public void addsOperationShapeLinks() { + TraceFile traceFile = getGeneratedTraceFile(getModel("output-structure.smithy")); + List shapeLinks = traceFile.getShapes().get(ShapeId.from("smithy.example#GetFoo")); + List shapeLinkIds = shapeLinks.stream().map(ShapeLink::getId).collect(Collectors.toList()); + String serviceBaseId = "software.amazon.awssdk.services."; + String commandBaseId = serviceBaseId + "commands.GetFooCommand"; + assertThat(shapeLinkIds, containsInAnyOrder(commandBaseId + ".GetFooCommand", + commandBaseId + ".GetFooCommandInput", + commandBaseId + ".GetFooCommandOutput", + commandBaseId + ".GetFooCommand#constructor", + commandBaseId + ".GetFooCommand#serialize", + commandBaseId + ".GetFooCommand#deserialize", + commandBaseId + ".GetFooCommand#resolveMiddleware", + serviceBaseId + "Example#getFoo")); + } + + @Test + public void addsUnionShapeLinks() { + TraceFile traceFile = getGeneratedTraceFile(getModel("simple-service-with-union.smithy")); + List shapeLinks = traceFile.getShapes().get(ShapeId.from("smithy.example#MyUnion")); + List shapeLinkIds = shapeLinks.stream().map(ShapeLink::getId).collect(Collectors.toList()); + String unionBaseId = "software.amazon.awssdk.services.models.models_0.MyUnion"; + assertThat(shapeLinkIds, containsInAnyOrder(unionBaseId, unionBaseId + ".Visitor", + unionBaseId + ".Visitor$stringA", + unionBaseId + ".Visitor$stringB", + unionBaseId + ".Visitor$value", + unionBaseId + "#visit")); + } + + @Test + public void addsStructureShapeLinks() { + TraceFile traceFile = getGeneratedTraceFile(getModel("output-structure.smithy")); + List shapeLinks = traceFile.getShapes().get(ShapeId.from("smithy.example#GetFooInput")); + List shapeLinkIds = shapeLinks.stream().map(ShapeLink::getId).collect(Collectors.toList()); + String structureBaseId = "software.amazon.awssdk.services.models.models_0.GetFooInput"; + assertThat(shapeLinkIds, containsInAnyOrder(structureBaseId, structureBaseId + "#isa", + structureBaseId + "#filterSensitiveLog")); + } + + @Test + public void addsEnumShapeLinks() { + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder().value("FOO").name("FOO").build()) + .addEnum(EnumDefinition.builder().value("BAR").name("BAR").build()) + .build(); + StringShape stringShape = StringShape.builder().id("smithy.example#Baz").addTrait(trait).build(); + ServiceShape serviceShape = ServiceShape.builder().id("smithy.example#Example") + .addResource("smithy.example#Baz") + .version("1.0") + .build(); + Model model = Model.builder().addShape(stringShape).addShape(serviceShape).build(); + TraceFile traceFile = getGeneratedTraceFile(model); + + List shapeLinks = traceFile.getShapes().get(ShapeId.from("smithy.example#Baz")); + List shapeLinkIds = shapeLinks.stream().map(ShapeLink::getId).collect(Collectors.toList()); + + String structureBaseId = "software.amazon.awssdk.services.models.index.GetFooInput"; + + assertThat(shapeLinkIds, contains("software.amazon.awssdk.services.models.models_0.Baz")); + } + + @Test + public void addsServiceShapeLinks() { + TraceFile traceFile = getGeneratedTraceFile(getModel("simple-service.smithy")); + List shapeLinks = traceFile.getShapes().get(ShapeId.from("smithy.example#Example")); + List shapeLinkIds = shapeLinks.stream().map(ShapeLink::getId).collect(Collectors.toList()); + String serviceBaseId = "software.amazon.awssdk.services.Example"; + String serviceClientBaseId = serviceBaseId + "Client."; + assertThat(shapeLinkIds, containsInAnyOrder(serviceClientBaseId + "ServiceInputTypes", + serviceClientBaseId + "ServiceOutputTypes", + serviceClientBaseId + "ClientDefaults", + serviceClientBaseId + "ExampleClientConfig", + serviceClientBaseId + "ExampleClientResolvedConfig", + serviceClientBaseId + "ExampleClient", + serviceClientBaseId + "ExampleClient$config", + serviceClientBaseId + "ExampleClient#constructor", + serviceClientBaseId + "ExampleClient#destroy", + serviceBaseId)); + } + + @Test + public void addsUnionMemberShapeLinks() { + TraceFile traceFile = getGeneratedTraceFile(getModel("simple-service-with-union.smithy")); + List stringShapeLinks = traceFile.getShapes().get(ShapeId.from("smithy.example#MyUnion$stringA")); + List valueShapeLinks = traceFile.getShapes().get(ShapeId.from("smithy.example#MyUnion$value")); + + String stringShapeId = stringShapeLinks.get(0).getId(); + String valueShapeId = valueShapeLinks.get(0).getId(); + + String unionBaseId = "software.amazon.awssdk.services.models.models_0.MyUnion."; + assertThat(stringShapeId, equalTo(unionBaseId + "StringAMember")); + assertThat(valueShapeId, equalTo(unionBaseId + "ValueMember")); + } + + @Test + public void addsNonUnionMemberShapeLinks() { + TraceFile traceFile = getGeneratedTraceFile(getModel("test-insensitive-simple-shape.smithy")); + String firstNameShapeId = traceFile.getShapes() + .get(ShapeId.from("smithy.example#GetFooInput$firstname")) + .get(0) + .getId(); + String lastNameShapeId = traceFile.getShapes() + .get(ShapeId.from("smithy.example#GetFooInput$lastname")) + .get(0) + .getId(); + + String memberBaseId = "software.amazon.awssdk.services.models.models_0.GetFooInput$"; + assertThat(firstNameShapeId, equalTo(memberBaseId + "firstname")); + assertThat(lastNameShapeId, equalTo(memberBaseId + "lastname")); + } + + private TraceFile getGeneratedTraceFile(Model model) { + MockManifest manifest = mockCodegen(model); + + // Get generated trace file as string and convert to node. + String contents = manifest.getFileString("/example.trace.json").get(); + ObjectNode node = Node.parse(contents).expectObjectNode(); + + return TraceFile.fromNode(node); + } + + private MockManifest mockCodegen(Model model) { + MockManifest manifest = new MockManifest(); + + PluginContext context = PluginContext.builder() + .model(model) + .fileManifest(manifest) + .settings(Node.objectNodeBuilder() + .withMember("service", Node.from("smithy.example#Example")) + .withMember("package", Node.from("example")) + .withMember("packageVersion", Node.from("1.0.0")) + .build()) + .build(); + + new TypeScriptCodegenPlugin().execute(context); + return manifest; + } + + private Model getModel(String file) { + return Model.assembler() + .addImport(getClass().getResource(file)) + .assemble() + .unwrap(); + } + + private void testFromExports(String modelFileName, String tsFileName, String baseId, List shapeIds) { + Model model = getModel(modelFileName); + MockManifest manifest = mockCodegen(model); + TraceFile traceFile = getGeneratedTraceFile(model); + + String contents = manifest.getFileString(tsFileName).get(); + List exportNames = new ArrayList<>(); + // Split by windows or unix newlines. + String[] lines = contents.split("\\r?\\n"); + + for (String line : lines) { + if (line.startsWith("export") && !(line.startsWith("export namespace"))) { + String[] words = line.split(" "); + if (words.length > 2) { + exportNames.add(words[2]); + } + } + } + + // Expected shapeLinkIds. + List expectedShapeLinkIds = exportNames.stream() + .distinct() + .map(name -> baseId + name) + .collect(Collectors.toList()); + + // Getting shapeLinkIds in TraceFile, only including shapeLinkIds in the GetFooCommand file. + List shapeLinks = new ArrayList<>(); + for (String shapeId : shapeIds) { + shapeLinks.addAll(traceFile.getShapes().get(ShapeId.from(shapeId))); + } + + List shapeLinkIds = shapeLinks.stream() + .filter(shapeLink -> shapeLink.getFile().get().equals("." + tsFileName)) + .map(ShapeLink::getId) + .filter(id -> !id.contains("#")) + .filter(id -> !id.contains("$")) + .collect(Collectors.toList()); + + assertThat(expectedShapeLinkIds, containsInAnyOrder(shapeLinkIds.toArray())); + } + +} diff --git a/smithy-typescript-codegen/src/test/resources/software/amazon/smithy/typescript/codegen/simple-service-with-union.smithy b/smithy-typescript-codegen/src/test/resources/software/amazon/smithy/typescript/codegen/simple-service-with-union.smithy new file mode 100644 index 00000000000..3a5ac57ccbf --- /dev/null +++ b/smithy-typescript-codegen/src/test/resources/software/amazon/smithy/typescript/codegen/simple-service-with-union.smithy @@ -0,0 +1,22 @@ +namespace smithy.example + +service Example { + version: "1.0.0", + operations: [GetFoo], +} + +operation GetFoo { + input: GetFooInput, + output: GetFooOutput, +} + +structure GetFooInput { + union: MyUnion, +} +structure GetFooOutput {} + +union MyUnion { + value: Integer, + stringA: String, + stringB: String, +}