diff --git a/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java b/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java index 2c93b2222..eda0b83b2 100644 --- a/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java +++ b/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java @@ -35,9 +35,6 @@ public class RestJson1ProtocolTests { @HttpClientRequestTests @ProtocolTestFilter( skipTests = { - // TODO: These tests require a payload even when the httpPayload member is null. Should it? - "RestJsonHttpWithHeadersButNoPayload", - "RestJsonHttpWithEmptyStructurePayload", "RestJsonHttpEmptyPrefixHeadersRequestClient" //FIXME https://github.com/smithy-lang/smithy-java/issues/647 }) public void requestTest(DataStream expected, DataStream actual) { diff --git a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java index a69ab86e2..f3123c398 100644 --- a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java +++ b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java @@ -5,6 +5,7 @@ package software.amazon.smithy.java.aws.client.restjson; +import java.net.URI; import software.amazon.smithy.aws.traits.protocols.RestJson1Trait; import software.amazon.smithy.java.aws.events.AwsEventDecoderFactory; import software.amazon.smithy.java.aws.events.AwsEventEncoderFactory; @@ -17,14 +18,20 @@ import software.amazon.smithy.java.client.http.binding.HttpBindingClientProtocol; import software.amazon.smithy.java.client.http.binding.HttpBindingErrorFactory; import software.amazon.smithy.java.context.Context; +import software.amazon.smithy.java.core.schema.ApiOperation; import software.amazon.smithy.java.core.schema.InputEventStreamingApiOperation; import software.amazon.smithy.java.core.schema.OutputEventStreamingApiOperation; +import software.amazon.smithy.java.core.schema.SerializableStruct; +import software.amazon.smithy.java.core.schema.TraitKey; import software.amazon.smithy.java.core.serde.Codec; import software.amazon.smithy.java.core.serde.event.EventDecoderFactory; import software.amazon.smithy.java.core.serde.event.EventEncoderFactory; import software.amazon.smithy.java.core.serde.event.EventStreamingException; +import software.amazon.smithy.java.http.api.HttpRequest; +import software.amazon.smithy.java.http.binding.RequestSerializer; import software.amazon.smithy.java.json.JsonCodec; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeType; /** * Implements aws.protocols#restJson1. @@ -55,6 +62,29 @@ public RestJsonClientProtocol(ShapeId service) { .build(); } + @Override + public HttpRequest createRequest( + ApiOperation operation, + I input, + Context context, + URI endpoint + ) { + RequestSerializer serializer = httpBinding().requestSerializer() + .operation(operation) + .payloadCodec(payloadCodec()) + .payloadMediaType(payloadMediaType()) + .shapeValue(input) + .endpoint(endpoint) + .omitEmptyPayload(omitEmptyPayload()) + .allowEmptyStructPayload(hasStructPayload(input)); + + if (operation instanceof InputEventStreamingApiOperation i) { + serializer.eventEncoderFactory(getEventEncoderFactory(i)); + } + + return serializer.serializeRequest(); + } + @Override public Codec payloadCodec() { return codec; @@ -94,6 +124,17 @@ protected EventDecoderFactory getEventDecoderFactory( return AwsEventDecoderFactory.forOutputStream(outputOperation, payloadCodec(), f -> f); } + private boolean hasStructPayload(I input) { + var members = input.schema().members(); + for (var member : members) { + if (member.type().equals(ShapeType.STRUCTURE) + && member.hasTrait(TraitKey.HTTP_PAYLOAD_TRAIT)) { + return true; + } + } + return false; + } + public static final class Factory implements ClientProtocolFactory { @Override public ShapeId id() { diff --git a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSerializer.java b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSerializer.java index 4535b4fc5..4f778da60 100644 --- a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSerializer.java +++ b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/HttpBindingSerializer.java @@ -52,6 +52,7 @@ final class HttpBindingSerializer extends SpecificShapeSerializer implements Sha private final String payloadMediaType; private final boolean omitEmptyPayload; private final boolean isFailure; + private final boolean allowEmptyStructPayload; private final Map labels = new LinkedHashMap<>(); private final Map> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); @@ -75,7 +76,8 @@ final class HttpBindingSerializer extends SpecificShapeSerializer implements Sha String payloadMediaType, BindingMatcher bindingMatcher, boolean omitEmptyPayload, - boolean isFailure + boolean isFailure, + boolean allowEmptyStructPayload ) { uriPattern = httpTrait.getUri(); responseStatus = httpTrait.getCode(); @@ -84,6 +86,7 @@ final class HttpBindingSerializer extends SpecificShapeSerializer implements Sha this.payloadMediaType = payloadMediaType; this.omitEmptyPayload = omitEmptyPayload; this.isFailure = isFailure; + this.allowEmptyStructPayload = allowEmptyStructPayload; headerSerializer = new HttpHeaderSerializer(headerConsumer); querySerializer = new HttpQuerySerializer(queryStringParams::add); labelSerializer = new HttpLabelSerializer(labels::put); @@ -95,7 +98,7 @@ public void writeStruct(Schema schema, SerializableStruct struct) { responseStatus = bindingMatcher.responseStatus(); } - if (bindingMatcher.writeBody(omitEmptyPayload)) { + if (allowEmptyStructPayload || bindingMatcher.writeBody(omitEmptyPayload)) { shapeBodyOutput = new ByteArrayOutputStream(); shapeBodySerializer = payloadCodec.createSerializer(shapeBodyOutput); // Serialize only the body members to the codec. diff --git a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/RequestSerializer.java b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/RequestSerializer.java index 94cc0f120..c378a46c0 100644 --- a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/RequestSerializer.java +++ b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/RequestSerializer.java @@ -34,6 +34,7 @@ public final class RequestSerializer { private SerializableShape shapeValue; private EventEncoderFactory eventStreamEncodingFactory; private boolean omitEmptyPayload = false; + private boolean allowEmptyStructPayload = false; private final ConcurrentMap bindingCache; RequestSerializer(ConcurrentMap bindingCache) { @@ -119,6 +120,11 @@ public RequestSerializer omitEmptyPayload(boolean omitEmptyPayload) { return this; } + public RequestSerializer allowEmptyStructPayload(boolean allowEmptyStructPayload) { + this.allowEmptyStructPayload = allowEmptyStructPayload; + return this; + } + /** * Finishes setting up the serializer and creates an HTTP request. * @@ -129,7 +135,6 @@ public HttpRequest serializeRequest() { Objects.requireNonNull(operation, "operation is not set"); Objects.requireNonNull(payloadCodec, "payloadCodec is not set"); Objects.requireNonNull(endpoint, "endpoint is not set"); - Objects.requireNonNull(shapeValue, "value is not set"); Objects.requireNonNull(payloadMediaType, "payloadMediaType is not set"); var matcher = bindingCache.computeIfAbsent(operation.inputSchema(), BindingMatcher::requestMatcher); @@ -140,7 +145,8 @@ public HttpRequest serializeRequest() { payloadMediaType, matcher, omitEmptyPayload, - false); + false, + allowEmptyStructPayload); shapeValue.serialize(serializer); serializer.flush(); diff --git a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/ResponseSerializer.java b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/ResponseSerializer.java index 28645102d..36ee35f80 100644 --- a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/ResponseSerializer.java +++ b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/ResponseSerializer.java @@ -143,7 +143,8 @@ public HttpResponse serializeResponse() { payloadMediaType, bindingCache.computeIfAbsent(schema, BindingMatcher::responseMatcher), omitEmptyPayload, - isFailure); + isFailure, + false); shapeValue.serialize(serializer); serializer.flush();