Skip to content

Commit 935ce5b

Browse files
committed
feat(codegen): xml factory
1 parent 8341b0b commit 935ce5b

File tree

2 files changed

+96
-76
lines changed

2 files changed

+96
-76
lines changed

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

+51-44
Original file line numberDiff line numberDiff line change
@@ -735,31 +735,30 @@ private void writeResolvedPath(
735735

736736
// Handle any label bindings.
737737
if (!labelBindings.isEmpty()) {
738+
writer.addImport("resolvedPath", "__resolvedPath", "@aws-sdk/smithy-client");
739+
738740
Model model = context.getModel();
739741
List<Segment> uriLabels = trait.getUri().getLabels();
740742
for (HttpBinding binding : labelBindings) {
741743
String memberName = symbolProvider.toMemberName(binding.getMember());
742744
Shape target = model.expectShape(binding.getMember().getTarget());
743-
String labelValue = getInputValue(context, binding.getLocation(), "input." + memberName,
744-
binding.getMember(), target);
745+
746+
String labelValueProvider = "() => " + getInputValue(
747+
context,
748+
binding.getLocation(),
749+
"input." + memberName + "!",
750+
binding.getMember(),
751+
target
752+
);
753+
745754
// Get the correct label to use.
746755
Segment uriLabel = uriLabels.stream().filter(s -> s.getContent().equals(memberName)).findFirst().get();
747-
writer.addImport("extendedEncodeURIComponent", "__extendedEncodeURIComponent",
748-
"@aws-sdk/smithy-client");
749-
String encodedSegment = uriLabel.isGreedyLabel()
750-
? "labelValue.split(\"/\").map(segment => __extendedEncodeURIComponent(segment)).join(\"/\")"
751-
: "__extendedEncodeURIComponent(labelValue)";
752-
753-
// Set the label's value and throw a clear error if empty or undefined.
754-
writer.write("if (input.$L !== undefined) {", memberName).indent()
755-
.write("const labelValue: string = $L;", labelValue)
756-
.openBlock("if (labelValue.length <= 0) {", "}", () -> {
757-
writer.write("throw new Error('Empty value provided for input HTTP label: $L.');", memberName);
758-
})
759-
.write("resolvedPath = resolvedPath.replace($S, $L);", uriLabel.toString(), encodedSegment).dedent()
760-
.write("} else {").indent()
761-
.write("throw new Error('No value provided for input HTTP label: $L.');", memberName).dedent()
762-
.write("}");
756+
writer.write("resolvedPath = __resolvedPath(resolvedPath, input, '$L', $L, '$L', $L)",
757+
memberName,
758+
labelValueProvider,
759+
uriLabel.toString(),
760+
uriLabel.isGreedyLabel() ? "true" : "false"
761+
);
763762
}
764763
}
765764
}
@@ -1650,11 +1649,11 @@ private void generateOperationRequestDeserializer(
16501649
.forEach((memberName, memberShape) -> writer.write(
16511650
"$L: undefined,", memberName));
16521651
});
1652+
readRequestHeaders(context, operation, bindingIndex, "output");
16531653
});
16541654
readQueryString(context, operation, bindingIndex);
16551655
readPath(context, operation, bindingIndex, trait);
16561656
readHost(context, operation);
1657-
readRequestHeaders(context, operation, bindingIndex, "output");
16581657
List<HttpBinding> documentBindings = readRequestBody(context, operation, bindingIndex);
16591658
// Track all shapes bound to the document so their deserializers may be generated.
16601659
documentBindings.forEach(binding -> {
@@ -1703,7 +1702,7 @@ private void handleContentType(
17031702
operation, getDocumentContentType());
17041703
writer.write("const contentTypeHeaderKey: string | undefined = Object.keys(output.headers)"
17051704
+ ".find(key => key.toLowerCase() === 'content-type');");
1706-
writer.openBlock("if (contentTypeHeaderKey !== undefined && contentTypeHeaderKey !== null) {", "};", () -> {
1705+
writer.openBlock("if (contentTypeHeaderKey != null) {", "};", () -> {
17071706
writer.write("const contentType = output.headers[contentTypeHeaderKey];");
17081707
if (optionalContentType.isPresent() || operation.getInput().isPresent()) {
17091708
String contentType = optionalContentType.orElse(getDocumentContentType());
@@ -1758,7 +1757,7 @@ private void handleAccept(
17581757
writer.addImport("acceptMatches", "__acceptMatches", "@aws-smithy/server-common");
17591758
writer.write("const acceptHeaderKey: string | undefined = Object.keys(output.headers)"
17601759
+ ".find(key => key.toLowerCase() === 'accept');");
1761-
writer.openBlock("if (acceptHeaderKey !== undefined && acceptHeaderKey !== null) {", "};", () -> {
1760+
writer.openBlock("if (acceptHeaderKey != null) {", "};", () -> {
17621761
writer.write("const accept = output.headers[acceptHeaderKey];");
17631762
String contentType = optionalContentType.orElse(getDocumentContentType());
17641763
// Validate that the content type matches the protocol default, or what's modeled if there's
@@ -1793,7 +1792,7 @@ private void readQueryString(
17931792
return;
17941793
}
17951794
writer.write("const query = output.query");
1796-
writer.openBlock("if (query !== undefined && query !== null) {", "}", () -> {
1795+
writer.openBlock("if (query != null) {", "}", () -> {
17971796
readDirectQueryBindings(context, directQueryBindings);
17981797
if (!mappedQueryBindings.isEmpty()) {
17991798
// There can only ever be one of these bindings on a given operation.
@@ -2041,8 +2040,10 @@ private void generateErrorDeserializer(GenerationContext context, StructureShape
20412040
+ " context: __SerdeContext\n"
20422041
+ "): Promise<$T> => {", "};",
20432042
errorDeserMethodName, outputName, errorSymbol, () -> {
2044-
writer.write("const contents: any = {};");
2045-
readResponseHeaders(context, error, bindingIndex, outputName);
2043+
writer.openBlock("const contents: any = map({", "});", () -> {
2044+
readResponseHeaders(context, error, bindingIndex, outputName);
2045+
});
2046+
20462047
List<HttpBinding> documentBindings = readErrorResponseBody(context, error, bindingIndex);
20472048
// Track all shapes bound to the document so their deserializers may be generated.
20482049
documentBindings.forEach(binding -> {
@@ -2159,26 +2160,27 @@ private void readPrefixHeaders(
21592160
TypeScriptWriter writer = context.getWriter();
21602161

21612162
// Run through the headers one time, matching any prefix groups.
2162-
writer.openBlock("...(Object.keys($L.headers).reduce((acc, header) => {", "}, {}));", outputName, () -> {
2163-
for (HttpBinding binding : prefixHeaderBindings) {
2164-
// Prepare a grab bag for these headers if necessary
2165-
String memberName = symbolProvider.toMemberName(binding.getMember());
2166-
writer.write("acc.$L = [, {}];", memberName);
2167-
2168-
// Generate a single block for each group of lower-cased prefix headers.
2163+
for (HttpBinding binding : prefixHeaderBindings) {
2164+
// Prepare a grab bag for these headers if necessary
2165+
String memberName = symbolProvider.toMemberName(binding.getMember());
2166+
writer.openBlock("$L: [, ", "],", memberName, () -> {
21692167
String headerLocation = binding.getLocationName().toLowerCase(Locale.US);
2170-
writer.openBlock("if (header.startsWith($S)) {", "}", headerLocation, () -> {
2168+
writer.write(
2169+
"Object.keys($L.headers).filter(header => header.startsWith('$L'))",
2170+
outputName,
2171+
headerLocation
2172+
);
2173+
writer.indent().openBlock(".reduce((acc, header) => {", "}, {} as any)", () -> {
21712174
MapShape prefixMap = model.expectShape(binding.getMember().getTarget()).asMapShape().get();
21722175
Shape target = model.expectShape(prefixMap.getValue().getTarget());
21732176
String headerValue = getOutputValue(context, binding.getLocation(),
2174-
outputName + ".headers[header]", binding.getMember(), target);
2175-
2176-
// Extract the non-prefix portion as the key.
2177-
writer.write("acc.$L[header.substring($L)] = [, $L];",
2178-
memberName, headerLocation.length(), headerValue);
2177+
outputName + ".headers[header]", binding.getMember(), target);
2178+
writer.write("acc[header.substring($L)] = $L;",
2179+
headerLocation.length(), headerValue);
2180+
writer.write("return acc;");
21792181
});
2180-
}
2181-
});
2182+
});
2183+
}
21822184
}
21832185

21842186
private List<HttpBinding> readRequestBody(
@@ -2253,12 +2255,17 @@ private List<HttpBinding> readBody(
22532255
// isn't set in the body.
22542256
// These are only relevant when a payload is not present, as it cannot
22552257
// coexist with a payload.
2256-
for (HttpBinding responseCodeBinding : responseCodeBindings) {
2257-
// The name of the member to get from the input shape.
2258-
String memberName = symbolProvider.toMemberName(responseCodeBinding.getMember());
2259-
writer.openBlock("if (contents.$L === undefined) {", "}", memberName, () ->
2260-
writer.write("contents.$L = output.statusCode;", memberName));
2258+
if (!responseCodeBindings.isEmpty()) {
2259+
writer.openBlock("map(contents, {", "});", () -> {
2260+
for (HttpBinding responseCodeBinding : responseCodeBindings) {
2261+
// The name of the member to get from the input shape.
2262+
String memberName = symbolProvider.toMemberName(responseCodeBinding.getMember());
2263+
2264+
writer.write("$L: [, output.statusCode]", memberName);
2265+
}
2266+
});
22612267
}
2268+
22622269
if (!documentBindings.isEmpty()) {
22632270
return documentBindings;
22642271
}

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

+45-32
Original file line numberDiff line numberDiff line change
@@ -348,28 +348,13 @@ static Set<StructureShape> generateErrorDispatcher(
348348

349349
// Error responses must be at least BaseException interface
350350
SymbolReference baseExceptionReference = getClientBaseException(context);
351-
writer.write("let response: $T;", baseExceptionReference);
352351
errorCodeGenerator.accept(context);
353-
writer.openBlock("switch (errorCode) {", "}", () -> {
354-
// Generate the case statement for each error, invoking the specific deserializer.
355-
new TreeSet<>(operationIndex.getErrors(operation, context.getService())).forEach(error -> {
356-
final ShapeId errorId = error.getId();
357-
// Track errors bound to the operation so their deserializers may be generated.
358-
errorShapes.add(error);
359-
Symbol errorSymbol = symbolProvider.toSymbol(error);
360-
String errorDeserMethodName = ProtocolGenerator.getDeserFunctionName(errorSymbol,
361-
context.getProtocolName()) + "Response";
362-
// Dispatch to the error deserialization function.
363-
String outputParam = shouldParseErrorBody ? "parsedOutput" : "output";
364-
writer.write("case $S:", errorId.getName());
365-
writer.write("case $S:", errorId.toString());
366-
writer.indent()
367-
.write("throw await $L($L, context);", errorDeserMethodName, outputParam)
368-
.dedent();
369-
});
370352

371-
// Build a generic error the best we can for ones we don't know about.
372-
writer.write("default:").indent();
353+
TreeSet<StructureShape> structureShapes = new TreeSet<>(
354+
operationIndex.getErrors(operation, context.getService())
355+
);
356+
357+
Runnable defaultErrorHandler = () -> {
373358
if (shouldParseErrorBody) {
374359
// Body is already parsed above
375360
writer.write("const parsedBody = parsedOutput.body;");
@@ -378,21 +363,49 @@ static Set<StructureShape> generateErrorDispatcher(
378363
writer.write("const parsedBody = await parseBody(output.body, context);");
379364
}
380365

366+
writer.addImport("throwDefaultError", "throwDefaultError", "@aws-sdk/smithy-client");
367+
381368
// Get the protocol specific error location for retrieving contents.
382369
String errorLocation = bodyErrorLocationModifier.apply(context, "parsedBody");
383-
writer.write("const $$metadata = deserializeMetadata(output);");
384-
writer.write("const statusCode = $$metadata.httpStatusCode ? $$metadata.httpStatusCode"
385-
+ " + '' : undefined;");
386-
writer.openBlock("response = new $T({", "});", baseExceptionReference, () -> {
387-
writer.write("name: $1L.code || $1L.Code || errorCode || statusCode || 'UnknowError',",
388-
errorLocation);
389-
writer.write("$$fault: \"client\",");
390-
writer.write("$$metadata");
370+
writer.openBlock("throwDefaultError({", "})", () -> {
371+
writer.write("output,");
372+
if (errorLocation.equals("parsedBody")) {
373+
writer.write("parsedBody,");
374+
} else {
375+
writer.write("parsedBody: $L,", errorLocation);
376+
}
377+
writer.write("exceptionCtor: $T,", baseExceptionReference);
378+
writer.write("errorCode");
391379
});
392-
writer.addImport("decorateServiceException", "__decorateServiceException",
393-
TypeScriptDependency.AWS_SMITHY_CLIENT.packageName);
394-
writer.write("throw __decorateServiceException(response, $L);", errorLocation);
395-
});
380+
};
381+
382+
if (!structureShapes.isEmpty()) {
383+
writer.openBlock("switch (errorCode) {", "}", () -> {
384+
// Generate the case statement for each error, invoking the specific deserializer.
385+
386+
structureShapes.forEach(error -> {
387+
final ShapeId errorId = error.getId();
388+
// Track errors bound to the operation so their deserializers may be generated.
389+
errorShapes.add(error);
390+
Symbol errorSymbol = symbolProvider.toSymbol(error);
391+
String errorDeserMethodName = ProtocolGenerator.getDeserFunctionName(errorSymbol,
392+
context.getProtocolName()) + "Response";
393+
// Dispatch to the error deserialization function.
394+
String outputParam = shouldParseErrorBody ? "parsedOutput" : "output";
395+
writer.write("case $S:", errorId.getName());
396+
writer.write("case $S:", errorId.toString());
397+
writer.indent()
398+
.write("throw await $L($L, context);", errorDeserMethodName, outputParam)
399+
.dedent();
400+
});
401+
402+
// Build a generic error the best we can for ones we don't know about.
403+
writer.write("default:").indent();
404+
defaultErrorHandler.run();
405+
});
406+
} else {
407+
defaultErrorHandler.run();
408+
}
396409
});
397410
writer.write("");
398411

0 commit comments

Comments
 (0)