Skip to content

Support for framework exceptions #295

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 2 commits into from
Mar 29, 2021
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 @@ -329,6 +329,7 @@ public Void serviceShape(ServiceShape shape) {
context.withSymbolProvider(serverSymbolProvider);
protocolGenerator.generateRequestDeserializers(serverContext);
protocolGenerator.generateResponseSerializers(serverContext);
protocolGenerator.generateFrameworkErrorSerializer(serverContext);
writers.useShapeWriter(shape, serverSymbolProvider, w -> {
protocolGenerator.generateHandlerFactory(serverContext.withWriter(w));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 software.amazon.smithy.model.Model;

public enum FrameworkErrorModel {

INSTANCE;

private final Model model = Model.assembler()
.addImport(FrameworkErrorModel.class.getResource("framework-errors.smithy"))
.assemble()
.unwrap();

public Model getModel() {
return model;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ static void generateServiceHandler(SymbolProvider symbolProvider,
writer.addImport("ServiceHandler", null, "@aws-smithy/server-common");
writer.addImport("Mux", null, "@aws-smithy/server-common");
writer.addImport("OperationSerializer", null, "@aws-smithy/server-common");
writer.addImport("UnknownOperationException", null, "@aws-smithy/server-common");
writer.addImport("InternalFailureException", null, "@aws-smithy/server-common");
writer.addImport("SerializationException", null, "@aws-smithy/server-common");
writer.addImport("SmithyFrameworkException", null, "@aws-smithy/server-common");
writer.addImport("SerdeContext", null, "@aws-sdk/types");
writer.addImport("NodeHttpHandler", null, "@aws-sdk/node-http-handler");
writer.addImport("streamCollector", null, "@aws-sdk/node-http-handler");
writer.addImport("fromBase64", null, "@aws-sdk/util-base64-node");
Expand All @@ -68,6 +73,8 @@ static void generateServiceHandler(SymbolProvider symbolProvider,
writer.write("private mux: Mux<$S, $T>;", serviceShape.getId().getName(), operationsType);
writer.write("private serializerFactory: <T extends $T>(operation: T) => "
+ "OperationSerializer<$T, T, SmithyException>;", operationsType, serviceSymbol);
writer.write("private serializeFrameworkException: (e: SmithyFrameworkException, "
+ "ctx: Omit<SerdeContext, 'endpoint'>) => Promise<HttpResponse>;");
writer.openBlock("private serdeContextBase = {", "};", () -> {
writer.write("base64Encoder: toBase64,");
writer.write("base64Decoder: fromBase64,");
Expand All @@ -87,51 +94,66 @@ static void generateServiceHandler(SymbolProvider symbolProvider,
writer.write("operation in $T that ", serviceSymbol);
writer.writeInline(" ")
.write("handles deserialization of requests and serialization of responses");
writer.write("@param serializeFrameworkException A function that can serialize "
+ "{@link SmithyFrameworkException}s");
});
writer.openBlock("constructor(service: $1T, "
+ "mux: Mux<$3S, $2T>, "
+ "serializerFactory: <T extends $2T>(op: T) => "
+ "OperationSerializer<$1T, T, SmithyException>) {", "}",
serviceSymbol, operationsType, serviceShape.getId().getName(), () -> {
writer.write("this.service = service;");
writer.write("this.mux = mux;");
writer.write("this.serializerFactory = serializerFactory;");
writer.openBlock("constructor(", ") {", () -> {
writer.write("service: $T,", serviceSymbol);
writer.write("mux: Mux<$S, $T>,", serviceShape.getId().getName(), operationsType);
writer.write("serializerFactory:<T extends $T>(op: T) => OperationSerializer<$T, T, SmithyException>,",
operationsType, serviceSymbol);
writer.write("serializeFrameworkException: (e: SmithyFrameworkException, ctx: Omit<SerdeContext, "
+ "'endpoint'>) => Promise<HttpResponse>");
});
writer.indent();
writer.write("this.service = service;");
writer.write("this.mux = mux;");
writer.write("this.serializerFactory = serializerFactory;");
writer.write("this.serializeFrameworkException = serializeFrameworkException;");
writer.closeBlock("}");
writer.openBlock("async handle(request: HttpRequest): Promise<HttpResponse> {", "}", () -> {
writer.write("const target = this.mux.match(request);");
writer.openBlock("if (target === undefined) {", "}", () -> {
writer.write("throw new Error(`Could not match any operation to $${request.method} "
+ "$${request.path} $${JSON.stringify(request.query)}`);");
writer.write("return serializeFrameworkException(new UnknownOperationException(), "
+ "this.serdeContextBase);");
});
writer.openBlock("switch (target.operation) {", "}", () -> {
for (OperationShape operation : operations) {
generateHandlerCase(writer, serviceSymbol, operation, symbolProvider.toSymbol(operation));
generateHandlerCase(writer, operation, symbolProvider.toSymbol(operation));
}
});
});
});
}

private static void generateHandlerCase(TypeScriptWriter writer,
Symbol serviceSymbol,
Shape operationShape,
Symbol operationSymbol) {
String opName = operationShape.getId().getName();
writer.openBlock("case $S : {", "}", opName, () -> {
writer.write("let serializer = this.serializerFactory($S);", opName);
writer.openBlock("try {", "} catch(error: unknown) {", () -> {
writer.openBlock("let input = await serializer.deserialize(request, {", "});", () -> {
writer.write("let input;");
writer.openBlock("try {", "} catch (error: unknown) {", () -> {
writer.openBlock("input = await serializer.deserialize(request, {", "});", () -> {
writer.write("endpoint: () => Promise.resolve(request), ...this.serdeContextBase");
});
writer.write("let output = this.service.$L(input, request);", operationSymbol.getName());
});
writer.indent();
writer.write("return this.serializeFrameworkException(new SerializationException(), "
+ "this.serdeContextBase);");
writer.closeBlock("}");
writer.openBlock("try {", "} catch(error: unknown) {", () -> {
writer.write("let output = await this.service.$L(input, request);", operationSymbol.getName());
writer.write("return serializer.serialize(output, this.serdeContextBase);");
});
writer.openBlock("", "}", () -> {
writer.openBlock("if (serializer.isOperationError(error)) {", "}", () -> {
writer.write("return serializer.serializeError(error, this.serdeContextBase);");
});
writer.write("throw error;");
writer.indent();
writer.openBlock("if (serializer.isOperationError(error)) {", "}", () -> {
writer.write("return serializer.serializeError(error, this.serdeContextBase);");
});
writer.write("console.log('Received an unexpected error', error);");
writer.write("return this.serializeFrameworkException(new InternalFailureException(), "
+ "this.serdeContextBase);");
writer.closeBlock("}");
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
import software.amazon.smithy.typescript.codegen.ApplicationProtocol;
import software.amazon.smithy.typescript.codegen.CodegenUtils;
import software.amazon.smithy.typescript.codegen.FrameworkErrorModel;
import software.amazon.smithy.typescript.codegen.TypeScriptDependency;
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
import software.amazon.smithy.utils.ListUtils;
Expand Down Expand Up @@ -216,6 +217,37 @@ public void generateResponseSerializers(GenerationContext context) {
}
}

@Override
public void generateFrameworkErrorSerializer(GenerationContext inputContext) {
final GenerationContext context = inputContext.copy();
context.setModel(FrameworkErrorModel.INSTANCE.getModel());

SymbolReference responseType = getApplicationProtocol().getResponseType();
HttpBindingIndex bindingIndex = HttpBindingIndex.of(context.getModel());
TypeScriptWriter writer = context.getWriter();

writer.addImport("SmithyFrameworkException", "__SmithyFrameworkException", "@aws-smithy/server-common");
writer.addUseImports(responseType);

writer.openBlock("export const serializeFrameworkException = async(\n"
+ " input: __SmithyFrameworkException,\n"
+ " ctx: Omit<__SerdeContext, 'endpoint'>\n"
+ "): Promise<$T> => {", "}", responseType, () -> {

writeEmptyEndpoint(context);

writer.openBlock("switch (input.name) {", "}", () -> {
for (final Shape shape : context.getModel().getShapesWithTrait(HttpErrorTrait.class)) {
StructureShape errorShape = shape.asStructureShape().orElseThrow(IllegalArgumentException::new);
writer.openBlock("case $S: {", "}", errorShape.getId().getName(), () -> {
generateErrorSerializationImplementation(context, errorShape, responseType, bindingIndex);
});
}
});
});
writer.write("");
}

private void generateMux(GenerationContext context) {
TopDownIndex topDownIndex = TopDownIndex.of(context.getModel());
TypeScriptWriter writer = context.getWriter();
Expand Down Expand Up @@ -295,6 +327,9 @@ public void generateHandlerFactory(GenerationContext context) {
Set<OperationShape> operations = index.getContainedOperations(context.getService());
SymbolProvider symbolProvider = context.getSymbolProvider();

writer.addImport("serializeFrameworkException", null,
"./protocols/" + ProtocolGenerator.getSanitizedName(getName()));

Symbol serviceSymbol = symbolProvider.toSymbol(context.getService());
Symbol handlerSymbol = serviceSymbol.expectProperty("handler", Symbol.class);
Symbol operationsSymbol = serviceSymbol.expectProperty("operations", Symbol.class);
Expand All @@ -311,7 +346,7 @@ public void generateHandlerFactory(GenerationContext context) {
.forEach(writeOperationCase(writer, symbolProvider));
});
});
writer.write("return new $T(service, mux, serFn);", handlerSymbol);
writer.write("return new $T(service, mux, serFn, serializeFrameworkException);", handlerSymbol);
});
}

Expand Down Expand Up @@ -402,25 +437,33 @@ private void generateErrorSerializer(GenerationContext context, StructureShape e
+ " ctx: Omit<__SerdeContext, 'endpoint'>\n"
+ "): Promise<$T> => {", "}", methodName, symbol, responseType, () -> {
writeEmptyEndpoint(context);
writeErrorStatusCode(context, error);
writeResponseHeaders(context, error, bindingIndex, () -> writeDefaultErrorHeaders(context, error));
generateErrorSerializationImplementation(context, error, responseType, bindingIndex);
});
writer.write("");
}

List<HttpBinding> bodyBindings = writeResponseBody(context, error, bindingIndex);
if (!bodyBindings.isEmpty()) {
// Track all shapes bound to the body so their serializers may be generated.
bodyBindings.stream()
.map(HttpBinding::getMember)
.map(member -> context.getModel().expectShape(member.getTarget()))
.forEach(serializingDocumentShapes::add);
}
private void generateErrorSerializationImplementation(GenerationContext context,
StructureShape error,
SymbolReference responseType,
HttpBindingIndex bindingIndex) {
TypeScriptWriter writer = context.getWriter();
writeErrorStatusCode(context, error);
writeResponseHeaders(context, error, bindingIndex, () -> writeDefaultErrorHeaders(context, error));

List<HttpBinding> bodyBindings = writeResponseBody(context, error, bindingIndex);
if (!bodyBindings.isEmpty()) {
// Track all shapes bound to the body so their serializers may be generated.
bodyBindings.stream()
.map(HttpBinding::getMember)
.map(member -> context.getModel().expectShape(member.getTarget()))
.forEach(serializingDocumentShapes::add);
}

writer.openBlock("return new $T({", "});", responseType, () -> {
writer.write("headers,");
writer.write("body,");
writer.write("statusCode,");
});
writer.openBlock("return new $T({", "});", responseType, () -> {
writer.write("headers,");
writer.write("body,");
writer.write("statusCode,");
});
writer.write("");
}

private void writeEmptyEndpoint(GenerationContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ public void generateRequestSerializers(GenerationContext context) {
}
}

@Override
public void generateFrameworkErrorSerializer(GenerationContext serverContext) {
LOGGER.warning("Framework error serialization is not currently supported for RPC protocols.");
}

@Override
public void generateRequestDeserializers(GenerationContext context) {
LOGGER.warning("Request deserialization is not currently supported for RPC protocols.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ default void generateSharedComponents(GenerationContext context) {
*/
void generateResponseSerializers(GenerationContext context);

/**
* Generates the code used to serialize unmodeled errors for servers.
*
* @param serverContext Serialization context.
*/
void generateFrameworkErrorSerializer(GenerationContext serverContext);

/**
* Generates the code used to determine the service and operation
* targeted by a given request.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace smithy.framework

@error("server")
@httpError(500)
structure InternalFailure {}

@error("client")
@httpError(404)
structure UnknownOperationException {}

@error("client")
@httpError(400)
structure SerializationException {}