Skip to content
Open
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 @@ -44,6 +44,7 @@
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.typescript.codegen.auth.http.HttpAuthSchemeProviderGenerator;
import software.amazon.smithy.typescript.codegen.endpointsV2.EndpointsV2Generator;
import software.amazon.smithy.typescript.codegen.integration.AddEventStreamDependency;
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator;
import software.amazon.smithy.typescript.codegen.integration.RuntimeClientPlugin;
import software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration;
Expand Down Expand Up @@ -258,9 +259,10 @@ public void generateService(GenerateServiceDirective<TypeScriptCodegenContext, T
}

if (settings.generateServerSdk()) {
boolean hasEventStream = AddEventStreamDependency.hasEventStream(directive.model(), service);
for (OperationShape operation : directive.operations()) {
delegator.useShapeWriter(operation, w -> {
ServerGenerator.generateOperationHandler(symbolProvider, service, operation, w);
ServerGenerator.generateOperationHandler(symbolProvider, service, operation, w, hasEventStream);
});
}
}
Expand Down Expand Up @@ -736,7 +738,13 @@ private void generateServiceInterface(
.useShapeWriter(service, writer -> {
ServerGenerator.generateOperationsType(symbolProvider, service, operations, writer);
ServerGenerator.generateServerInterfaces(symbolProvider, service, operations, writer);
ServerGenerator.generateServiceHandler(symbolProvider, service, operations, writer);
ServerGenerator.generateServiceHandler(
symbolProvider,
service,
operations,
writer,
AddEventStreamDependency.hasEventStream(directive.model(), service)
);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@ static void generateServiceHandler(
SymbolProvider symbolProvider,
Shape serviceShape,
Set<OperationShape> operations,
TypeScriptWriter writer
TypeScriptWriter writer,
boolean hasEventStream
) {
addCommonHandlerImports(writer);

Symbol serviceSymbol = symbolProvider.toSymbol(serviceShape);
Symbol handlerSymbol = serviceSymbol.expectProperty("handler", Symbol.class);
Symbol operationsType = serviceSymbol.expectProperty("operations", Symbol.class);

writeSerdeContextBase(writer);
writeSerdeContextBase(writer, hasEventStream);

writer.openBlock(
"const $LValidators: { [K in $T]: (input: any) => __ValidationFailure[] } = {",
Expand Down Expand Up @@ -211,12 +212,13 @@ static void generateOperationHandler(
SymbolProvider symbolProvider,
Shape serviceShape,
OperationShape operation,
TypeScriptWriter writer
TypeScriptWriter writer,
boolean hasEventStream
) {
addCommonHandlerImports(writer);
writer.addImport("Operation", "__Operation", TypeScriptDependency.SERVER_COMMON);

writeSerdeContextBase(writer);
writeSerdeContextBase(writer, hasEventStream);

Symbol serviceSymbol = symbolProvider.toSymbol(serviceShape);
Symbol operationSymbol = symbolProvider.toSymbol(operation);
Expand Down Expand Up @@ -670,21 +672,35 @@ private static void addCommonHandlerImports(TypeScriptWriter writer) {
writer.addImport("MetricsRecorderFactory", "__MetricsRecorderFactory", TypeScriptDependency.SMITHY_TYPES);
}

private static void writeSerdeContextBase(TypeScriptWriter writer) {
private static void writeSerdeContextBase(TypeScriptWriter writer, boolean hasEventStream) {
writer.addImport("ServerSerdeContext", "__ServerSerdeContext", TypeScriptDependency.SERVER_COMMON);
writer.addImport("NodeHttpHandler", null, TypeScriptDependency.AWS_SDK_NODE_HTTP_HANDLER);
writer.addImport("streamCollector", null, TypeScriptDependency.AWS_SDK_NODE_HTTP_HANDLER);
writer.addImportSubmodule("fromBase64", null, TypeScriptDependency.SMITHY_CORE, SmithyCoreSubmodules.SERDE);
writer.addImportSubmodule("toBase64", null, TypeScriptDependency.SMITHY_CORE, SmithyCoreSubmodules.SERDE);
writer.addImportSubmodule("fromUtf8", null, TypeScriptDependency.SMITHY_CORE, SmithyCoreSubmodules.SERDE);
writer.addImportSubmodule("toUtf8", null, TypeScriptDependency.SMITHY_CORE, SmithyCoreSubmodules.SERDE);
if (hasEventStream) {
writer.addImportSubmodule(
"eventStreamSerdeProvider",
null,
TypeScriptDependency.SMITHY_CORE,
SmithyCoreSubmodules.EVENT_STREAMS
);
}

writer.openBlock("const serdeContextBase = {", "};", () -> {
writer.write("base64Encoder: toBase64,");
writer.write("base64Decoder: fromBase64,");
writer.write("utf8Encoder: toUtf8,");
writer.write("utf8Decoder: fromUtf8,");
writer.write("streamCollector: streamCollector,");
if (hasEventStream) {
writer.write(
"eventStreamMarshaller: eventStreamSerdeProvider("
+ "{ utf8Encoder: toUtf8, utf8Decoder: fromUtf8 }),"
);
}
writer.write("requestHandler: new NodeHttpHandler(),");
writer.write("disableHostPrefix: true");
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public List<? extends CodeInterceptor<? extends CodeSection, TypeScriptWriter>>
);
}

private static boolean hasEventStream(Model model, ServiceShape service) {
public static boolean hasEventStream(Model model, ServiceShape service) {
TopDownIndex topDownIndex = TopDownIndex.of(model);
Set<OperationShape> operations = topDownIndex.getContainedOperations(service);
EventStreamIndex eventStreamIndex = EventStreamIndex.of(model);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -698,12 +698,23 @@ private void generateOperationResponseSerializer(
Symbol outputType = symbol.expectProperty("outputType", Symbol.class);
writer.addImport("ServerSerdeContext", null, TypeScriptDependency.SERVER_COMMON);

String ctxType = "ServerSerdeContext";
if (EventStreamGenerator.hasEventStreamOutput(context, operation)) {
writer.addTypeImport(
"EventStreamSerdeContext",
"__EventStreamSerdeContext",
TypeScriptDependency.SMITHY_TYPES
);
ctxType += " & __EventStreamSerdeContext";
}

writer.openBlock(
"export const $L = async (\n" + " input: $T,\n" + " ctx: ServerSerdeContext\n"
"export const $L = async (\n" + " input: $T,\n" + " ctx: $L\n"
+ "): Promise<$T> => {",
"};",
methodName,
outputType,
ctxType,
responseType,
() -> {
writeEmptyEndpoint(context, operation);
Expand Down Expand Up @@ -834,8 +845,13 @@ private void writeEmptyEndpoint(GenerationContext context, OperationShape operat
String contextType = "__SerdeContext";
boolean hasEventStreamResponse = EventStreamGenerator.hasEventStreamOutput(context, operation);
if (hasEventStreamResponse) {
// todo: unsupported SSDK feature.
contextType += "& any /*event stream context unsupported in ssdk*/";
context.getWriter()
.addTypeImport(
"EventStreamSerdeContext",
"__EventStreamSerdeContext",
TypeScriptDependency.SMITHY_TYPES
);
contextType += " & __EventStreamSerdeContext";
}
context
.getWriter()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.typescript.codegen;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;

import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.HttpBinding;
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;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
import software.amazon.smithy.typescript.codegen.integration.HttpBindingProtocolGenerator;
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext;

public class HttpBindingEventStreamServerTest {

/**
* A minimal concrete {@link HttpBindingProtocolGenerator} for exercising the shared
* server-side generation paths. Document-body serde is stubbed because event stream
* payloads are handled through the payload/event-stream path, not document bodies.
*/
private static final class TestHttpBindingProtocolGenerator extends HttpBindingProtocolGenerator {
TestHttpBindingProtocolGenerator() {
super(false);
}

@Override
public ShapeId getProtocol() {
return ShapeId.from("smithy.example#fakeProtocol");
}

@Override
public void generateProtocolTests(GenerationContext context) {}

@Override
protected Format getDocumentTimestampFormat() {
return Format.EPOCH_SECONDS;
}

@Override
protected String getDocumentContentType() {
return "application/json";
}

@Override
protected void generateDocumentBodyShapeSerializers(GenerationContext context, Set<Shape> shapes) {}

@Override
protected void generateDocumentBodyShapeDeserializers(GenerationContext context, Set<Shape> shapes) {}

@Override
protected void serializeInputDocumentBody(
GenerationContext context,
OperationShape operation,
List<HttpBinding> documentBindings
) {}

@Override
protected void serializeInputEventDocumentPayload(GenerationContext context) {}

@Override
protected void serializeOutputDocumentBody(
GenerationContext context,
OperationShape operation,
List<HttpBinding> documentBindings
) {}

@Override
protected void serializeErrorDocumentBody(
GenerationContext context,
StructureShape error,
List<HttpBinding> documentBindings
) {}

@Override
protected void writeErrorCodeParser(GenerationContext context) {}

@Override
protected void deserializeInputDocumentBody(
GenerationContext context,
OperationShape operation,
List<HttpBinding> documentBindings
) {}

@Override
protected void deserializeOutputDocumentBody(
GenerationContext context,
OperationShape operation,
List<HttpBinding> documentBindings
) {}

@Override
protected void deserializeErrorDocumentBody(
GenerationContext context,
StructureShape error,
List<HttpBinding> documentBindings
) {}

@Override
protected boolean requiresNumericEpochSecondsInPayload() {
return false;
}
}

private GenerationContext serverContext() {
Model model = Model.assembler(getClass().getClassLoader())
.discoverModels(getClass().getClassLoader())
.addImport(getClass().getResource("server-event-stream-http.smithy"))
.assemble()
.unwrap();
ServiceShape service = model.expectShape(ShapeId.from("smithy.example#Example"), ServiceShape.class);
TypeScriptSettings settings = TypeScriptSettings.from(
model,
Node.objectNodeBuilder()
.withMember("service", Node.from("smithy.example#Example"))
.withMember("package", Node.from("example-ssdk"))
.withMember("packageVersion", Node.from("1.0.0"))
.withMember("disableDefaultValidation", Node.from(true))
.build()
);
GenerationContext context = new GenerationContext();
context.setModel(model);
context.setService(service);
context.setSettings(settings);
context.setSymbolProvider(new SymbolVisitor(model, settings));
context.setProtocolName("fakeProtocol");
context.setWriter(new TypeScriptWriter("./Publish"));
return context;
}

@Test
public void responseSerializerUsesTypedEventStreamContext() {
GenerationContext context = serverContext();
new TestHttpBindingProtocolGenerator().generateResponseSerializers(context);
String generated = context.getWriter().toString();
// The response serializer's serde context must be typed with __EventStreamSerdeContext
// rather than the previous `& any` escape hatch.
assertThat(generated, containsString("ctx: ServerSerdeContext & __EventStreamSerdeContext"));
assertThat(generated, not(containsString("unsupported in ssdk")));
assertThat(generated, not(containsString("& any")));
}

@Test
public void requestDeserializerUsesTypedEventStreamContext() {
GenerationContext context = serverContext();
new TestHttpBindingProtocolGenerator().generateRequestDeserializers(context);
String generated = context.getWriter().toString();
// The request deserializer's serde context must include __EventStreamSerdeContext
// for operations with an event stream input.
assertThat(generated, containsString("__EventStreamSerdeContext"));
assertThat(generated, not(containsString("& any")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.typescript.codegen;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;

import org.junit.jupiter.api.Test;
import software.amazon.smithy.build.MockManifest;
import software.amazon.smithy.build.PluginContext;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;

public class ServerEventStreamTest {

private MockManifest generateServer() {
Model model = Model.assembler(getClass().getClassLoader())
.discoverModels(getClass().getClassLoader())
.addImport(getClass().getResource("server-event-stream.smithy"))
.assemble()
.unwrap();
MockManifest manifest = new MockManifest();
PluginContext context = PluginContext.builder()
.model(model)
.fileManifest(manifest)
.pluginClassLoader(getClass().getClassLoader())
.settings(
Node.objectNodeBuilder()
.withMember("service", Node.from("smithy.example#Example"))
.withMember("package", Node.from("example-ssdk"))
.withMember("packageVersion", Node.from("1.0.0"))
.withMember("disableDefaultValidation", Node.from(true))
.build()
)
.build();
new TypeScriptServerCodegenPlugin().execute(context);
return manifest;
}

@Test
public void wiresEventStreamMarshallerIntoServerSerdeContext() {
MockManifest manifest = generateServer();
String handler = manifest.getFileString(
CodegenUtils.SOURCE_FOLDER + "/server/operations/Publish.ts"
).get();
// The server handler's serde context base must supply an event stream marshaller,
// reusing the Node event stream serde provider (which only needs utf8 encode/decode).
assertThat(handler, containsString("eventStreamSerdeProvider"));
assertThat(handler, containsString("eventStreamMarshaller: eventStreamSerdeProvider("));
}

@Test
public void generatesEventStreamSerdeWithTypedContext() {
MockManifest manifest = generateServer();
String protocol = manifest.getFileString(
CodegenUtils.SOURCE_FOLDER + "/protocols/Rpcv2cbor.ts"
).get();
// Event stream serde functions are generated and use the strongly typed
// __EventStreamSerdeContext rather than an untyped `any` escape hatch.
assertThat(protocol, containsString("__EventStreamSerdeContext"));
assertThat(protocol, not(containsString("unsupported in ssdk")));
}
}
Loading