From 423715f2818dd1cce566d52b8b27ccedeaf9291a Mon Sep 17 00:00:00 2001 From: Luc Talatinian <102624213+lucix-aws@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:02:21 -0500 Subject: [PATCH] feat-svcgen-deserialize (#495) --- .../smithy/go/codegen/GoStdlibTypes.java | 11 + .../amazon/smithy/go/codegen/GoWriter.java | 2 +- .../smithy/go/codegen/SmithyGoTypes.java | 11 + .../amazon/smithy/go/codegen/SymbolUtils.java | 8 + .../smithy/go/codegen/service/Util.java | 63 +++++ .../protocol/JsonDeserializerGenerator.java | 261 ++++++++++++++++++ .../protocol/JsonSerializerGenerator.java | 194 +++++++++++++ .../aws/AwsJson10ProtocolGenerator.java | 59 +++- 8 files changed, 603 insertions(+), 6 deletions(-) create mode 100644 codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/Util.java create mode 100644 codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/JsonDeserializerGenerator.java create mode 100644 codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/JsonSerializerGenerator.java diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoStdlibTypes.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoStdlibTypes.java index c7929d89d..fb64a6538 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoStdlibTypes.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoStdlibTypes.java @@ -28,6 +28,17 @@ public static final class Context { public static final Symbol Context = SmithyGoDependency.CONTEXT.valueSymbol("Context"); } + public static final class Encoding { + public static final class Json { + public static final Symbol NewDecoder = SmithyGoDependency.JSON.valueSymbol("NewDecoder"); + public static final Symbol Number = SmithyGoDependency.JSON.valueSymbol("Number"); + } + + public static final class Base64 { + public static final Symbol StdEncoding = SmithyGoDependency.BASE64.valueSymbol("StdEncoding"); + } + } + public static final class Fmt { public static final Symbol Errorf = SmithyGoDependency.FMT.valueSymbol("Errorf"); public static final Symbol Sprintf = SmithyGoDependency.FMT.valueSymbol("Sprintf"); diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java index 2440dfaf9..e98da0cfb 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java @@ -1029,7 +1029,7 @@ public static ChainWritable of(GoWriter.Writable... writables) { return chain; } - public static ChainWritable of(List writables) { + public static ChainWritable of(Collection writables) { var chain = new ChainWritable(); chain.writables.addAll(writables); return chain; diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SmithyGoTypes.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SmithyGoTypes.java index 50a34ea42..32265d4b1 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SmithyGoTypes.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SmithyGoTypes.java @@ -29,9 +29,20 @@ public static final class Smithy { public static final Symbol OperationError = SmithyGoDependency.SMITHY.pointableSymbol("OperationError"); } + public static final class Encoding { + public static final class Json { + public static final Symbol NewEncoder = SmithyGoDependency.SMITHY_JSON.valueSymbol("NewEncoder"); + public static final Symbol Value = SmithyGoDependency.SMITHY_JSON.valueSymbol("Value"); + } + } + public static final class Ptr { public static final Symbol String = SmithyGoDependency.SMITHY_PTR.valueSymbol("String"); public static final Symbol Bool = SmithyGoDependency.SMITHY_PTR.valueSymbol("Bool"); + public static final Symbol Int8 = SmithyGoDependency.SMITHY_PTR.valueSymbol("Int8"); + public static final Symbol Int16 = SmithyGoDependency.SMITHY_PTR.valueSymbol("Int16"); + public static final Symbol Int32 = SmithyGoDependency.SMITHY_PTR.valueSymbol("Int32"); + public static final Symbol Int64 = SmithyGoDependency.SMITHY_PTR.valueSymbol("Int64"); } public static final class Middleware { diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SymbolUtils.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SymbolUtils.java index e3059dcaf..c964d2fba 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SymbolUtils.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SymbolUtils.java @@ -193,6 +193,14 @@ public static boolean isUniverseType(Symbol symbol) { .orElse(false); } + public static boolean isPointable(Symbol symbol) { + return symbol.getProperty(SymbolUtils.POINTABLE, Boolean.class).orElse(false); + } + + public static Symbol getReference(Symbol symbol) { + return symbol.getProperty(SymbolUtils.GO_ELEMENT_TYPE, Symbol.class).orElse(null); + } + /** * Builds a symbol within the context of the package in which codegen is taking place. * diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/Util.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/Util.java new file mode 100644 index 000000000..ab9b2746c --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/Util.java @@ -0,0 +1,63 @@ +/* + * Copyright 2024 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.go.codegen.service; + +import static java.util.stream.Collectors.toSet; + +import java.util.Set; +import java.util.stream.Stream; +import software.amazon.smithy.model.Model; +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.DoubleShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntegerShape; +import software.amazon.smithy.model.shapes.LongShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShortShape; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.TimestampShape; + +public final class Util { + private Util() {} + + public static Set getShapesToSerde(Model model, Shape shape) { + return Stream.concat( + Stream.of(normalize(shape)), + shape.members().stream() + .map(it -> model.expectShape(it.getTarget())) + .flatMap(it -> getShapesToSerde(model, it).stream()) + ).filter(it -> !it.getId().toString().equals("smithy.api#Unit")).collect(toSet()); + } + + public static Shape normalize(Shape shape) { + return switch (shape.getType()) { + // TODO should be marked synthetic and keyed into from there by caller to avoid shape name conflicts + case BLOB -> BlobShape.builder().id("com.amazonaws.synthetic#Blob").build(); + case BOOLEAN -> BooleanShape.builder().id("com.amazonaws.synthetic#Bool").build(); + case STRING -> StringShape.builder().id("com.amazonaws.synthetic#String").build(); + case TIMESTAMP -> TimestampShape.builder().id("com.amazonaws.synthetic#Time").build(); + case BYTE -> ByteShape.builder().id("com.amazonaws.synthetic#Int8").build(); + case SHORT -> ShortShape.builder().id("com.amazonaws.synthetic#Int16").build(); + case INTEGER -> IntegerShape.builder().id("com.amazonaws.synthetic#Int32").build(); + case LONG -> LongShape.builder().id("com.amazonaws.synthetic#Int64").build(); + case FLOAT -> FloatShape.builder().id("com.amazonaws.synthetic#Float32").build(); + case DOUBLE -> DoubleShape.builder().id("com.amazonaws.synthetic#Float64").build(); + default -> shape; + }; + } +} diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/JsonDeserializerGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/JsonDeserializerGenerator.java new file mode 100644 index 000000000..fa0a16dc7 --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/JsonDeserializerGenerator.java @@ -0,0 +1,261 @@ +/* + * Copyright 2024 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.go.codegen.service.protocol; + +import static software.amazon.smithy.go.codegen.GoWriter.goTemplate; +import static software.amazon.smithy.go.codegen.SymbolUtils.getReference; +import static software.amazon.smithy.go.codegen.SymbolUtils.isPointable; +import static software.amazon.smithy.go.codegen.service.Util.normalize; + +import java.util.Set; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.go.codegen.GoStdlibTypes; +import software.amazon.smithy.go.codegen.GoWriter; +import software.amazon.smithy.go.codegen.SmithyGoTypes; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.CollectionShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class JsonDeserializerGenerator { + private final Model model; + private final SymbolProvider symbolProvider; + + public JsonDeserializerGenerator(Model model, SymbolProvider symbolProvider) { + this.model = model; + this.symbolProvider = symbolProvider; + } + + public static String getDeserializerName(Shape shape) { + return "deserialize" + shape.getId().getName(); + } + + public GoWriter.Writable generate(Set shapes) { + return GoWriter.ChainWritable.of( + shapes.stream() + .map(this::generateShapeDeserializer) + .toList() + ).compose(); + } + + private GoWriter.Writable generateShapeDeserializer(Shape shape) { + return goTemplate(""" + func $name:L(v interface{}) ($shapeType:P, error) { + av, ok := v.($assert:W) + if !ok { + return $zero:W, $error:T("invalid") + } + $deserialize:W + } + """, + MapUtils.of( + "name", getDeserializerName(shape), + "shapeType", symbolProvider.toSymbol(shape), + "assert", generateOpaqueAssert(shape), + "zero", generateZeroValue(shape), + "error", GoStdlibTypes.Fmt.Errorf, + "deserialize", generateDeserializeAssertedValue(shape, "av") + )); + } + + private GoWriter.Writable generateOpaqueAssert(Shape shape) { + return switch (shape.getType()) { + case BYTE, SHORT, INTEGER, LONG, FLOAT, DOUBLE, INT_ENUM -> + goTemplate("$T", GoStdlibTypes.Encoding.Json.Number); + case STRING, BLOB, TIMESTAMP, ENUM, BIG_DECIMAL, BIG_INTEGER -> + goTemplate("string"); + case BOOLEAN -> + goTemplate("bool"); + case LIST, SET -> + goTemplate("[]interface{}"); + case MAP, STRUCTURE, UNION -> + goTemplate("map[string]interface{}"); + case DOCUMENT -> + throw new CodegenException("TODO: document is special"); + default -> + throw new CodegenException("? " + shape.getType()); + }; + } + + private GoWriter.Writable generateZeroValue(Shape shape) { + return switch (shape.getType()) { + case BYTE, SHORT, INTEGER, LONG, FLOAT, DOUBLE -> + goTemplate("0"); + case STRING -> + goTemplate("\"\""); + case BOOLEAN -> + goTemplate("false"); + case BLOB, LIST, SET, MAP, STRUCTURE, UNION -> + goTemplate("nil"); + case ENUM -> + goTemplate("$T(\"\")", symbolProvider.toSymbol(shape)); + case INT_ENUM -> + goTemplate("$T(0)", symbolProvider.toSymbol(shape)); + case DOCUMENT -> + throw new CodegenException("TODO: document is special"); + default -> + throw new CodegenException("? " + shape.getType()); + }; + } + + private GoWriter.Writable generateDeserializeAssertedValue(Shape shape, String ident) { + return switch (shape.getType()) { + case BYTE -> generateDeserializeIntegral(ident, "int8", Byte.MIN_VALUE, Byte.MAX_VALUE); + case SHORT -> generateDeserializeIntegral(ident, "int16", Short.MIN_VALUE, Short.MAX_VALUE); + case INTEGER -> generateDeserializeIntegral(ident, "int32", Integer.MIN_VALUE, Integer.MAX_VALUE); + case LONG -> generateDeserializeIntegral(ident, "int64", Long.MIN_VALUE, Long.MAX_VALUE); + case STRING, BOOLEAN -> goTemplate("return $L, nil", ident); + case ENUM -> goTemplate("return $T($L), nil", symbolProvider.toSymbol(shape), ident); + case BLOB -> goTemplate(""" + p, err := $b64:T.DecodeString($ident:L) + if err != nil { + return nil, err + } + return p, nil + """, + MapUtils.of( + "ident", ident, + "b64", GoStdlibTypes.Encoding.Base64.StdEncoding + )); + case LIST, SET -> { + var target = normalize(model.expectShape(((CollectionShape) shape).getMember().getTarget())); + var symbol = symbolProvider.toSymbol(shape); + var targetSymbol = symbolProvider.toSymbol(target); + yield goTemplate(""" + var deserializedList $type:T + for _, serializedItem := range $ident:L { + deserializedItem, err := $deserialize:L(serializedItem) + if err != nil { + return nil, err + } + deserializedList = append(deserializedList, $deref:L) + } + return deserializedList, nil + """, + MapUtils.of( + "type", symbol, + "ident", ident, + "deserialize", getDeserializerName(target), + "deref", isPointable(getReference(symbol)) != isPointable(targetSymbol) + ? "*deserializedItem" : "deserializedItem" + )); + } + case MAP -> { + var value = normalize(model.expectShape(((MapShape) shape).getValue().getTarget())); + var symbol = symbolProvider.toSymbol(shape); + var valueSymbol = symbolProvider.toSymbol(value); + yield goTemplate(""" + deserializedMap := $type:T{} + for key, serializedValue := range $ident:L { + deserializedValue, err := $deserialize:L(serializedValue) + if err != nil { + return nil, err + } + deserializedMap[key] = $deref:L + } + return deserializedMap, nil + """, + MapUtils.of( + "type", symbol, + "ident", ident, + "deserialize", getDeserializerName(value), + "deref", isPointable(getReference(symbol)) != isPointable(valueSymbol) + ? "*deserializedValue" : "deserializedValue" + )); + } + case STRUCTURE -> goTemplate(""" + deserializedStruct := &$type:T{} + for key, serializedValue := range $ident:L { + $deserializeFields:W + } + return deserializedStruct, nil + """, + MapUtils.of( + "type", symbolProvider.toSymbol(shape), + "ident", ident, + "deserializeFields", GoWriter.ChainWritable.of( + shape.getAllMembers().entrySet().stream() + .map(it -> { + var target = model.expectShape(it.getValue().getTarget()); + return goTemplate(""" + if key == $field:S { + fieldValue, err := $deserialize:L(serializedValue) + if err != nil { + return nil, err + } + deserializedStruct.$fieldName:L = $deref:W + } + """, + MapUtils.of( + "field", it.getKey(), + "fieldName", symbolProvider.toMemberName(it.getValue()), + "deserialize", getDeserializerName(normalize(target)), + "deref", generateStructFieldDeref( + it.getValue(), "fieldValue") + )); + }) + .toList() + ).compose(false) + )); + case UNION -> goTemplate("// TODO (union)"); + default -> + throw new CodegenException("? " + shape.getType()); + }; + } + + private GoWriter.Writable generateDeserializeIntegral(String ident, String castTo, long min, long max) { + return goTemplate(""" + $nextident:L, err := $ident:L.Int64() + if err != nil { + return 0, err + } + if $nextident:L < $min:L || $nextident:L > $max:L { + return 0, $errorf:T("invalid") + } + return $cast:L($nextident:L), nil + """, + MapUtils.of( + "errorf", GoStdlibTypes.Fmt.Errorf, + "ident", ident, + "nextident", ident + "_", + "min", min, + "max", max, + "cast", castTo + )); + } + + private GoWriter.Writable generateStructFieldDeref(MemberShape member, String ident) { + var symbol = symbolProvider.toSymbol(member); + if (!isPointable(symbol)) { + return goTemplate(ident); + } + return switch (model.expectShape(member.getTarget()).getType()) { + case BYTE -> goTemplate("$T($L)", SmithyGoTypes.Ptr.Int8, ident); + case SHORT -> goTemplate("$T($L)", SmithyGoTypes.Ptr.Int16, ident); + case INTEGER -> goTemplate("$T($L)", SmithyGoTypes.Ptr.Int32, ident); + case LONG -> goTemplate("$T($L)", SmithyGoTypes.Ptr.Int64, ident); + case STRING -> goTemplate("$T($L)", SmithyGoTypes.Ptr.String, ident); + case BOOLEAN -> goTemplate("$T($L)", SmithyGoTypes.Ptr.Bool, ident); + default -> goTemplate(ident); + }; + } + +} diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/JsonSerializerGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/JsonSerializerGenerator.java new file mode 100644 index 000000000..4396e6473 --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/JsonSerializerGenerator.java @@ -0,0 +1,194 @@ +/* + * Copyright 2024 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.go.codegen.service.protocol; + +import static software.amazon.smithy.go.codegen.GoWriter.goTemplate; +import static software.amazon.smithy.go.codegen.SymbolUtils.getReference; +import static software.amazon.smithy.go.codegen.SymbolUtils.isPointable; +import static software.amazon.smithy.go.codegen.service.Util.normalize; + +import java.util.Set; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.go.codegen.GoWriter; +import software.amazon.smithy.go.codegen.SmithyGoTypes; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.CollectionShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class JsonSerializerGenerator { + private final Model model; + private final SymbolProvider symbolProvider; + + public JsonSerializerGenerator(Model model, SymbolProvider symbolProvider) { + this.model = model; + this.symbolProvider = symbolProvider; + } + + public static String getSerializerName(Shape shape) { + return "serialize" + shape.getId().getName(); + } + + public GoWriter.Writable generate(Set shapes) { + return GoWriter.ChainWritable.of( + shapes.stream() + .map(this::generateShapeSerializer) + .toList() + ).compose(); + } + + private GoWriter.Writable generateShapeSerializer(Shape shape) { + return goTemplate(""" + func $name:L(v $shapeType:P, jv $jsonValue:T) (error) { + $serialize:W + return nil + } + """, + MapUtils.of( + "name", getSerializerName(shape), + "shapeType", symbolProvider.toSymbol(shape), + "jsonValue", SmithyGoTypes.Encoding.Json.Value, + "serialize", generateSerializeValue(shape) + )); + } + + private GoWriter.Writable generateSerializeValue(Shape shape) { + return switch (shape.getType()) { + case BYTE -> goTemplate("jv.Byte(v)"); + case SHORT -> goTemplate("jv.Short(v)"); + case INTEGER -> goTemplate("jv.Integer(v)"); + case LONG -> goTemplate("jv.Long(v)"); + case FLOAT -> goTemplate("jv.Float(v)"); + case DOUBLE -> goTemplate("jv.Double(v)"); + case STRING -> goTemplate("jv.String(v)"); + case BOOLEAN -> goTemplate("jv.Boolean(v)"); + case BLOB -> goTemplate("jv.Base64EncodeBytes(v)"); + case ENUM -> goTemplate("jv.String(string(v))"); + case INT_ENUM -> goTemplate("jv.Integer(int32(v))"); + case LIST, SET -> generateDeserializeList((CollectionShape) shape); + case MAP -> generateSerializeMap((MapShape) shape); + case STRUCTURE -> generateSerializeStruct((StructureShape) shape); + case TIMESTAMP -> goTemplate("// TODO timestamp"); + case UNION -> goTemplate("// TODO union"); + default -> + throw new CodegenException("? " + shape.getType()); + }; + } + + private GoWriter.Writable generateDeserializeList(CollectionShape shape) { + var target = normalize(model.expectShape(shape.getMember().getTarget())); + var symbol = symbolProvider.toSymbol(shape); + var targetSymbol = symbolProvider.toSymbol(target); + return goTemplate(""" + a := jv.Array() + defer a.Close() + for i := range v { + av := a.Value() + if err := $serialize:L($indirect:L, av); err != nil { + return err + } + } + """, + MapUtils.of( + "serialize", getSerializerName(target), + "indirect", isPointable(getReference(symbol)) != isPointable(targetSymbol) + ? "&v[i]" : "v[i]" + )); + } + + private GoWriter.Writable generateSerializeMap(MapShape shape) { + var value = normalize(model.expectShape(shape.getValue().getTarget())); + var symbol = symbolProvider.toSymbol(shape); + var valueSymbol = symbolProvider.toSymbol(value); + return goTemplate(""" + mp := jv.Object() + defer mp.Close() + for k, vv := range v { + mv := mp.Key(k) + if err := $serialize:L($indirect:L, mv); err != nil { + return err + } + } + """, + MapUtils.of( + "serialize", getSerializerName(value), + "indirect", isPointable(getReference(symbol)) != isPointable(valueSymbol) + ? "&vv" : "vv" + )); + } + + private GoWriter.Writable generateSerializeStruct(StructureShape shape) { + return goTemplate(""" + mp := jv.Object() + defer mp.Close() + $W + """, GoWriter.ChainWritable.of( + shape.getAllMembers().values().stream() + .map(this::generateSerializeField) + .toList() + ).compose(false)); + } + + private GoWriter.Writable generateSerializeField(MemberShape member) { + var symbol = symbolProvider.toSymbol(member); + var target = normalize(model.expectShape(member.getTarget())); + return switch (target.getType()) { + case BYTE, SHORT, INTEGER, LONG, FLOAT, DOUBLE, STRING, BOOLEAN -> + isPointable(symbol) + ? serializeNilableMember(member, target, true) + : serializeMember(member, target); + case BLOB, LIST, SET, MAP, STRUCTURE, UNION -> + serializeNilableMember(member, target, false); + default -> + serializeMember(member, target); + }; + } + + private GoWriter.Writable serializeNilableMember(MemberShape member, Shape target, boolean deref) { + return goTemplate(""" + if v.$field:L != nil { + if err := $serialize:L($deref:L v.$field:L, mp.Key($key:S)); err != nil { + return err + } + } + """, + MapUtils.of( + "field", symbolProvider.toMemberName(member), + "key", member.getMemberName(), + "serialize", getSerializerName(target), + "deref", deref ? "*" : "" + )); + } + + private GoWriter.Writable serializeMember(MemberShape member, Shape target) { + return goTemplate(""" + if err := $serialize:L(v.$field:L, mp.Key($key:S)); err != nil { + return err + } + """, + MapUtils.of( + "field", symbolProvider.toMemberName(member), + "key", member.getMemberName(), + "serialize", getSerializerName(target) + )); + } +} diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/aws/AwsJson10ProtocolGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/aws/AwsJson10ProtocolGenerator.java index 4ab8510c3..596b71e64 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/aws/AwsJson10ProtocolGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/service/protocol/aws/AwsJson10ProtocolGenerator.java @@ -15,22 +15,30 @@ package software.amazon.smithy.go.codegen.service.protocol.aws; +import static java.util.stream.Collectors.toSet; import static software.amazon.smithy.go.codegen.GoWriter.goTemplate; +import static software.amazon.smithy.go.codegen.service.Util.getShapesToSerde; +import static software.amazon.smithy.go.codegen.service.protocol.JsonDeserializerGenerator.getDeserializerName; +import static software.amazon.smithy.go.codegen.service.protocol.JsonSerializerGenerator.getSerializerName; import software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.go.codegen.GoStdlibTypes; import software.amazon.smithy.go.codegen.GoWriter; +import software.amazon.smithy.go.codegen.SmithyGoTypes; import software.amazon.smithy.go.codegen.service.NotImplementedError; import software.amazon.smithy.go.codegen.service.ServerCodegenUtils; import software.amazon.smithy.go.codegen.service.ServerInterface; import software.amazon.smithy.go.codegen.service.protocol.HttpServerProtocolGenerator; +import software.amazon.smithy.go.codegen.service.protocol.JsonDeserializerGenerator; +import software.amazon.smithy.go.codegen.service.protocol.JsonSerializerGenerator; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.OperationIndex; import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.utils.MapUtils; import software.amazon.smithy.utils.SmithyInternalApi; @@ -63,10 +71,28 @@ public ShapeId getProtocol() { public GoWriter.Writable generateSource() { return GoWriter.ChainWritable.of( super.generateSource(), + generateDeserializers(), + generateSerializers(), generateSerializeError() ).compose(); } + private GoWriter.Writable generateDeserializers() { + var shapes = TopDownIndex.of(model).getContainedOperations(service).stream() + .map(it -> model.expectShape(it.getInputShape(), StructureShape.class)) + .flatMap(it -> getShapesToSerde(model, it).stream()) + .collect(toSet()); + return new JsonDeserializerGenerator(model, symbolProvider).generate(shapes); + } + + private GoWriter.Writable generateSerializers() { + var shapes = TopDownIndex.of(model).getContainedOperations(service).stream() + .map(it -> model.expectShape(it.getOutputShape(), StructureShape.class)) + .flatMap(it -> getShapesToSerde(model, it).stream()) + .collect(toSet()); + return new JsonSerializerGenerator(model, symbolProvider).generate(shapes); + } + private GoWriter.Writable generateSerializeError() { return goTemplate(""" func serializeError(w $rw:T, err error) { @@ -150,19 +176,42 @@ private String getOperationTarget(OperationShape operation) { private GoWriter.Writable generateHandleOperation(OperationShape operation) { return goTemplate(""" - w.Header().Set("X-Amz-Target", $target:S) - _, err := h.service.$operation:L(r.Context(), nil) + d := $decoder:T(r.Body) + d.UseNumber() + var jv map[string]interface{} + if err := d.Decode(&jv); err != nil { + serializeError(w, err) + return + } + + in, err := $deserialize:L(jv) if err != nil { serializeError(w, err) return } - writeEmpty(w, http.StatusOK) + out, err := h.service.$operation:L(r.Context(), in) + if err != nil { + serializeError(w, err) + return + } + + e := $encoder:T() + if err := $serialize:L(out, e.Value); err != nil { + serializeError(w, err) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(e.Bytes()) return """, MapUtils.of( - "target", getOperationTarget(operation), - "operation", symbolProvider.toSymbol(operation).getName() + "decoder", GoStdlibTypes.Encoding.Json.NewDecoder, + "deserialize", getDeserializerName(model.expectShape(operation.getInputShape())), + "operation", symbolProvider.toSymbol(operation).getName(), + "encoder", SmithyGoTypes.Encoding.Json.NewEncoder, + "serialize", getSerializerName(model.expectShape(operation.getOutputShape())) )); } }