Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>The standard implementations are as follows; these implementations may be
* overridden unless otherwise specified.
*
* <ul>
* <li>Blob: base64 encoded and encoded to a utf-8 string.</li>
* <li>Timestamp: serialized to a string.</li>
* <li>Service, Operation, Resource, Member: not deserializable from documents. <b>Not overridable.</b></li>
* <li>List, Map, Set, Structure, Union: delegated to a serialization function.
* <b>Not overridable.</b></li>
* <li>All other types: unmodified.</li>
* </ul>
*/
public class DocumentMemberSerVisitor implements ShapeVisitor<String> {
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> 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.
*
* <p>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)";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
@SmithyUnstableApi
public abstract class HttpBindingProtocolGenerator implements ProtocolGenerator {

private final Set<Shape> serializingDocumentShapes = new TreeSet<>();
private final Set<Shape> deserializingDocumentShapes = new TreeSet<>();

@Override
Expand Down Expand Up @@ -118,6 +119,7 @@ public void generateRequestSerializers(GenerationContext context) {
writer.popState();
});
}
generateDocumentBodyShapeSerializers(context, serializingDocumentShapes);
}

/**
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -395,6 +404,18 @@ protected abstract void serializeDocumentBody(
List<HttpBinding> documentBindings
);

/**
* Generates serialization functions for shapes in the given set.
*
* <p>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<Shape> shapes
);

/**
* Writes the code needed to serialize the input payload of a request.
Expand Down
Loading