diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenUtils.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenUtils.java index 8be87ace8bf..b1b301d5081 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenUtils.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenUtils.java @@ -134,4 +134,20 @@ static void writeStreamingMemberType( + " `%2$s` defined in {@link %1$s}", containerSymbol.getName(), memberName)); writer.write("export interface $1L extends $1LType {}", typeName); } + + /** + * Ease the input streaming member restriction so that users don't need to construct a stream every time. + * This is used for inline type declarations (such as parameters) that need to take more permissive inputs + * Refer here for more rationales: https://github.com/aws/aws-sdk-js-v3/issues/843 + */ + static void writeInlineStreamingMemberType( + TypeScriptWriter writer, + Symbol containerSymbol, + MemberShape streamingMember + ) { + String memberName = streamingMember.getMemberName(); + String optionalSuffix = streamingMember.isRequired() ? "" : "?"; + writer.writeInline("Omit<$1T, $2S> & { $2L$3L: $1T[$2S]|string|Uint8Array|Buffer }", + containerSymbol, memberName, optionalSuffix); + } } diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/HttpProtocolTestGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/HttpProtocolTestGenerator.java index 2531b4ef6a5..8c9fc1096eb 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/HttpProtocolTestGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/HttpProtocolTestGenerator.java @@ -293,15 +293,20 @@ 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.openBlock("const testService: Partial<$T> = {", "};", serviceSymbol, () -> { - writer.write("$L: testFunction as $T,", operationSymbol.getName(), operationSymbol); + writer.openBlock("const testService: Partial<$T<{}>> = {", "};", serviceSymbol, () -> { + writer.write("$L: testFunction as $T<{}>,", operationSymbol.getName(), operationSymbol); }); String getHandlerName = "get" + handlerSymbol.getName(); writer.addImport(getHandlerName, null, "./server/"); + writer.addImport("ValidationFailure", "__ValidationFailure", "@aws-smithy/server-common"); // 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 $T);", getHandlerName, serviceSymbol); + writer.openBlock( + "const handler = $L(testService as $T<{}>, (ctx: {}, failures: __ValidationFailure[]) => {", + "});", getHandlerName, serviceSymbol, + () -> writer.write("if (failures) { throw failures; } return undefined;") + ); // Construct a new http request according to the test case definition. writer.openBlock("const request = new HttpRequest({", "});", () -> { @@ -609,7 +614,9 @@ private void writeServerResponseTest(OperationShape operation, HttpResponseTestC writer.addImport("serializeFrameworkException", null, "./protocols/" + ProtocolGenerator.getSanitizedName(protocolGenerator.getName())); - writer.write("const handler = new $T(service, testMux, serFn, serializeFrameworkException);", handlerSymbol); + writer.addImport("ValidationFailure", "__ValidationFailure", "@aws-smithy/server-common"); + writer.write("const handler = new $T(service, testMux, serFn, serializeFrameworkException, " + + "(ctx: {}, f: __ValidationFailure[]) => { if (f) { throw f; } return undefined;});", handlerSymbol); writer.write("let r = await handler.handle(request, {})").write(""); writeHttpResponseAssertions(testCase); } diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServerCommandGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServerCommandGenerator.java index fbee5d2b97d..4823f367693 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServerCommandGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServerCommandGenerator.java @@ -102,6 +102,11 @@ private void writeInputType(String typeName, Optional inputShape } else { // If the input is non-existent, then use an empty object. writer.write("export interface $L {}", typeName); + writer.openBlock("export namespace $L {", "}", typeName, () -> { + writer.addImport("ValidationFailure", "__ValidationFailure", "@aws-smithy/server-common"); + writer.writeDocs("@internal"); + writer.write("export const validate: () => __ValidationFailure[] = () => [];"); + }); } } @@ -110,8 +115,10 @@ private void renderNamespace(String typeName, StructureShape input) { writer.openBlock("export namespace $L {", "}", typeName, () -> { writer.addImport("ValidationFailure", "__ValidationFailure", "@aws-smithy/server-common"); writer.writeDocs("@internal"); - writer.write("export const validate: (obj: $L) => __ValidationFailure[] = $T.validate;", - symbol.getName(), symbol); + // Streaming makes the type of the object being validated weird on occasion. + // Using `Parameters` here means we don't have to try to derive the weird type twice + writer.write("export const validate: (obj: Parameters[0]) => " + + "__ValidationFailure[] = $1T.validate;", symbol); }); } diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/StructureGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/StructureGenerator.java index 9d56d763ee6..9ad2d09bc9f 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/StructureGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/StructureGenerator.java @@ -15,12 +15,17 @@ package software.amazon.smithy.typescript.codegen; +import static software.amazon.smithy.typescript.codegen.CodegenUtils.getBlobStreamingMembers; +import static software.amazon.smithy.typescript.codegen.CodegenUtils.writeInlineStreamingMemberType; + +import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.codegen.core.SymbolReference; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.traits.ErrorTrait; import software.amazon.smithy.typescript.codegen.integration.HttpProtocolGeneratorUtils; @@ -215,17 +220,17 @@ private void renderStructureNamespace(StructuredMemberWriter structuredMemberWri writer.addImport("ValidationFailure", "__ValidationFailure", "@aws-smithy/server-common"); writer.writeDocs("@internal"); - writer.openBlock("export const validate = ($L: $L, path: string = \"\"): __ValidationFailure[] => {", "}", - objectParam, symbol.getName(), - () -> { - // TODO: move this somewhere so it only gets run once. - // Putting it at the top of the namespace can result in runtime errors when - // you have mutually recursive structures because the validator of one will - // be defined before the validator of the other exists at all. - structuredMemberWriter.writeMemberValidatorFactory(writer, "memberValidators"); - structuredMemberWriter.writeValidateMethodContents(writer, objectParam); - } - ); + List blobStreamingMembers = getBlobStreamingMembers(model, shape); + writer.writeInline("export const validate = ($L: ", objectParam); + if (blobStreamingMembers.isEmpty()) { + writer.writeInline("$L", symbol.getName()); + } else { + writeInlineStreamingMemberType(writer, symbol, blobStreamingMembers.get(0)); + } + writer.openBlock(", path: string = \"\"): __ValidationFailure[] => {", "}", () -> { + structuredMemberWriter.writeMemberValidatorFactory(writer, "memberValidators"); + structuredMemberWriter.writeValidateMethodContents(writer, objectParam); + }); }); } } diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/StructuredMemberWriter.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/StructuredMemberWriter.java index 50d481da3bd..74676f12288 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/StructuredMemberWriter.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/StructuredMemberWriter.java @@ -36,6 +36,7 @@ import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.IdempotencyTokenTrait; import software.amazon.smithy.model.traits.LengthTrait; +import software.amazon.smithy.model.traits.MediaTypeTrait; import software.amazon.smithy.model.traits.PatternTrait; import software.amazon.smithy.model.traits.RangeTrait; import software.amazon.smithy.model.traits.RequiredTrait; @@ -347,8 +348,14 @@ void writeMemberValidatorFactory(TypeScriptWriter writer, String cacheName) { void writeValidateMethodContents(TypeScriptWriter writer, String param) { writer.openBlock("return [", "];", () -> { for (MemberShape member : members) { - writer.write("...getMemberValidator($1S).validate($2L.$1L, `$${path}/$3L`),", - getSanitizedMemberName(member), param, member.getMemberName()); + String optionalSuffix = ""; + if (member.getMemberTrait(model, MediaTypeTrait.class).isPresent() + && model.expectShape(member.getTarget()) instanceof StringShape) { + // lazy JSON wrapper validation should be done based on the serialized form of the object + optionalSuffix = "?.toString()"; + } + writer.write("...getMemberValidator($1S).validate($2L.$1L$4L, `$${path}/$3L`),", + getSanitizedMemberName(member), param, member.getMemberName(), optionalSuffix); } }); } @@ -553,6 +560,14 @@ private Symbol getSymbolForValidatedType(Shape shape) { return symbolProvider.toSymbol(model.expectShape(ShapeId.from("smithy.api#String"))); } + // Streaming blob inputs can also take string, Uint8Array and Buffer, so we widen the symbol + if (shape.isBlobShape() && shape.hasTrait(StreamingTrait.class)) { + return symbolProvider.toSymbol(shape) + .toBuilder() + .name("Readable | ReadableStream | Blob | string | Uint8Array | Buffer") + .build(); + } + return symbolProvider.toSymbol(shape); }