Skip to content

Commit 3208770

Browse files
AllanZhengYPsrchase
authored andcommitted
Update response and error response parsing (smithy-lang#66)
Currently when response body is a payload member, the SDK either keeps reponse intact if it has a streaming trait, or parse the body to JS object. Actually when response payload is not streaming, it is NOT always parsable(not a valid JSON or XML string). Specificly, when payload is String, we shouldn't parse it with XML or JSON parser; We should also treat Blob shape differently as we should encode the binaries into string; Only when shape is Union or Structure can we assume payload is parsable by JSON or XML parser. For some protocols, error type flag exists in error response body, then we need to collect response stream to JS object and parse the error type; For other protocols, error type flag doesn't exist in error response body, then we don't need to collect the response stream in error dispatcher. Instead, we can treat the error like normal response. So that error shape supports the same traits as normal responses like streaming, payload etc. This is done by add a new flag in Protocol generator-- isErrorCodeInBody. When it return true, it means error type flag exists in error response body, then body is parsed in errors dispatcher, and each error deser only need to deal with parsed response body in JS object format. When it returns false, it means error type can be inferred without touching response body, then error deser can access the error response intact.
1 parent 65a80a6 commit 3208770

File tree

3 files changed

+158
-48
lines changed

3 files changed

+158
-48
lines changed

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
import software.amazon.smithy.model.shapes.NumberShape;
4949
import software.amazon.smithy.model.shapes.OperationShape;
5050
import software.amazon.smithy.model.shapes.Shape;
51-
import software.amazon.smithy.model.shapes.ShapeType;
5251
import software.amazon.smithy.model.shapes.StringShape;
5352
import software.amazon.smithy.model.shapes.StructureShape;
5453
import software.amazon.smithy.model.shapes.TimestampShape;
@@ -72,6 +71,17 @@ public abstract class HttpBindingProtocolGenerator implements ProtocolGenerator
7271
private final Set<Shape> serializingDocumentShapes = new TreeSet<>();
7372
private final Set<Shape> deserializingDocumentShapes = new TreeSet<>();
7473
private final Set<StructureShape> deserializingErrorShapes = new TreeSet<>();
74+
private final boolean isErrorCodeInBody;
75+
76+
/**
77+
* Creates a Http binding protocol generator.
78+
*
79+
* @param isErrorCodeInBody A boolean that indicates if the error code for the implementing protocol is located in
80+
* the error response body, meaning this generator will parse the body before attempting to load an error code.
81+
*/
82+
public HttpBindingProtocolGenerator(boolean isErrorCodeInBody) {
83+
this.isErrorCodeInBody = isErrorCodeInBody;
84+
}
7585

7686
@Override
7787
public ApplicationProtocol getApplicationProtocol() {
@@ -120,6 +130,8 @@ public void generateSharedComponents(GenerationContext context) {
120130
generateDocumentBodyShapeSerializers(context, serializingDocumentShapes);
121131
generateDocumentBodyShapeDeserializers(context, deserializingDocumentShapes);
122132
HttpProtocolGeneratorUtils.generateMetadataDeserializer(context, getApplicationProtocol().getResponseType());
133+
HttpProtocolGeneratorUtils.generateCollectBody(context);
134+
HttpProtocolGeneratorUtils.generateCollectBodyString(context);
123135
}
124136

125137
/**
@@ -594,7 +606,7 @@ private void generateOperationDeserializer(
594606

595607
// Write out the error deserialization dispatcher.
596608
Set<StructureShape> errorShapes = HttpProtocolGeneratorUtils.generateErrorDispatcher(
597-
context, operation, responseType, this::writeErrorCodeParser);
609+
context, operation, responseType, this::writeErrorCodeParser, isErrorCodeInBody);
598610
deserializingErrorShapes.addAll(errorShapes);
599611
}
600612

@@ -608,9 +620,10 @@ private void generateErrorDeserializer(GenerationContext context, StructureShape
608620
context.getProtocolName()) + "Response";
609621

610622
writer.openBlock("const $L = async (\n"
611-
+ " output: any,\n"
623+
+ " $L: any,\n"
612624
+ " context: __SerdeContext\n"
613-
+ "): Promise<$T> => {", "};", errorDeserMethodName, errorSymbol, () -> {
625+
+ "): Promise<$T> => {", "};",
626+
errorDeserMethodName, isErrorCodeInBody ? "parsedOutput" : "output", errorSymbol, () -> {
614627
writer.openBlock("const contents: $T = {", "};", errorSymbol, () -> {
615628
writer.write("__type: $S,", error.getId().getName());
616629
writer.write("$$fault: $S,", error.getTrait(ErrorTrait.class).get().getValue());
@@ -621,7 +634,7 @@ private void generateErrorDeserializer(GenerationContext context, StructureShape
621634
});
622635

623636
readHeaders(context, error, bindingIndex);
624-
List<HttpBinding> documentBindings = readResponseBody(context, error, bindingIndex);
637+
List<HttpBinding> documentBindings = readErrorResponseBody(context, error, bindingIndex);
625638
// Track all shapes bound to the document so their deserializers may be generated.
626639
documentBindings.forEach(binding -> {
627640
Shape target = model.expectShape(binding.getMember().getTarget());
@@ -633,6 +646,24 @@ private void generateErrorDeserializer(GenerationContext context, StructureShape
633646
writer.write("");
634647
}
635648

649+
private List<HttpBinding> readErrorResponseBody(
650+
GenerationContext context,
651+
Shape error,
652+
HttpBindingIndex bindingIndex
653+
) {
654+
TypeScriptWriter writer = context.getWriter();
655+
if (isErrorCodeInBody) {
656+
// Body is already parsed in error dispatcher, simply assign body to data.
657+
writer.write("const data: any = output.body;");
658+
List<HttpBinding> responseBindings = bindingIndex.getResponseBindings(error, Location.DOCUMENT);
659+
responseBindings.sort(Comparator.comparing(HttpBinding::getMemberName));
660+
return responseBindings;
661+
} else {
662+
// Deserialize response body just like in normal response.
663+
return readResponseBody(context, error, bindingIndex);
664+
}
665+
}
666+
636667
private void readHeaders(
637668
GenerationContext context,
638669
Shape operationOrError,
@@ -691,42 +722,48 @@ private List<HttpBinding> readResponseBody(
691722
documentBindings.sort(Comparator.comparing(HttpBinding::getMemberName));
692723
List<HttpBinding> payloadBindings = bindingIndex.getResponseBindings(operationOrError, Location.PAYLOAD);
693724

725+
// Detect if operation output or error shape contains a streaming member.
726+
OperationIndex operationIndex = context.getModel().getKnowledge(OperationIndex.class);
727+
StructureShape operationOutputOrError = operationOrError.asStructureShape()
728+
.orElseGet(() -> operationIndex.getOutput(operationOrError).orElse(null));
729+
boolean hasStreamingComponent = Optional.ofNullable(operationOutputOrError)
730+
.map(structure -> structure.getAllMembers().values().stream()
731+
.anyMatch(memberShape -> memberShape.hasTrait(StreamingTrait.class)))
732+
.orElse(false);
733+
694734
if (!documentBindings.isEmpty()) {
695-
readReponseBodyData(context, operationOrError);
735+
// If response has document binding, the body can be parsed to JavaScript object.
736+
writer.write("const data: any = await parseBody(output.body, context);");
696737
deserializeOutputDocument(context, operationOrError, documentBindings);
697738
return documentBindings;
698739
}
699740
if (!payloadBindings.isEmpty()) {
700-
readReponseBodyData(context, operationOrError);
701741
// There can only be one payload binding.
702742
HttpBinding binding = payloadBindings.get(0);
703743
Shape target = context.getModel().expectShape(binding.getMember().getTarget());
744+
if (hasStreamingComponent) {
745+
// If payload is streaming, return raw low-level stream directly.
746+
writer.write("const data: any = output.body;");
747+
} else if (target instanceof BlobShape) {
748+
// If payload is non-streaming blob, only need to collect stream to binary data(Uint8Array).
749+
writer.write("const data: any = await collectBody(output.body, context);");
750+
} else if (target instanceof StructureShape || target instanceof UnionShape) {
751+
// If body is Structure or Union, they we need to parse the string into JavaScript object.
752+
writer.write("const data: any = await parseBody(output.body, context);");
753+
} else if (target instanceof StringShape) {
754+
// If payload is string, we need to collect body and encode binary to string.
755+
writer.write("const data: any = await collectBodyString(output.body, context);");
756+
} else {
757+
throw new CodegenException(String.format("Unexpected shape type bound to payload: `%s`",
758+
target.getType()));
759+
}
704760
writer.write("contents.$L = $L;", binding.getMemberName(), getOutputValue(context,
705761
Location.PAYLOAD, "data", binding.getMember(), target));
706762
return payloadBindings;
707763
}
708764
return ListUtils.of();
709765
}
710766

711-
private void readReponseBodyData(GenerationContext context, Shape operationOrError) {
712-
TypeScriptWriter writer = context.getWriter();
713-
// Prepare response body for deserializing.
714-
OperationIndex operationIndex = context.getModel().getKnowledge(OperationIndex.class);
715-
StructureShape operationOutputOrError = operationOrError.asStructureShape()
716-
.orElseGet(() -> operationIndex.getOutput(operationOrError).orElse(null));
717-
boolean hasStreamingComponent = Optional.ofNullable(operationOutputOrError)
718-
.map(structure -> structure.getAllMembers().values().stream()
719-
.anyMatch(memberShape -> memberShape.hasTrait(StreamingTrait.class)))
720-
.orElse(false);
721-
if (hasStreamingComponent || operationOrError.getType().equals(ShapeType.STRUCTURE)) {
722-
// For operations with streaming output or errors with streaming body we keep the body intact.
723-
writer.write("const data: any = output.body;");
724-
} else {
725-
// Otherwise, we collect the response body to structured object with parseBody().
726-
writer.write("const data: any = await parseBody(output.body, context);");
727-
}
728-
}
729-
730767
/**
731768
* Given context and a source of data, generate an output value provider for the
732769
* shape. This may use native types (like generating a Date for timestamps,)
@@ -890,10 +927,16 @@ private String getNumberOutputParam(Location bindingType, String dataSource, Sha
890927
* Writes the code that loads an {@code errorCode} String with the content used
891928
* to dispatch errors to specific serializers.
892929
*
893-
* <p>Three variables will be in scope:
930+
* <p>Two variables will be in scope:
894931
* <ul>
895-
* <li>{@code output}: a value of the HttpResponse type.</li>
896-
* <li>{@code data}: the contents of the response body.</li>
932+
* <li>{@code output} or {@code parsedOutput}: a value of the HttpResponse type.
933+
* <ul>
934+
* <li>{@code output} is a raw HttpResponse, available when {@code isErrorCodeInBody} is set to
935+
* {@code false}</li>
936+
* <li>{@code parsedOutput} is a HttpResponse type with body parsed to JavaScript object, available
937+
* when {@code isErrorCodeInBody} is set to {@code true}</li>
938+
* </ul>
939+
* </li>
897940
* <li>{@code context}: the SerdeContext.</li>
898941
* </ul>
899942
*

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpProtocolGeneratorUtils.java

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,44 @@ static void generateMetadataDeserializer(GenerationContext context, SymbolRefere
108108
writer.write("");
109109
}
110110

111+
/**
112+
* Writes a response body stream collector. This function converts the low-level response body stream to
113+
* Uint8Array binary data.
114+
*
115+
* @param context The generation context.
116+
*/
117+
static void generateCollectBody(GenerationContext context) {
118+
TypeScriptWriter writer = context.getWriter();
119+
120+
writer.addImport("SerdeContext", "__SerdeContext", "@aws-sdk/types");
121+
writer.write("// Collect low-level response body stream to Uint8Array.");
122+
writer.openBlock("const collectBody = (streamBody: any, context: __SerdeContext): Promise<Uint8Array> => {",
123+
"};", () -> {
124+
writer.write("return context.streamCollector(streamBody) || new Uint8Array();");
125+
});
126+
127+
writer.write("");
128+
}
129+
130+
/**
131+
* Writes a function converting the low-level response body stream to utf-8 encoded string. It depends on
132+
* response body stream collector {@link #generateCollectBody(GenerationContext)}.
133+
*
134+
* @param context The generation context
135+
*/
136+
static void generateCollectBodyString(GenerationContext context) {
137+
TypeScriptWriter writer = context.getWriter();
138+
139+
writer.addImport("SerdeContext", "__SerdeContext", "@aws-sdk/types");
140+
writer.write("// Encode Uint8Array data into string with utf-8.");
141+
writer.openBlock("const collectBodyString = (streamBody: any, context: __SerdeContext): Promise<string> => {",
142+
"};", () -> {
143+
writer.write("return collectBody(streamBody, context).then(body => context.utf8Encoder(body));");
144+
});
145+
146+
writer.write("");
147+
}
148+
111149
/**
112150
* Writes a function used to dispatch to the proper error deserializer
113151
* for each error that the operation can return. The generated function
@@ -118,13 +156,15 @@ static void generateMetadataDeserializer(GenerationContext context, SymbolRefere
118156
* @param operation The operation to generate for.
119157
* @param responseType The response type for the HTTP protocol.
120158
* @param errorCodeGenerator A consumer
159+
* @param shouldParseErrorBody Flag indicating whether need to parse response body in this dispatcher function
121160
* @return A set of all error structure shapes for the operation that were dispatched to.
122161
*/
123162
static Set<StructureShape> generateErrorDispatcher(
124163
GenerationContext context,
125164
OperationShape operation,
126165
SymbolReference responseType,
127-
Consumer<GenerationContext> errorCodeGenerator
166+
Consumer<GenerationContext> errorCodeGenerator,
167+
boolean shouldParseErrorBody
128168
) {
129169
TypeScriptWriter writer = context.getWriter();
130170
SymbolProvider symbolProvider = context.getSymbolProvider();
@@ -138,14 +178,14 @@ static Set<StructureShape> generateErrorDispatcher(
138178
+ " output: $T,\n"
139179
+ " context: __SerdeContext,\n"
140180
+ "): Promise<$T> {", "}", errorMethodName, responseType, outputType, () -> {
141-
writer.write("const data: any = await parseBody(output.body, context);");
142-
// We only consume the parsedOutput if we're dispatching, so only generate if we will.
143-
if (!operation.getErrors().isEmpty()) {
144-
// Create a holding object since we have already parsed the body, but retain the rest of the output.
145-
writer.openBlock("const parsedOutput: any = {", "};", () -> {
146-
writer.write("...output,");
147-
writer.write("body: data,");
148-
});
181+
// Prepare error response for parsing error code. If error code needs to be parsed from response body
182+
// then we collect body and parse it to JS object, otherwise leave the response body as is.
183+
if (shouldParseErrorBody) {
184+
writer.openBlock("const parsedOutput: any = {", "};",
185+
() -> {
186+
writer.write("...output,");
187+
writer.write("body: await parseBody(output.body, context)");
188+
});
149189
}
150190

151191
// Error responses must be at least SmithyException and MetadataBearer implementations.
@@ -167,7 +207,8 @@ static Set<StructureShape> generateErrorDispatcher(
167207
context.getProtocolName()) + "Response";
168208
writer.openBlock("case $S:\ncase $S:", " break;", errorId.getName(), errorId.toString(), () -> {
169209
// Dispatch to the error deserialization function.
170-
writer.write("response = await $L(parsedOutput, context);", errorDeserMethodName);
210+
writer.write("response = await $L($L, context);",
211+
errorDeserMethodName, shouldParseErrorBody ? "parsedOutput" : "output");
171212
});
172213
});
173214

0 commit comments

Comments
 (0)