diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/CodegenUtils.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/CodegenUtils.java index 3d1931959..afc1d41aa 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/CodegenUtils.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/CodegenUtils.java @@ -38,9 +38,11 @@ import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.NullableIndex; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.ErrorTrait; import software.amazon.smithy.model.traits.TimestampFormatTrait; import software.amazon.smithy.model.traits.TimestampFormatTrait.Format; @@ -288,4 +290,44 @@ private static ZonedDateTime parseHttpDate(Node value) { Instant instant = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(value.expectStringNode().getValue())); return instant.atZone(ZoneId.of("UTC")); } + + /** + * Writes an accessor for a structure member, handling defaultedness and nullability. + * + * @param context The generation context. + * @param writer The writer to write to. + * @param variableName The python variable name pointing to the structure to be accessed. + * @param member The member to access. + * @param runnable A runnable which uses the member. + */ + public static void accessStructureMember( + GenerationContext context, + PythonWriter writer, + String variableName, + MemberShape member, + Runnable runnable + ) { + var shouldDedent = false; + var isNullable = NullableIndex.of(context.model()).isMemberNullable(member); + var memberName = context.symbolProvider().toMemberName(member); + if (member.getMemberTrait(context.model(), DefaultTrait.class).isPresent()) { + if (isNullable) { + writer.write("if $1L._hasattr($2S) and $1L.$2L is not None:", variableName, memberName); + } else { + writer.write("if $L._hasattr($S):", variableName, memberName); + } + writer.indent(); + shouldDedent = true; + } else if (isNullable) { + writer.write("if $L.$L is not None:", variableName, memberName); + writer.indent(); + shouldDedent = true; + } + + runnable.run(); + + if (shouldDedent) { + writer.dedent(); + } + } } diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/DocumentMemberSerVisitor.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/DocumentMemberSerVisitor.java new file mode 100644 index 000000000..856bab5e6 --- /dev/null +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/DocumentMemberSerVisitor.java @@ -0,0 +1,256 @@ +/* + * Copyright 2023 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.python.codegen.integration; + +import java.util.Optional; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.model.shapes.BigDecimalShape; +import software.amazon.smithy.model.shapes.BigIntegerShape; +import software.amazon.smithy.model.shapes.BlobShape; +import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.ByteShape; +import software.amazon.smithy.model.shapes.DocumentShape; +import software.amazon.smithy.model.shapes.DoubleShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntegerShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.LongShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.ShortShape; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.TimestampShape; +import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.TimestampFormatTrait.Format; +import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.PythonWriter; + +/** + * Visitor to serialize member values for aggregate types into document bodies. + * + *

The standard implementations are as follows; these implementations may be + * overridden unless otherwise specified. + * + *

+ */ +public class DocumentMemberSerVisitor implements ShapeVisitor { + private final GenerationContext context; + private final PythonWriter writer; + private final MemberShape member; + private final String dataSource; + private final Format defaultTimestampFormat; + + /** + * @param context The generation context. + * @param writer The writer to write to. + * @param member The member shape being deserialized. Used for any extra traits + * it might bear, such as the timestamp format. + * @param dataSource The in-code location of the data to provide an output of + * ({@code output.foo}, {@code entry}, etc.) + * @param defaultTimestampFormat The default timestamp format used in absence + * of a TimestampFormat trait. + */ + public DocumentMemberSerVisitor( + GenerationContext context, + PythonWriter writer, + MemberShape member, + String dataSource, + Format defaultTimestampFormat + ) { + this.context = context; + this.writer = writer; + this.member = member; + this.dataSource = dataSource; + this.defaultTimestampFormat = defaultTimestampFormat; + } + + /** + * @return the member this visitor is being run against. Used to discover member-applied + * traits, such as @timestampFormat. + */ + protected Optional memberShape() { + return Optional.ofNullable(member); + } + + /** + * Gets the generation context. + * + * @return The generation context. + */ + protected final GenerationContext context() { + return context; + } + + /** + * Gets the PythonWriter being written to. + * + *

This should only be used to add imports. + * + * @return The writer to add imports to. + */ + protected final PythonWriter writer() { + return writer; + } + + /** + * Gets the in-code location of the data to provide an output of + * ({@code output.foo}, {@code entry}, etc.). + * + * @return The data source. + */ + protected final String dataSource() { + return dataSource; + } + + /** + * Gets the default timestamp format used in absence of a TimestampFormat trait. + * + * @return The default timestamp format. + */ + protected final Format getDefaultTimestampFormat() { + return defaultTimestampFormat; + } + + @Override + public String blobShape(BlobShape blobShape) { + writer.addStdlibImport("base64", "b64encode"); + return String.format("b64encode(%s).decode('utf-8')", dataSource()); + } + + @Override + public String booleanShape(BooleanShape booleanShape) { + return dataSource(); + } + + @Override + public String byteShape(ByteShape byteShape) { + return dataSource(); + } + + @Override + public String shortShape(ShortShape shortShape) { + return dataSource(); + } + + @Override + public String integerShape(IntegerShape integerShape) { + return dataSource(); + } + + @Override + public String longShape(LongShape longShape) { + return dataSource(); + } + + @Override + public String floatShape(FloatShape floatShape) { + return dataSource(); + } + + @Override + public String documentShape(DocumentShape documentShape) { + return dataSource(); + } + + @Override + public String doubleShape(DoubleShape doubleShape) { + return dataSource(); + } + + @Override + public String bigIntegerShape(BigIntegerShape bigIntegerShape) { + return String.format("str(%s)", dataSource()); + } + + @Override + public String bigDecimalShape(BigDecimalShape bigDecimalShape) { + return String.format("str(%s.normalize())", dataSource()); + } + + @Override + public final String operationShape(OperationShape shape) { + throw new CodegenException("Operation shapes cannot be bound to documents."); + } + + @Override + public final String resourceShape(ResourceShape shape) { + throw new CodegenException("Resource shapes cannot be bound to documents."); + } + + @Override + public final String serviceShape(ServiceShape shape) { + throw new CodegenException("Service shapes cannot be bound to documents."); + } + + @Override + public final String memberShape(MemberShape shape) { + throw new CodegenException("Member shapes cannot be bound to documents."); + } + + @Override + public String stringShape(StringShape stringShape) { + return dataSource(); + } + + @Override + public String timestampShape(TimestampShape timestampShape) { + return HttpProtocolGeneratorUtils.getTimestampInputParam( + context(), writer(), dataSource(), timestampShape, getDefaultTimestampFormat()); + } + + @Override + public String listShape(ListShape listShape) { + return getDelegateSerializer(listShape); + } + + @Override + public String mapShape(MapShape mapShape) { + return getDelegateSerializer(mapShape); + } + + @Override + public String structureShape(StructureShape structureShape) { + return getDelegateSerializer(structureShape); + } + + @Override + public String unionShape(UnionShape unionShape) { + return getDelegateSerializer(unionShape); + } + + private String getDelegateSerializer(Shape shape) { + return getDelegateSerializer(shape, dataSource); + } + + private String getDelegateSerializer(Shape shape, String customDataSource) { + var serSymbol = context.protocolGenerator().getSerializationFunction(context, shape.getId()); + writer.addImport(serSymbol, serSymbol.getName()); + return serSymbol.getName() + "(" + customDataSource + ", config)"; + } +} diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/HttpBindingProtocolGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/HttpBindingProtocolGenerator.java index 8777f7e36..405703a9a 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/HttpBindingProtocolGenerator.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/HttpBindingProtocolGenerator.java @@ -71,6 +71,7 @@ @SmithyUnstableApi public abstract class HttpBindingProtocolGenerator implements ProtocolGenerator { + private final Set serializingDocumentShapes = new TreeSet<>(); private final Set deserializingDocumentShapes = new TreeSet<>(); @Override @@ -118,6 +119,7 @@ public void generateRequestSerializers(GenerationContext context) { writer.popState(); }); } + generateDocumentBodyShapeSerializers(context, serializingDocumentShapes); } /** @@ -343,11 +345,18 @@ private void serializeBody( var documentBindings = bindingIndex.getRequestBindings(operation, DOCUMENT); if (!documentBindings.isEmpty() || shouldWriteDefaultBody(context, operation)) { serializeDocumentBody(context, writer, operation, documentBindings); + for (HttpBinding binding : documentBindings) { + var target = context.model().expectShape(binding.getMember().getTarget()); + serializingDocumentShapes.add(target); + } } var payloadBindings = bindingIndex.getRequestBindings(operation, PAYLOAD); if (!payloadBindings.isEmpty()) { - serializePayloadBody(context, writer, operation, payloadBindings.get(0)); + var binding = payloadBindings.get(0); + serializePayloadBody(context, writer, operation, binding); + var target = context.model().expectShape(binding.getMember().getTarget()); + serializingDocumentShapes.add(target); } writer.popState(); } @@ -395,6 +404,18 @@ protected abstract void serializeDocumentBody( List documentBindings ); + /** + * Generates serialization functions for shapes in the given set. + * + *

These are the functions that serializeDocumentBody will call out to. + * + * @param context The generation context. + * @param shapes The shapes to generate deserialization for. + */ + protected abstract void generateDocumentBodyShapeSerializers( + GenerationContext context, + Set shapes + ); /** * Writes the code needed to serialize the input payload of a request. diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/JsonMemberSerVisitor.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/JsonMemberSerVisitor.java new file mode 100644 index 000000000..1e43307a3 --- /dev/null +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/JsonMemberSerVisitor.java @@ -0,0 +1,87 @@ +/* + * Copyright 2023 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.python.codegen.integration; + +import java.util.Set; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.traits.TimestampFormatTrait.Format; +import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.PythonWriter; +import software.amazon.smithy.utils.SetUtils; + + +/** + * Visitor to serialize member values for aggregate types into JSON document bodies. + * + *

This does not delegate to serializers for lists or maps that would return them + * unchanged. A list of booleans, for example, will never need any serialization changes. + */ +public class JsonMemberSerVisitor extends DocumentMemberSerVisitor { + + private static final Set NOOP_TARGETS = SetUtils.of( + ShapeType.STRING, ShapeType.ENUM, ShapeType.BOOLEAN, ShapeType.DOCUMENT, ShapeType.BYTE, ShapeType.SHORT, + ShapeType.INTEGER, ShapeType.INT_ENUM, ShapeType.LONG, ShapeType.FLOAT, ShapeType.DOUBLE + ); + + /** + * @param context The generation context. + * @param writer The writer to write to. + * @param member The member shape being deserialized. Used for any extra traits + * it might bear, such as the timestamp format. + * @param dataSource The in-code location of the data to provide an output of + * ({@code output.foo}, {@code entry}, etc.) + * @param defaultTimestampFormat The default timestamp format used in absence + * of a TimestampFormat trait. + */ + public JsonMemberSerVisitor( + GenerationContext context, + PythonWriter writer, + MemberShape member, + String dataSource, + Format defaultTimestampFormat + ) { + super(context, writer, member, dataSource, defaultTimestampFormat); + } + + @Override + public String listShape(ListShape listShape) { + if (isNoOpMember(listShape.getMember())) { + return dataSource(); + } + return super.listShape(listShape); + } + + @Override + public String mapShape(MapShape mapShape) { + if (isNoOpMember(mapShape.getValue())) { + return dataSource(); + } + return super.mapShape(mapShape); + } + + private boolean isNoOpMember(MemberShape member) { + var target = context().model().expectShape(member.getTarget()); + if (target.isListShape()) { + return isNoOpMember(target.asListShape().get().getMember()); + } else if (target.isMapShape()) { + return isNoOpMember(target.asMapShape().get().getValue()); + } + return NOOP_TARGETS.contains(target.getType()); + } +} diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/JsonShapeDeserVisitor.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/JsonShapeDeserVisitor.java index ae67caa53..a795cdca7 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/JsonShapeDeserVisitor.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/JsonShapeDeserVisitor.java @@ -154,7 +154,7 @@ public Void mapShape(MapShape shape) { } else { writer.write("return {k: $L for k, v in output.items() if v is not None}", valueDeserializer); } - ;writer.dedent(); + writer.dedent(); return null; } diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/JsonShapeSerVisitor.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/JsonShapeSerVisitor.java new file mode 100644 index 000000000..923bfa8c1 --- /dev/null +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/JsonShapeSerVisitor.java @@ -0,0 +1,218 @@ +/* + * Copyright 2023 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.python.codegen.integration; + +import java.util.Collection; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.JsonNameTrait; +import software.amazon.smithy.model.traits.SparseTrait; +import software.amazon.smithy.model.traits.StringTrait; +import software.amazon.smithy.model.traits.TimestampFormatTrait.Format; +import software.amazon.smithy.python.codegen.CodegenUtils; +import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.PythonWriter; +import software.amazon.smithy.python.codegen.SmithyPythonDependency; + +/** + * Visitor to generate serialization functions for shapes in JSON document bodies. + */ +public class JsonShapeSerVisitor extends ShapeVisitor.Default { + private final GenerationContext context; + private final PythonWriter writer; + + /** + * @param context The generation context. + * @param writer The writer being written to. + */ + public JsonShapeSerVisitor(GenerationContext context, PythonWriter writer) { + this.context = context; + this.writer = writer; + } + + protected DocumentMemberSerVisitor getMemberVisitor(MemberShape member, String dataSource) { + return new JsonMemberSerVisitor(context, writer, member, dataSource, Format.EPOCH_SECONDS); + } + + @Override + protected Void getDefault(Shape shape) { + return null; + } + + @Override + public final Void operationShape(OperationShape shape) { + throw new CodegenException("Operation shapes cannot be bound to documents."); + } + + @Override + public final Void resourceShape(ResourceShape shape) { + throw new CodegenException("Resource shapes cannot be bound to documents."); + } + + @Override + public final Void serviceShape(ServiceShape shape) { + throw new CodegenException("Service shapes cannot be bound to documents."); + } + + @Override + public Void listShape(ListShape shape) { + var functionName = context.protocolGenerator().getSerializationFunctionName(context, shape); + var config = CodegenUtils.getConfigSymbol(context.settings()); + var listSymbol = context.symbolProvider().toSymbol(shape); + + var target = context.model().expectShape(shape.getMember().getTarget()); + var memberVisitor = getMemberVisitor(shape.getMember(), "e"); + var memberSerializer = target.accept(memberVisitor); + + // If we're not doing a transform, there's no need to have a function for it. + if (memberSerializer.equals("e")) { + return null; + } + + var sparseTrailer = ""; + if (shape.hasTrait(SparseTrait.class)) { + sparseTrailer = " if e is not None else None"; + } + + writer.addDependency(SmithyPythonDependency.SMITHY_PYTHON); + writer.addImport("smithy_python.types", "Document"); + writer.write(""" + def $1L(input: $2T, config: $3T) -> list[Document]: + return [$4L$5L for e in input] + """, functionName, listSymbol, config, memberSerializer, sparseTrailer); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + var functionName = context.protocolGenerator().getSerializationFunctionName(context, shape); + var config = CodegenUtils.getConfigSymbol(context.settings()); + var mapSymbol = context.symbolProvider().toSymbol(shape); + + var target = context.model().expectShape(shape.getValue().getTarget()); + var valueVisitor = getMemberVisitor(shape.getValue(), "v"); + var valueSerializer = target.accept(valueVisitor); + + // If we're not doing a transform, there's no need to have a function for it. + if (valueSerializer.equals("v")) { + return null; + } + + var sparseTrailer = ""; + if (shape.hasTrait(SparseTrait.class)) { + sparseTrailer = " if v is not None else None"; + } + + writer.addDependency(SmithyPythonDependency.SMITHY_PYTHON); + writer.addImport("smithy_python.types", "Document"); + writer.write(""" + def $1L(input: $2T, config: $3T) -> dict[str, Document]: + return {k: $4L$5L for k, v in input.items()} + """, functionName, mapSymbol, config, valueSerializer, sparseTrailer); + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + var functionName = context.protocolGenerator().getSerializationFunctionName(context, shape); + var config = CodegenUtils.getConfigSymbol(context.settings()); + var structureSymbol = context.symbolProvider().toSymbol(shape); + writer.addDependency(SmithyPythonDependency.SMITHY_PYTHON); + writer.addImport("smithy_python.types", "Document"); + + writer.write(""" + def $1L(input: $2T, config: $3T) -> dict[str, Document]: + result: dict[str, Document] = {} + + ${4C|} + return result + """, functionName, structureSymbol, config, (Runnable) () -> structureMembers(shape.members())); + return null; + } + + /** + * Generate serializers for structure members. + * + *

The structure to serialize must exist in the variable {@literal input}, and + * the output will be stored in a dict called {@literal result}, which must also + * exist. + * + * @param members The members to generate serializers for. + */ + public void structureMembers(Collection members) { + for (MemberShape member : members) { + var pythonName = context.symbolProvider().toMemberName(member); + var jsonName = locationName(member); + var target = context.model().expectShape(member.getTarget()); + + var memberVisitor = getMemberVisitor(member, "input." + pythonName); + var memberSerializer = target.accept(memberVisitor); + + CodegenUtils.accessStructureMember(context, writer, "input", member, () -> { + writer.write("result[$S] = $L\n", jsonName, memberSerializer); + }); + } + } + + protected String locationName(MemberShape member) { + return member.getMemberTrait(context.model(), JsonNameTrait.class) + .map(StringTrait::getValue) + .orElse(member.getMemberName()); + } + + @Override + public Void unionShape(UnionShape shape) { + var functionName = context.protocolGenerator().getSerializationFunctionName(context, shape); + var config = CodegenUtils.getConfigSymbol(context.settings()); + var unionSymbol = context.symbolProvider().toSymbol(shape); + var errorSymbol = CodegenUtils.getServiceError(context.settings()); + writer.addDependency(SmithyPythonDependency.SMITHY_PYTHON); + writer.addImport("smithy_python.types", "Document"); + + writer.write(""" + def $1L(input: $2T, config: $3T) -> dict[str, Document]: + match input: + ${5C|} + case _: + raise $4T(f"Unexpected union variant: {type(input)}") + """, functionName, unionSymbol, config, errorSymbol, (Runnable) () -> unionMembers(shape.members())); + return null; + } + + private void unionMembers(Collection members) { + for (MemberShape member : members) { + var jsonName = locationName(member); + var memberSymbol = context.symbolProvider().toSymbol(member); + var target = context.model().expectShape(member.getTarget()); + var memberVisitor = getMemberVisitor(member, "input.value"); + var memberSerializer = target.accept(memberVisitor); + + writer.write(""" + case $T(): + return {$S: $L} + """, memberSymbol, jsonName, memberSerializer); + } + } +} diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/RestJsonProtocolGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/RestJsonProtocolGenerator.java index ce350c902..a3c84500c 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/RestJsonProtocolGenerator.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/RestJsonProtocolGenerator.java @@ -88,7 +88,7 @@ private boolean filterTests(GenerationContext context, Shape shape, HttpMessageT } if (testCase instanceof HttpRequestTestCase) { // Request serialization isn't finished, so here we only test the bindings that are supported. - Set implementedBindings = SetUtils.of(Location.LABEL); + Set implementedBindings = SetUtils.of(Location.LABEL, Location.DOCUMENT); var bindingIndex = HttpBindingIndex.of(context.model()); // If any member specified in the test is bound to a location we haven't yet implemented, @@ -125,8 +125,30 @@ protected void serializeDocumentBody( OperationShape operation, List documentBindings ) { - // TODO: implement this - writer.write("body = b'{}'"); + writer.addDependency(SmithyPythonDependency.SMITHY_PYTHON); + writer.addImport("smithy_python.types", "Document"); + writer.write("result: dict[str, Document] = {}\n"); + + var bodyMembers = documentBindings.stream() + .map(HttpBinding::getMember) + .collect(Collectors.toSet()); + + var serVisitor = new JsonShapeSerVisitor(context, writer); + serVisitor.structureMembers(bodyMembers); + + writer.addStdlibImport("json", "dumps", "json_dumps"); + writer.write("body = json_dumps(result).encode('utf-8')"); + } + + @Override + protected void generateDocumentBodyShapeSerializers(GenerationContext context, Set shapes) { + for (Shape shape : getConnectedShapes(context, shapes)) { + var serFunction = context.protocolGenerator().getSerializationFunction(context, shape); + context.writerDelegator().useFileWriter(serFunction.getDefinitionFile(), + serFunction.getNamespace(), writer -> { + shape.accept(new JsonShapeSerVisitor(context, writer)); + }); + } } @Override @@ -158,11 +180,7 @@ protected void generateDocumentBodyShapeDeserializers( GenerationContext context, Set shapes ) { - var shapeWalker = new Walker(NeighborProviderIndex.of(context.model()).getProvider()); - var shapesToGenerate = new TreeSet<>(shapes); - shapes.forEach(shape -> shapesToGenerate.addAll(shapeWalker.walkShapes(shape))); - - for (Shape shape : shapesToGenerate) { + for (Shape shape : getConnectedShapes(context, shapes)) { var deserFunction = context.protocolGenerator().getDeserializationFunction(context, shape); context.writerDelegator().useFileWriter(deserFunction.getDefinitionFile(), @@ -171,4 +189,11 @@ protected void generateDocumentBodyShapeDeserializers( }); } } + + private Set getConnectedShapes(GenerationContext context, Set initialShapes) { + var shapeWalker = new Walker(NeighborProviderIndex.of(context.model()).getProvider()); + var connectedShapes = new TreeSet<>(initialShapes); + initialShapes.forEach(shape -> connectedShapes.addAll(shapeWalker.walkShapes(shape))); + return connectedShapes; + } } diff --git a/python-packages/smithy-python/smithy_python/types.py b/python-packages/smithy-python/smithy_python/types.py index 2d8e03b53..5c317982a 100644 --- a/python-packages/smithy-python/smithy_python/types.py +++ b/python-packages/smithy-python/smithy_python/types.py @@ -1,5 +1,5 @@ -from typing import TypeAlias +from typing import Mapping, Sequence, TypeAlias Document: TypeAlias = ( - dict[str, "Document"] | list["Document"] | str | int | float | bool | None + Mapping[str, "Document"] | Sequence["Document"] | str | int | float | bool | None )