Skip to content

Use server symbol provider in protocol test gen #284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import software.amazon.smithy.codegen.core.Symbol;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.EventStreamIndex;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.traits.StreamingTrait;

/**
* Utility methods needed across Java packages.
Expand Down Expand Up @@ -91,4 +97,34 @@ private static List<String> getDefaultOperationSerdeContextTypes(TypeScriptWrite
contextInterfaceList.add("__SerdeContext");
return contextInterfaceList;
}

static List<MemberShape> getBlobStreamingMembers(Model model, StructureShape shape) {
return shape.getAllMembers().values().stream()
.filter(memberShape -> {
// Streaming blobs need to have their types modified
// See `writeStreamingInputType`
Shape target = model.expectShape(memberShape.getTarget());
return target.isBlobShape() && target.hasTrait(StreamingTrait.class);
})
.collect(Collectors.toList());
}

/**
* Ease the input streaming member restriction so that users don't need to construct a stream every time.
* This type decoration is allowed in Smithy because it makes input type more permissive than output type
* for the same member.
* Refer here for more rationales: https://github.com/aws/aws-sdk-js-v3/issues/843
*/
static void writeStreamingMemberType(
TypeScriptWriter writer,
Symbol containerSymbol,
String typeName,
MemberShape streamingMember
) {
String memberName = streamingMember.getMemberName();
String optionalSuffix = streamingMember.isRequired() ? "" : "?";
writer.openBlock("export type $L = Omit<$T, $S> & {", "};", typeName, containerSymbol, memberName, () ->
writer.write("$1L$2L: $3T[$1S]|string|Uint8Array|Buffer;",
memberName, optionalSuffix, containerSymbol));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ void execute() {
// Generate protocol tests IFF found in the model.
if (protocolGenerator != null) {
ShapeId protocol = protocolGenerator.getProtocol();
new HttpProtocolTestGenerator(settings, model, protocol, symbolProvider, writers, protocolGenerator).run();
new HttpProtocolTestGenerator(
settings, model, protocol, symbolProvider, serverSymbolProvider, writers, protocolGenerator).run();
}

// Write each pending writer.
Expand Down Expand Up @@ -406,9 +407,8 @@ private void generateCommands(ServiceShape shape) {
}

if (settings.generateServerSdk()) {
writers.useShapeWriter(operation, serverSymbolProvider, commandWriter -> new CommandGenerator(
settings, model, operation, serverSymbolProvider, commandWriter,
runtimePlugins, protocolGenerator, applicationProtocol).run());
writers.useShapeWriter(operation, serverSymbolProvider, commandWriter -> new ServerCommandGenerator(
settings, model, operation, serverSymbolProvider, commandWriter).run());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

package software.amazon.smithy.typescript.codegen;

import static software.amazon.smithy.typescript.codegen.CodegenUtils.getBlobStreamingMembers;
import static software.amazon.smithy.typescript.codegen.CodegenUtils.writeStreamingMemberType;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
Expand All @@ -25,9 +28,7 @@
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.StructureShape;
import software.amazon.smithy.model.traits.StreamingTrait;
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator;
import software.amazon.smithy.typescript.codegen.integration.RuntimeClientPlugin;
import software.amazon.smithy.utils.OptionalUtils;
Expand Down Expand Up @@ -86,9 +87,7 @@ final class CommandGenerator implements Runnable {
@Override
public void run() {
addInputAndOutputTypes();
if (settings.generateClient()) {
generateClientCommand();
}
generateClientCommand();
}

private void generateClientCommand() {
Expand Down Expand Up @@ -195,11 +194,11 @@ private void addInputAndOutputTypes() {
private void writeInputType(String typeName, Optional<StructureShape> inputShape) {
if (inputShape.isPresent()) {
StructureShape input = inputShape.get();
List<MemberShape> blobStreamingMembers = getBlobStreamingMembers(input);
List<MemberShape> blobStreamingMembers = getBlobStreamingMembers(model, input);
if (blobStreamingMembers.isEmpty()) {
writer.write("export type $L = $T;", typeName, symbolProvider.toSymbol(input));
} else {
writeStreamingInputType(typeName, input, blobStreamingMembers.get(0));
writeStreamingMemberType(writer, symbolProvider.toSymbol(input), typeName, blobStreamingMembers.get(0));
}
} else {
// If the input is non-existent, then use an empty object.
Expand All @@ -219,31 +218,6 @@ private void writeOutputType(String typeName, Optional<StructureShape> outputSha
}
}

private List<MemberShape> getBlobStreamingMembers(StructureShape shape) {
return shape.getAllMembers().values().stream()
.filter(memberShape -> {
// Streaming blobs need to have their types modified
// See `writeStreamingInputType`
Shape target = model.expectShape(memberShape.getTarget());
return target.isBlobShape() && target.hasTrait(StreamingTrait.class);
})
.collect(Collectors.toList());
}

/**
* Ease the input streaming member restriction so that users don't need to construct a stream every time.
* This type decoration is allowed in Smithy because it makes input type more permissive than output type
* for the same member.
* Refer here for more rationales: https://github.com/aws/aws-sdk-js-v3/issues/843
*/
private void writeStreamingInputType(String typeName, StructureShape inputShape, MemberShape streamingMember) {
Symbol inputSymbol = symbolProvider.toSymbol(inputShape);
String memberName = streamingMember.getMemberName();
String optionalSuffix = streamingMember.isRequired() ? "" : "?";
writer.openBlock("export type $L = Omit<$T, $S> & {", "};", typeName, inputSymbol, memberName, () ->
writer.write("$1L$2L: $3T[$1S]|string|Uint8Array|Buffer;", memberName, optionalSuffix, inputSymbol));
}

private void addCommandSpecificPlugins() {
// Some plugins might only apply to specific commands. They are added to the
// command's middleware stack here. Plugins that apply to all commands are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
import software.amazon.smithy.utils.IoUtils;
import software.amazon.smithy.utils.MapUtils;
import software.amazon.smithy.utils.Pair;
import software.amazon.smithy.utils.StringUtils;

/**
* Generates HTTP protocol test cases to be run using Jest.
Expand All @@ -91,6 +90,7 @@ final class HttpProtocolTestGenerator implements Runnable {
private final ServiceShape service;
private final SymbolProvider symbolProvider;
private final Symbol serviceSymbol;
private final SymbolProvider serverSymbolProvider;
private final Set<String> additionalStubs = new TreeSet<>();
private final ProtocolGenerator protocolGenerator;

Expand All @@ -105,6 +105,7 @@ final class HttpProtocolTestGenerator implements Runnable {
Model model,
ShapeId protocol,
SymbolProvider symbolProvider,
SymbolProvider serverSymbolProvider,
TypeScriptDelegator delegator,
ProtocolGenerator protocolGenerator
) {
Expand All @@ -113,6 +114,7 @@ final class HttpProtocolTestGenerator implements Runnable {
this.protocol = protocol;
this.service = settings.getService(model);
this.symbolProvider = symbolProvider;
this.serverSymbolProvider = serverSymbolProvider;
this.delegator = delegator;
this.protocolGenerator = protocolGenerator;
serviceSymbol = symbolProvider.toSymbol(service);
Expand Down Expand Up @@ -247,7 +249,7 @@ private void generateClientRequestTest(OperationShape operation, HttpRequestTest
}

private void generateServerRequestTest(OperationShape operation, HttpRequestTestCase testCase) {
Symbol operationSymbol = symbolProvider.toSymbol(operation);
Symbol operationSymbol = serverSymbolProvider.toSymbol(operation);

// Lowercase all the headers we're expecting as this is what we'll get.
Map<String, String> headers = testCase.getHeaders().entrySet().stream()
Expand All @@ -260,9 +262,8 @@ private void generateServerRequestTest(OperationShape operation, HttpRequestTest
String testName = testCase.getId() + ":ServerRequest";
testCase.getDocumentation().ifPresent(writer::writeDocs);
writer.openBlock("it($S, async () => {", "});\n", testName, () -> {
// TODO: use the symbol provider when it's ready
String serviceName = StringUtils.capitalize(service.getId().getName());
String operationName = StringUtils.capitalize(operation.getId().getName());
Symbol serviceSymbol = serverSymbolProvider.toSymbol(service);
Symbol handlerSymbol = serviceSymbol.expectProperty("handler", Symbol.class);
Symbol inputType = operationSymbol.expectProperty("inputType", Symbol.class);
Symbol outputType = operationSymbol.expectProperty("outputType", Symbol.class);

Expand All @@ -274,18 +275,18 @@ private void generateServerRequestTest(OperationShape operation, HttpRequestTest
// We use a partial here so that we don't have to define the entire service, but still get the advantages
// the type checker, including excess property checking. Later on we'll use `as` to cast this to the
// full service so that we can actually use it.
writer.addImport(serviceName + "Service", null, "./server");
writer.openBlock("const testService: Partial<$LService> = {", "};", serviceName, () -> {
writer.openBlock("const testService: Partial<$T> = {", "};", serviceSymbol, () -> {
writer.addImport("Operation", "__Operation", "@aws-smithy/server-common");
writer.write("$L: testFunction as __Operation<$T, $T>,", operationName, inputType, outputType);
writer.write("$L: testFunction as __Operation<$T, $T>,",
operationSymbol.getName(), inputType, outputType);
});

String getHandlerName = String.format("get%sServiceHandler", serviceName);
String getHandlerName = "get" + handlerSymbol.getName();
writer.addImport(getHandlerName, getHandlerName,
"./protocols/" + ProtocolGenerator.getSanitizedName(protocolGenerator.getName()));

// Cast the service as any so TS will ignore the fact that the type being passed in is incomplete.
writer.write("const handler = $L(testService as $LService);", getHandlerName, serviceName);
writer.write("const handler = $L(testService as $T);", getHandlerName, serviceSymbol);

// Construct a new http request according to the test case definition.
writer.openBlock("const request = new HttpRequest({", "});", () -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ static void writeServerIndex(
TopDownIndex topDownIndex = TopDownIndex.of(model);
Set<OperationShape> containedOperations = new TreeSet<>(topDownIndex.getContainedOperations(service));
for (OperationShape operation : containedOperations) {
writer.write("export * from \"./types/$L\";", symbolProvider.toSymbol(operation).getName());
writer.write("export * from \"./operations/$L\";", symbolProvider.toSymbol(operation).getName());
}
writer.write("export * from \"./$L\"", symbol.getName());
fileManifest.writeFile("server/index.ts", writer.toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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 static software.amazon.smithy.typescript.codegen.CodegenUtils.getBlobStreamingMembers;
import static software.amazon.smithy.typescript.codegen.CodegenUtils.writeStreamingMemberType;

import java.util.List;
import java.util.Optional;
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.OperationIndex;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.StructureShape;

/**
* Generates server operation types.
*/
final class ServerCommandGenerator implements Runnable {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like the association of the term command with this at all (but no, I don't have a better idea off the top of my head)


private final TypeScriptSettings settings;
private final Model model;
private final OperationShape operation;
private final SymbolProvider symbolProvider;
private final TypeScriptWriter writer;
private final OperationIndex operationIndex;
private final Symbol inputType;
private final Symbol outputType;

ServerCommandGenerator(
TypeScriptSettings settings,
Model model,
OperationShape operation,
SymbolProvider symbolProvider,
TypeScriptWriter writer
) {
this.settings = settings;
this.model = model;
this.operation = operation;
this.symbolProvider = symbolProvider;
this.writer = writer;

Symbol operationSymbol = symbolProvider.toSymbol(operation);
operationIndex = OperationIndex.of(model);
inputType = operationSymbol.expectProperty("inputType", Symbol.class);
outputType = operationSymbol.expectProperty("outputType", Symbol.class);
}

@Override
public void run() {
addInputAndOutputTypes();
}

private void addInputAndOutputTypes() {
writeInputType(inputType.getName(), operationIndex.getInput(operation));
writeOutputType(outputType.getName(), operationIndex.getOutput(operation));
writer.write("");
}

// TODO: Flip these so that metadata is attached to input and streaming customization is attached to output.
private void writeInputType(String typeName, Optional<StructureShape> inputShape) {
if (inputShape.isPresent()) {
StructureShape input = inputShape.get();
List<MemberShape> blobStreamingMembers = getBlobStreamingMembers(model, input);
if (blobStreamingMembers.isEmpty()) {
writer.write("export type $L = $T;", typeName, symbolProvider.toSymbol(input));
} else {
writeStreamingMemberType(writer, symbolProvider.toSymbol(input), typeName, blobStreamingMembers.get(0));
}
} else {
// If the input is non-existent, then use an empty object.
writer.write("export type $L = {}", typeName);
}
}

private void writeOutputType(String typeName, Optional<StructureShape> outputShape) {
// Output types should always be MetadataBearers, possibly in addition
// to a defined output shape.
writer.addImport("MetadataBearer", "__MetadataBearer", TypeScriptDependency.AWS_SDK_TYPES.packageName);
if (outputShape.isPresent()) {
writer.write("export type $L = $T & __MetadataBearer;",
typeName, symbolProvider.toSymbol(outputShape.get()));
} else {
writer.write("export type $L = __MetadataBearer", typeName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public String formatModuleName(Shape shape, String name) {
if (shape.getType() == ShapeType.SERVICE) {
return "./server/" + name;
} else if (shape.getType() == ShapeType.OPERATION) {
return "./server/types/" + name;
return "./server/operations/" + name;
}

throw new IllegalArgumentException("Unsupported shape type: " + shape.getType());
Expand Down