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
)