From b7ae994edf62ffa0a4db58bea9fefb1f32bc75a7 Mon Sep 17 00:00:00 2001 From: Chase Coalwell <782571+srchase@users.noreply.github.com> Date: Tue, 9 May 2023 14:51:32 -0600 Subject: [PATCH] Convert SerdeElision to KnowledgeIndex (#759) * Convert SerdeElision to KnowledgeIndex * Add SerdeElisionIndex description --- .../DocumentMemberDeserVisitor.java | 11 +- .../integration/DocumentMemberSerVisitor.java | 11 +- .../DocumentShapeDeserVisitor.java | 6 +- .../integration/DocumentShapeSerVisitor.java | 6 +- .../integration/EventStreamGenerator.java | 40 ++-- .../HttpBindingProtocolGenerator.java | 20 +- .../integration/HttpRpcProtocolGenerator.java | 11 +- .../codegen/knowledge/SerdeElisionIndex.java | 131 ++++++++++ .../codegen/validation/SerdeElision.java | 208 ---------------- .../DocumentMemberDeserVisitorTest.java | 6 +- .../DocumentMemberSerVisitorTest.java | 2 + .../knowledge/SerdeElisionIndexTest.java | 136 +++++++++++ .../codegen/validation/SerdeElisionTest.java | 130 ---------- .../codegen/knowledge/serde-elision.smithy | 226 ++++++++++++++++++ 14 files changed, 562 insertions(+), 382 deletions(-) create mode 100644 smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/knowledge/SerdeElisionIndex.java delete mode 100644 smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/validation/SerdeElision.java create mode 100644 smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/knowledge/SerdeElisionIndexTest.java delete mode 100644 smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/validation/SerdeElisionTest.java create mode 100644 smithy-typescript-codegen/src/test/resources/software/amazon/smithy/typescript/codegen/knowledge/serde-elision.smithy diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberDeserVisitor.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberDeserVisitor.java index 75ab5a96b97..33dcad2017f 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberDeserVisitor.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberDeserVisitor.java @@ -45,7 +45,7 @@ import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.TimestampFormatTrait.Format; import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext; -import software.amazon.smithy.typescript.codegen.validation.SerdeElision; +import software.amazon.smithy.typescript.codegen.knowledge.SerdeElisionIndex; import software.amazon.smithy.utils.SmithyUnstableApi; /** @@ -67,10 +67,11 @@ */ @SmithyUnstableApi public class DocumentMemberDeserVisitor implements ShapeVisitor { - protected final SerdeElision serdeElision; + protected boolean serdeElisionEnabled; private final GenerationContext context; private final String dataSource; private final Format defaultTimestampFormat; + private final SerdeElisionIndex serdeElisionIndex; /** * Constructor. @@ -89,8 +90,8 @@ public DocumentMemberDeserVisitor( this.context = context; this.dataSource = dataSource; this.defaultTimestampFormat = defaultTimestampFormat; - this.serdeElision = SerdeElision.forModel(context.getModel()) - .setEnabledForModel(false); + this.serdeElisionEnabled = false; + this.serdeElisionIndex = SerdeElisionIndex.of(context.getModel()); } /** @@ -288,7 +289,7 @@ private String getDelegateDeserializer(Shape shape, String customDataSource) { // Use the shape for the function name. Symbol symbol = context.getSymbolProvider().toSymbol(shape); - if (serdeElision.mayElide(shape)) { + if (serdeElisionEnabled && serdeElisionIndex.mayElide(shape)) { return "_json(" + customDataSource + ")"; } diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberSerVisitor.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberSerVisitor.java index 25c38f6fbd8..210df33d2a9 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberSerVisitor.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberSerVisitor.java @@ -45,7 +45,7 @@ import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.TimestampFormatTrait.Format; import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext; -import software.amazon.smithy.typescript.codegen.validation.SerdeElision; +import software.amazon.smithy.typescript.codegen.knowledge.SerdeElisionIndex; import software.amazon.smithy.utils.SmithyUnstableApi; /** @@ -66,10 +66,11 @@ */ @SmithyUnstableApi public class DocumentMemberSerVisitor implements ShapeVisitor { - protected final SerdeElision serdeElision; + protected boolean serdeElisionEnabled; private final GenerationContext context; private final String dataSource; private final Format defaultTimestampFormat; + private final SerdeElisionIndex serdeElisionIndex; /** * Constructor. @@ -88,8 +89,8 @@ public DocumentMemberSerVisitor( this.context = context; this.dataSource = dataSource; this.defaultTimestampFormat = defaultTimestampFormat; - this.serdeElision = SerdeElision.forModel(context.getModel()) - .setEnabledForModel(false); + this.serdeElisionEnabled = false; + this.serdeElisionIndex = SerdeElisionIndex.of(context.getModel()); } /** @@ -257,7 +258,7 @@ private String getDelegateSerializer(Shape shape) { // Use the shape for the function name. Symbol symbol = context.getSymbolProvider().toSymbol(shape); - if (serdeElision.mayElide(shape)) { + if (serdeElisionEnabled && serdeElisionIndex.mayElide(shape)) { return "_json(" + dataSource + ")"; } diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentShapeDeserVisitor.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentShapeDeserVisitor.java index 9a1fe824a97..65166a59fe4 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentShapeDeserVisitor.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentShapeDeserVisitor.java @@ -34,7 +34,7 @@ import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.typescript.codegen.TypeScriptWriter; import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext; -import software.amazon.smithy.typescript.codegen.validation.SerdeElision; +import software.amazon.smithy.typescript.codegen.knowledge.SerdeElisionIndex; import software.amazon.smithy.utils.SmithyUnstableApi; /** @@ -66,10 +66,12 @@ */ @SmithyUnstableApi public abstract class DocumentShapeDeserVisitor extends ShapeVisitor.Default { + protected boolean serdeElisionEnabled; private final GenerationContext context; public DocumentShapeDeserVisitor(GenerationContext context) { this.context = context; + this.serdeElisionEnabled = false; } /** @@ -306,7 +308,7 @@ protected final void generateDeserFunction( String methodLongName = ProtocolGenerator.getDeserFunctionName(symbol, context.getProtocolName()); - boolean mayElide = SerdeElision.forModel(context.getModel()).mayElide(shape); + boolean mayElide = serdeElisionEnabled && SerdeElisionIndex.of(context.getModel()).mayElide(shape); if (mayElide) { writer.write("// " + methodName + " omitted."); writer.write(""); diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentShapeSerVisitor.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentShapeSerVisitor.java index 7bb41859834..c1f4f3b2533 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentShapeSerVisitor.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DocumentShapeSerVisitor.java @@ -34,7 +34,7 @@ import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.typescript.codegen.TypeScriptWriter; import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext; -import software.amazon.smithy.typescript.codegen.validation.SerdeElision; +import software.amazon.smithy.typescript.codegen.knowledge.SerdeElisionIndex; import software.amazon.smithy.utils.SmithyUnstableApi; /** @@ -66,10 +66,12 @@ */ @SmithyUnstableApi public abstract class DocumentShapeSerVisitor extends ShapeVisitor.Default { + protected boolean serdeElisionEnabled; private final GenerationContext context; public DocumentShapeSerVisitor(GenerationContext context) { this.context = context; + this.serdeElisionEnabled = false; } /** @@ -303,7 +305,7 @@ private void generateSerFunction( writer.addImport(symbol, symbol.getName()); - boolean mayElide = SerdeElision.forModel(context.getModel()).mayElide(shape); + boolean mayElide = serdeElisionEnabled && SerdeElisionIndex.of(context.getModel()).mayElide(shape); if (mayElide) { writer.write("// " + methodName + " omitted."); writer.write(""); diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/EventStreamGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/EventStreamGenerator.java index 3d68d225149..a1d432be09b 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/EventStreamGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/EventStreamGenerator.java @@ -42,7 +42,7 @@ import software.amazon.smithy.typescript.codegen.TypeScriptDependency; import software.amazon.smithy.typescript.codegen.TypeScriptWriter; import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext; -import software.amazon.smithy.typescript.codegen.validation.SerdeElision; +import software.amazon.smithy.typescript.codegen.knowledge.SerdeElisionIndex; import software.amazon.smithy.utils.SmithyUnstableApi; /** @@ -117,13 +117,15 @@ public void generateEventStreamSerializers( eventUnionsToSerialize.forEach(eventsUnion -> { generateEventStreamSerializer(context, eventsUnion); }); + SerdeElisionIndex serdeElisionIndex = SerdeElisionIndex.of(model); eventShapesToMarshall.forEach(event -> { generateEventMarshaller( context, event, documentContentType, serializeInputEventDocumentPayload, - documentShapesToSerialize); + documentShapesToSerialize, + serdeElisionIndex); }); } @@ -143,7 +145,9 @@ public void generateEventStreamDeserializers( ServiceShape service, Set errorShapesToDeserialize, Set eventShapesToDeserialize, - boolean isErrorCodeInBody + boolean isErrorCodeInBody, + boolean serdeElisionEnabled, + SerdeElisionIndex serdeElisionIndex ) { Model model = context.getModel(); @@ -171,7 +175,9 @@ public void generateEventStreamDeserializers( event, errorShapesToDeserialize, eventShapesToDeserialize, - isErrorCodeInBody + isErrorCodeInBody, + serdeElisionEnabled, + serdeElisionIndex ); }); } @@ -234,7 +240,8 @@ public void generateEventMarshaller( StructureShape event, String documentContentType, Runnable serializeInputEventDocumentPayload, - Set documentShapesToSerialize + Set documentShapesToSerialize, + SerdeElisionIndex serdeElisionIndex ) { String methodName = getEventSerFunctionName(context, event); Symbol symbol = getSymbol(context, event); @@ -252,7 +259,7 @@ public void generateEventMarshaller( }); writeEventHeaders(context, event); writeEventBody(context, event, serializeInputEventDocumentPayload, - documentShapesToSerialize); + documentShapesToSerialize, serdeElisionIndex); writer.openBlock("return { headers, body };"); }); } @@ -336,7 +343,8 @@ private void writeEventBody( GenerationContext context, StructureShape event, Runnable serializeInputEventDocumentPayload, - Set documentShapesToSerialize + Set documentShapesToSerialize, + SerdeElisionIndex serdeElisionIndex ) { TypeScriptWriter writer = context.getWriter(); Optional payloadMemberOptional = getEventPayloadMember(event); @@ -352,7 +360,7 @@ private void writeEventBody( } else if (payloadShape instanceof BlobShape || payloadShape instanceof StringShape) { Symbol symbol = getSymbol(context, payloadShape); String serFunctionName = ProtocolGenerator.getSerFunctionShortName(symbol); - boolean mayElide = SerdeElision.forModel(context.getModel()).mayElide(payloadShape); + boolean mayElide = serdeElisionIndex.mayElide(payloadShape); documentShapesToSerialize.add(payloadShape); if (mayElide) { writer.write("body = $L(input.$L);", "_json", payloadMemberName); @@ -375,7 +383,7 @@ private void writeEventBody( Symbol symbol = getSymbol(context, event); String serFunctionName = ProtocolGenerator.getSerFunctionShortName(symbol); documentShapesToSerialize.add(event); - boolean mayElide = SerdeElision.forModel(context.getModel()).mayElide(event); + boolean mayElide = serdeElisionIndex.mayElide(event); if (mayElide) { writer.write("body = $L(input);", "_json"); } else { @@ -431,7 +439,9 @@ public void generateEventUnmarshaller( StructureShape event, Set errorShapesToDeserialize, Set eventShapesToDeserialize, - boolean isErrorCodeInBody + boolean isErrorCodeInBody, + boolean serdeElisionEnabled, + SerdeElisionIndex serdeElisionIndex ) { String methodName = getEventDeserFunctionName(context, event); Symbol symbol = getSymbol(context, event); @@ -445,7 +455,7 @@ public void generateEventUnmarshaller( } else { writer.write("const contents: $L = {} as any;", symbol.getName()); readEventHeaders(context, event); - readEventBody(context, event, eventShapesToDeserialize); + readEventBody(context, event, eventShapesToDeserialize, serdeElisionEnabled, serdeElisionIndex); writer.write("return contents;"); } }); @@ -492,7 +502,9 @@ private void readEventHeaders(GenerationContext context, StructureShape event) { private void readEventBody( GenerationContext context, StructureShape event, - Set eventShapesToDeserialize + Set eventShapesToDeserialize, + boolean serdeElisionEnabled, + SerdeElisionIndex serdeElisionIndex ) { TypeScriptWriter writer = context.getWriter(); Optional payloadmemberOptional = getEventPayloadMember(event); @@ -507,7 +519,7 @@ private void readEventBody( writer.write("const data: any = await parseBody(output.body, context);"); Symbol symbol = getSymbol(context, payloadShape); String deserFunctionName = ProtocolGenerator.getDeserFunctionShortName(symbol); - boolean mayElide = SerdeElision.forModel(context.getModel()).mayElide(payloadShape); + boolean mayElide = serdeElisionEnabled && serdeElisionIndex.mayElide(payloadShape); if (mayElide) { writer.addImport("_json", null, "@aws-sdk/smithy-client"); writer.write("contents.$L = $L(data);", payloadMemberName, "_json"); @@ -520,7 +532,7 @@ private void readEventBody( writer.write("const data: any = await parseBody(output.body, context);"); Symbol symbol = getSymbol(context, event); String deserFunctionName = ProtocolGenerator.getDeserFunctionShortName(symbol); - boolean mayElide = SerdeElision.forModel(context.getModel()).mayElide(event); + boolean mayElide = serdeElisionEnabled && serdeElisionIndex.mayElide(event); if (mayElide) { writer.addImport("_json", null, "@aws-sdk/smithy-client"); writer.write("Object.assign(contents, $L(data));", "_json"); diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java index a80d3ad5f20..e0a1b1c4cae 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java @@ -75,7 +75,7 @@ import software.amazon.smithy.typescript.codegen.TypeScriptDependency; import software.amazon.smithy.typescript.codegen.TypeScriptWriter; import software.amazon.smithy.typescript.codegen.endpointsV2.RuleSetParameterFinder; -import software.amazon.smithy.typescript.codegen.validation.SerdeElision; +import software.amazon.smithy.typescript.codegen.knowledge.SerdeElisionIndex; import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.OptionalUtils; import software.amazon.smithy.utils.SetUtils; @@ -183,6 +183,7 @@ public void generateSharedComponents(GenerationContext context) { }, serializingDocumentShapes ); + SerdeElisionIndex serdeElisionIndex = SerdeElisionIndex.of(context.getModel()); // Error shapes that only referred in the error event of an eventstream Set errorEventShapes = new TreeSet<>(); eventStreamGenerator.generateEventStreamDeserializers( @@ -190,7 +191,9 @@ public void generateSharedComponents(GenerationContext context) { service, errorEventShapes, deserializingDocumentShapes, - isErrorCodeInBody + isErrorCodeInBody, + enableSerdeElision(), + serdeElisionIndex ); errorEventShapes.removeIf(deserializingErrorShapes::contains); errorEventShapes.forEach(error -> generateErrorDeserializer(context, error)); @@ -1355,9 +1358,8 @@ private String getNamedMembersInputParam( case PAYLOAD: Symbol symbol = context.getSymbolProvider().toSymbol(target); - boolean mayElideInput = SerdeElision.forModel(context.getModel()) - .setEnabledForModel(enableSerdeElision() && !context.getSettings().generateServerSdk()) - .mayElide(target); + boolean mayElideInput = SerdeElisionIndex.of(context.getModel()).mayElide(target) + && (enableSerdeElision() && !context.getSettings().generateServerSdk()); if (mayElideInput) { return "_json(" + dataSource + ")"; @@ -2689,9 +2691,8 @@ private String getNamedMembersOutputParam( // Redirect to a deserialization function. Symbol symbol = context.getSymbolProvider().toSymbol(target); - boolean mayElideOutput = SerdeElision.forModel(context.getModel()) - .setEnabledForModel(enableSerdeElision() && !context.getSettings().generateServerSdk()) - .mayElide(target); + boolean mayElideOutput = SerdeElisionIndex.of(context.getModel()).mayElide(target) + && (enableSerdeElision() && !context.getSettings().generateServerSdk()); if (mayElideOutput) { return "_json(" + dataSource + ")"; @@ -2883,8 +2884,7 @@ protected abstract void deserializeErrorDocumentBody( protected abstract boolean requiresNumericEpochSecondsInPayload(); /** - * Implement a return true if the protocol allows elision of serde functions - * as defined in {@link SerdeElision}. + * Implement a return true if the protocol allows elision of serde functions. * * @return whether protocol implementation is compatible with serde elision. */ diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java index 62268432e6d..2a1a12e3bf8 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java @@ -33,7 +33,7 @@ import software.amazon.smithy.typescript.codegen.CodegenUtils; import software.amazon.smithy.typescript.codegen.TypeScriptDependency; import software.amazon.smithy.typescript.codegen.TypeScriptWriter; -import software.amazon.smithy.typescript.codegen.validation.SerdeElision; +import software.amazon.smithy.typescript.codegen.knowledge.SerdeElisionIndex; import software.amazon.smithy.utils.OptionalUtils; import software.amazon.smithy.utils.SmithyUnstableApi; @@ -113,12 +113,15 @@ public void generateSharedComponents(GenerationContext context) { ); // Error shapes that only referred in the error event of an eventstream Set errorEventShapes = new TreeSet<>(); + SerdeElisionIndex serdeElisionIndex = SerdeElisionIndex.of(context.getModel()); eventStreamGenerator.generateEventStreamDeserializers( context, service, errorEventShapes, deserializingDocumentShapes, - isErrorCodeInBody + isErrorCodeInBody, + enableSerdeElision(), + serdeElisionIndex ); errorEventShapes.removeIf(deserializingErrorShapes::contains); errorEventShapes.forEach(error -> generateErrorDeserializer(context, error)); @@ -495,7 +498,7 @@ private void generateErrorDeserializer(GenerationContext context, StructureShape writer.write("const body = parseBody($L.body, context);", outputReference); } - if (SerdeElision.forModel(context.getModel()).mayElide(error)) { + if (SerdeElisionIndex.of(context.getModel()).mayElide(error) && enableSerdeElision()) { writer.write("const deserialized: any = _json($L);", getErrorBodyLocation(context, "body")); } else { @@ -613,7 +616,7 @@ protected abstract void deserializeOutputDocument( ); /** - * See {@link software.amazon.smithy.typescript.codegen.validation.SerdeElision}. + * See {@link SerdeElisionIndex}. * * @return whether protocol implementation is compatible with serde elision. */ diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/knowledge/SerdeElisionIndex.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/knowledge/SerdeElisionIndex.java new file mode 100644 index 00000000000..30028eaff41 --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/knowledge/SerdeElisionIndex.java @@ -0,0 +1,131 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.typescript.codegen.knowledge; + +import java.util.HashMap; +import java.util.Map; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.KnowledgeIndex; +import software.amazon.smithy.model.selector.Selector; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ToShapeId; +import software.amazon.smithy.model.traits.IdempotencyTokenTrait; +import software.amazon.smithy.model.traits.JsonNameTrait; +import software.amazon.smithy.model.traits.MediaTypeTrait; +import software.amazon.smithy.model.traits.SparseTrait; +import software.amazon.smithy.model.traits.StreamingTrait; +import software.amazon.smithy.utils.MapUtils; + +/** + * Index of ShapeIds to a boolean indicating whether a shape's serde function + * may be omitted. If the shape is of a certain type, and has no downstream + * incompatible shapes or traits that require additional handling, its serde + * function may be emitted. + */ +public class SerdeElisionIndex implements KnowledgeIndex { + private final Map elisionBinding = new HashMap<>(); + private final Map mutatingTraits = MapUtils.of( + "jsonName", JsonNameTrait.class, + "streaming", StreamingTrait.class, + "mediaType", MediaTypeTrait.class, + "sparse", SparseTrait.class, + "idempotencyToken", IdempotencyTokenTrait.class + ); + + public SerdeElisionIndex(Model model) { + for (Shape shape : model.toSet()) { + elisionBinding.put(shape.toShapeId(), canBeElided(shape, model)); + } + } + + public static SerdeElisionIndex of(Model model) { + return model.getKnowledge(SerdeElisionIndex.class, SerdeElisionIndex::new); + } + + public boolean mayElide(ToShapeId id) { + return elisionBinding.getOrDefault(id.toShapeId(), false); + } + + private boolean canBeElided(Shape shape, Model model) { + if (hasIncompatibleTypes(shape, model, 0)) { + return false; + } + return !hasMutatingTraits(shape, model); + } + + private boolean hasMutatingTraits(Shape shape, Model model) { + for (Map.Entry entry : mutatingTraits.entrySet()) { + if (shape.hasTrait(entry.getValue())) { + return true; + } + if (shape.getMemberTrait(model, entry.getValue()).isPresent()) { + return true; + } + Selector selector = Selector.parse( + "[id = '" + shape.getId() + "']" + " ~> [trait|" + entry.getKey() + "]"); + if (!selector.select(model).isEmpty()) { + return true; + } + } + return false; + } + + private boolean hasIncompatibleTypes(Shape shape, Model model, int depth) { + if (depth > 10) { + return true; // bailout for recursive types. + } + + Shape target = shape; + if (shape.isMemberShape()) { + target = model.expectShape(shape.asMemberShape().get().getTarget()); + } + + switch (target.getType()) { + case LIST: + return hasIncompatibleTypes(target.asListShape().get().getMember(), model, depth + 1); + case SET: + return hasIncompatibleTypes(target.asSetShape().get().getMember(), model, depth + 1); + case STRUCTURE: + return target.asStructureShape().get().getAllMembers().values().stream().anyMatch( + s -> hasIncompatibleTypes(s, model, depth + 1) + ); + case UNION: + return target.asUnionShape().get().getAllMembers().values().stream().anyMatch( + s -> hasIncompatibleTypes(s, model, depth + 1) + ); + case MAP: + return hasIncompatibleTypes(model.getShape(target.asMapShape().get().getValue().getTarget()).get(), + model, depth + 1); + case BIG_DECIMAL: + case BIG_INTEGER: + case BLOB: + case DOCUMENT: + case TIMESTAMP: + case DOUBLE: // possible call to parseFloatString or serializeFloat. + case FLOAT: // possible call to parseFloatString or serializeFloat. + // types that generate parsers. + return true; + case MEMBER: + case OPERATION: + case RESOURCE: + case SERVICE: + // non-applicable types. + return false; + case BOOLEAN: + case BYTE: + case ENUM: + case INTEGER: + case INT_ENUM: + case LONG: + case SHORT: + case STRING: + default: + // compatible types with no special parser. + return false; + } + } +} diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/validation/SerdeElision.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/validation/SerdeElision.java deleted file mode 100644 index 3cdb4c1ce1e..00000000000 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/validation/SerdeElision.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.typescript.codegen.validation; - -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.selector.Selector; -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.SetShape; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeType; -import software.amazon.smithy.model.shapes.StructureShape; -import software.amazon.smithy.model.shapes.UnionShape; -import software.amazon.smithy.model.traits.IdempotencyTokenTrait; -import software.amazon.smithy.model.traits.JsonNameTrait; -import software.amazon.smithy.model.traits.MediaTypeTrait; -import software.amazon.smithy.model.traits.SparseTrait; -import software.amazon.smithy.model.traits.StreamingTrait; -import software.amazon.smithy.model.traits.TimestampFormatTrait; - -/** - * For determining whether a serde function for a shape may be omitted. - */ -public final class SerdeElision { - private static final Map INSTANCES = new ConcurrentHashMap<>(); - private static final SerdeElision NULL_INSTANCE = new SerdeElision(null); - private final Model model; - private final Map cache = new ConcurrentHashMap<>(); - private boolean enabledForModel = false; - - private SerdeElision(Model model) { - this.model = model; - } - - /** - * @param model - cache key. - * @return cached instance for the given model. - */ - public static SerdeElision forModel(Model model) { - if (model == null) { - return NULL_INSTANCE; - } - if (!INSTANCES.containsKey(model)) { - INSTANCES.put(model, new SerdeElision(model)); - } - return INSTANCES.get(model); - } - - /** - * @param shape - to be examined. - * @return whether the shape's serializer/deserializer may be elided. - * To qualify, the shape must contain only booleans, strings, numbers - * and containers thereof, and not have any JsonName replacements or - * other mutation parsing effects like timestamps. - * The protocol context must be JSON (not checked in this method). - */ - public boolean mayElide(Shape shape) { - if (!enabledForModel) { - return false; - } - boolean mayElide = check(shape); - cache.put(shape, mayElide); - return mayElide; - } - - /** - * This method allows the protocol and its serde implementation - * to enable this feature selectively. - * @param enabled - Gate for {@link #mayElide(Shape)}. - * @return this for chaining. - */ - public SerdeElision setEnabledForModel(boolean enabled) { - enabledForModel = enabled; - return this; - } - - /** - * Check for incompatible types and incompatible traits. - * In both cases there exist special serde functions that make - * omission of the serde function impossible without additional - * handling. - */ - private boolean check(Shape shape) { - if (cache.containsKey(shape)) { - return cache.get(shape); - } - - if (isTraitDownstream(shape, JsonNameTrait.class, "jsonName") - || isTraitDownstream(shape, StreamingTrait.class, "streaming") - || isTraitDownstream(shape, MediaTypeTrait.class, "mediaType") - || isTraitDownstream(shape, SparseTrait.class, "sparse") - || isTraitDownstream(shape, TimestampFormatTrait.class, "timestampFormat") - || isTraitDownstream(shape, IdempotencyTokenTrait.class, "idempotencyToken")) { - cache.put(shape, false); - return false; - } - - if (hasIncompatibleTypes(shape)) { - cache.put(shape, false); - return false; - } - - cache.put(shape, true); - return true; - } - - private boolean hasIncompatibleTypes(Shape shape) { - return hasIncompatibleTypes(shape, new HashSet<>(), 0); - } - - /** - * Checks whether incompatible types exist downstream of the shape. - * Incompatible types refers to types that need special serde mapping - * functions, like timestamps. - */ - private boolean hasIncompatibleTypes(Shape shape, Set types, int depth) { - if (depth > 10) { - return true; // bailout for recursive types. - } - - Shape target; - if (shape instanceof MemberShape) { - target = model.getShape(((MemberShape) shape).getTarget()).get(); - } else { - target = shape; - } - - switch (target.getType()) { - case LIST: - ListShape list = (ListShape) target; - return hasIncompatibleTypes(list.getMember(), types, depth + 1); - case SET: - SetShape set = (SetShape) target; - return hasIncompatibleTypes(set.getMember(), types, depth + 1); - case STRUCTURE: - StructureShape structure = (StructureShape) target; - return structure.getAllMembers().values().stream().anyMatch( - s -> hasIncompatibleTypes(s, types, depth + 1) - ); - case UNION: - UnionShape union = (UnionShape) target; - return union.getAllMembers().values().stream().anyMatch( - s -> hasIncompatibleTypes(s, types, depth + 1) - ); - case MAP: - MapShape map = (MapShape) target; - return hasIncompatibleTypes( - model.getShape(map.getValue().getTarget()).get(), - types, - depth + 1 - ); - case BIG_DECIMAL: - case BIG_INTEGER: - case BLOB: - case DOCUMENT: - case TIMESTAMP: - case DOUBLE: // possible call to parseFloatString or serializeFloat. - case FLOAT: // possible call to parseFloatString or serializeFloat. - // types that generate parsers. - return true; - case MEMBER: - case OPERATION: - case RESOURCE: - case SERVICE: - // non-applicable types. - return false; - case BOOLEAN: - case BYTE: - case ENUM: - case INTEGER: - case INT_ENUM: - case LONG: - case SHORT: - case STRING: - default: - // compatible types with no special parser. - return false; - } - } - - private boolean isTraitDownstream(Shape shape, Class trait, String traitName) { - if (shape.hasTrait(trait)) { - return true; - } - - if (shape.getMemberTrait(model, trait).isPresent()) { - return true; - } - - Selector selector = Selector.parse("[id = '" + shape.getId() + "']" + " ~> [trait|" + traitName + "]"); - Set matches = selector.select(model); - boolean found = !matches.isEmpty(); - - if (found) { - return true; - } - - return false; - } -} diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberDeserVisitorTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberDeserVisitorTest.java index 709c9370f59..9d306538565 100644 --- a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberDeserVisitorTest.java +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberDeserVisitorTest.java @@ -29,6 +29,7 @@ import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.SetShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShortShape; import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; @@ -49,6 +50,7 @@ public class DocumentMemberDeserVisitorTest { private static final Format FORMAT = Format.EPOCH_SECONDS; private static GenerationContext mockContext; private static TypeScriptSettings mockSettings; + private static StringShape target = StringShape.builder().id(ShapeId.from("com.smithy.example#FooTarget")).build(); static { mockContext = new GenerationContext(); @@ -64,7 +66,7 @@ public class DocumentMemberDeserVisitorTest { @MethodSource("validMemberTargetTypes") public void providesExpectedDefaults(Shape shape, String expected, MemberShape memberShape) { Shape fakeStruct = StructureShape.builder().id("com.smithy.example#Enclosing").addMember(memberShape).build(); - mockContext.setModel(Model.builder().addShapes(shape, fakeStruct).build()); + mockContext.setModel(Model.builder().addShapes(shape, fakeStruct, target).build()); DocumentMemberDeserVisitor visitor = new DocumentMemberDeserVisitor(mockContext, DATA_SOURCE, FORMAT) { @Override @@ -77,7 +79,7 @@ protected MemberShape getMemberShape() { public static Collection validMemberTargetTypes() { String id = "com.smithy.example#Foo"; - String targetId = id + "Target"; + String targetId = String.valueOf(target.getId()); MemberShape source = MemberShape.builder().id("com.smithy.example#Enclosing$sourceMember").target(id).build(); MemberShape member = MemberShape.builder().id(id + "$member").target(targetId).build(); MemberShape key = MemberShape.builder().id(id + "$key").target(targetId).build(); diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberSerVisitorTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberSerVisitorTest.java index 0fc499855dc..fcee0c535f6 100644 --- a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberSerVisitorTest.java +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/integration/DocumentMemberSerVisitorTest.java @@ -11,6 +11,7 @@ import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.BigDecimalShape; import software.amazon.smithy.model.shapes.BigIntegerShape; import software.amazon.smithy.model.shapes.BlobShape; @@ -51,6 +52,7 @@ public class DocumentMemberSerVisitorTest { mockContext.setProtocolName(PROTOCOL); mockContext.setSymbolProvider(new MockProvider()); mockContext.setWriter(new TypeScriptWriter("foo")); + mockContext.setModel(Model.builder().build()); } @ParameterizedTest diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/knowledge/SerdeElisionIndexTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/knowledge/SerdeElisionIndexTest.java new file mode 100644 index 00000000000..d868afe6a72 --- /dev/null +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/knowledge/SerdeElisionIndexTest.java @@ -0,0 +1,136 @@ +package software.amazon.smithy.typescript.codegen.knowledge; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.SetShape; +import software.amazon.smithy.model.shapes.ShapeId; + +public class SerdeElisionIndexTest { + private static Model model; + + @BeforeAll + public static void before() { + model = Model.assembler() + .addImport(SerdeElisionIndexTest.class.getResource("serde-elision.smithy")) + .assemble() + .unwrap(); + } + + @AfterAll + public static void after() { + model = null; + } + + @Test + public void mayElideSimpleObjects() { + SerdeElisionIndex index = SerdeElisionIndex.of(model); + + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#SimpleString")).get())); + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#SimpleList")).get())); + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#SimpleMap")).get())); + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#SimpleStruct")).get())); + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#Boolean")).get())); + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#Byte")).get())); + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#Enum")).get())); + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#Integer")).get())); + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#IntEnum")).get())); + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#Long")).get())); + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#Short")).get())); + assertTrue(index.mayElide(model.getShape(ShapeId.from("foo.bar#SimpleStruct")).get())); + } + + @Test + public void cannotElideUnsupportedTypes() { + SerdeElisionIndex index = SerdeElisionIndex.of(model); + + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigDecimal")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigInteger")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#Blob")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#Document")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#Timestamp")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#Double")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#Float")).get())); + } + + @Test + public void cannotElideNestedUnsupportedTypes() { + model = model.toBuilder().addShapes( + // Shim set shapes into 2.0 model. + SetShape.builder().id("foo.bar#BigDecimalSet").member(ShapeId.from("foo.bar#BigDecimal")).build(), + SetShape.builder().id("foo.bar#BigIntegerSet").member(ShapeId.from("foo.bar#BigInteger")).build(), + SetShape.builder().id("foo.bar#BlobSet").member(ShapeId.from("foo.bar#Blob")).build(), + SetShape.builder().id("foo.bar#DocumentSet").member(ShapeId.from("foo.bar#Document")).build(), + SetShape.builder().id("foo.bar#TimestampSet").member(ShapeId.from("foo.bar#Timestamp")).build(), + SetShape.builder().id("foo.bar#DoubleSet").member(ShapeId.from("foo.bar#Double")).build(), + SetShape.builder().id("foo.bar#FloatSet").member(ShapeId.from("foo.bar#Float")).build() + ).build(); + SerdeElisionIndex index = SerdeElisionIndex.of(model); + + + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigDecimalList")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigIntegerList")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BlobList")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#DocumentList")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#TimestampList")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#DoubleList")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#FloatList")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigDecimalSet")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigIntegerSet")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BlobSet")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#DocumentSet")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#TimestampSet")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#DoubleSet")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#FloatSet")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigDecimalStructure")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigIntegerStructure")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BlobStructure")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#DocumentStructure")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#TimestampStructure")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#DoubleStructure")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#FloatStructure")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigDecimalUnion")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigIntegerUnion")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BlobUnion")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#DocumentUnion")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#TimestampUnion")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#DoubleUnion")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#FloatUnion")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigDecimalMap")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BigIntegerMap")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#BlobMap")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#DocumentMap")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#TimestampMap")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#DoubleMap")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#FloatMap")).get())); + } + + @Test + public void cannotElideWithMutatingTraits() { + SerdeElisionIndex index = SerdeElisionIndex.of(model); + + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#NestedJsonName")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#JsonNameStructure")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#JsonNameStructure$foo")).get())); + + // Blobs are incompatible types, so we only need to check for @streaming traits on unions. + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#NestedEventStream")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#EventStreamUnion")).get())); + + // Blobs are incompatible types, so we only need to check for @mediaType traits on strings. + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#NestedMediaType")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#MediaTypeString")).get())); + + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#NestedSparseList")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#SparseList")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#NestedSparseMap")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#SparseMap")).get())); + + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#NestedIdempotencyToken")).get())); + assertFalse(index.mayElide(model.getShape(ShapeId.from("foo.bar#IdempotencyTokenStructure")).get())); + } +} diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/validation/SerdeElisionTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/validation/SerdeElisionTest.java deleted file mode 100644 index a580728294a..00000000000 --- a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/validation/SerdeElisionTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package software.amazon.smithy.typescript.codegen.validation; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import java.util.Collections; -import java.util.List; -import org.junit.jupiter.api.Test; -import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.shapes.FloatShape; -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.Shape; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.shapes.StringShape; -import software.amazon.smithy.model.shapes.StructureShape; -import software.amazon.smithy.model.traits.IdempotencyTokenTrait; - -public class SerdeElisionTest { - StringShape string = StringShape.builder() - .id("foo.bar#string_a") - .build(); - - StringShape stringWithTrait = StringShape.builder() - .id("foo.bar#stringWithTrait") - .traits(Collections.singleton(new IdempotencyTokenTrait())) - .build(); - - FloatShape floaty = FloatShape.builder() - .id("foo.bar#float") - .build(); - - @Test - public void mayElide_simpleObjects() { - Model model = getModel(string); - SerdeElision serdeElision = SerdeElision.forModel(getModel(string)).setEnabledForModel(true); - - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#string")).get()), equalTo(true)); - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#list")).get()), equalTo(true)); - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#map")).get()), equalTo(true)); - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#structure")).get()), equalTo(true)); - } - - @Test - public void mayElide_hasBooleanGate() { - Model model = getModel(stringWithTrait); - SerdeElision serdeElision = SerdeElision.forModel(model).setEnabledForModel(false); - - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#string")).get()), equalTo(false)); - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#list")).get()), equalTo(false)); - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#map")).get()), equalTo(false)); - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#structure")).get()), equalTo(false)); - } - - @Test - public void mayElide_bailsOnTypes() { - Model model = getModel(floaty); - SerdeElision serdeElision = SerdeElision.forModel(model).setEnabledForModel(true); - - // string doesn't include float. - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#string")).get()), equalTo(true)); - - // others contain float and cannot qualify. - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#list")).get()), equalTo(false)); - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#map")).get()), equalTo(false)); - assertThat(serdeElision.mayElide(model.getShape(ShapeId.from("foo.bar#structure")).get()), equalTo(false)); - } - - private Model getModel(Shape buildingBlock) { - StringShape string = StringShape.builder() - .id("foo.bar#string") - .build(); - - ListShape list = ListShape.builder() - .id("foo.bar#list") - .member(buildingBlock.getId()) - .build(); - - MapShape map = MapShape.builder() - .id("foo.bar#map") - .key(MemberShape.builder() - .id("foo.bar#map$member") - .target(string.getId()) - .build()) - .value(MemberShape.builder() - .id("foo.bar#map$member") - .target(buildingBlock.getId()) - .build()) - .build(); - - MemberShape memberForString = MemberShape.builder() - .id("foo.bar#structure$string") - .target(string.getId()) - .build(); - - MemberShape memberForList = MemberShape.builder() - .id("foo.bar#structure$list") - .target(list.getId()) - .build(); - - MemberShape memberForMap = MemberShape.builder() - .id("foo.bar#structure$map") - .target(map.getId()) - .build(); - - MemberShape memberForBuildingBlock = MemberShape.builder() - .id("foo.bar#structure$buildingBlock") - .target(buildingBlock.getId()) - .build(); - - StructureShape structure = StructureShape.builder() - .id("foo.bar#structure") - .members( - List.of( - memberForString, memberForList, memberForMap, - memberForBuildingBlock - ) - ) - .build(); - - Model model = Model.builder() - .addShapes( - string, list, map, structure, buildingBlock, - memberForString, memberForList, memberForMap, memberForBuildingBlock - ) - .build(); - - return model; - } -} diff --git a/smithy-typescript-codegen/src/test/resources/software/amazon/smithy/typescript/codegen/knowledge/serde-elision.smithy b/smithy-typescript-codegen/src/test/resources/software/amazon/smithy/typescript/codegen/knowledge/serde-elision.smithy new file mode 100644 index 00000000000..3eb93a38470 --- /dev/null +++ b/smithy-typescript-codegen/src/test/resources/software/amazon/smithy/typescript/codegen/knowledge/serde-elision.smithy @@ -0,0 +1,226 @@ +$version: "2.0" + +namespace foo.bar + +string SimpleString + +list SimpleList { + member: A +} + +map SimpleMap { + key: A + value: A +} + +structure SimpleStruct { + a: A +} + +boolean Boolean + +byte Byte + +enum Enum { + A +} + +integer Integer + +intEnum IntEnum { + A = 1 +} + +long Long + +short Short + +bigDecimal BigDecimal + +bigInteger BigInteger + +blob Blob + +timestamp Timestamp + +document Document + +double Double + +float Float + +list BigDecimalList { + member: BigDecimal +} + +list BigIntegerList { + member: BigInteger +} + +list BlobList { + member: Blob +} + +list DocumentList { + member: Document +} + +list TimestampList { + member: Timestamp +} + +list DoubleList { + member: Double +} + +list FloatList { + member: Float +} + +structure BigDecimalStructure { + member: BigDecimal +} + +structure BigIntegerStructure { + member: BigInteger +} + +structure BlobStructure { + member: Blob +} + +structure DocumentStructure { + member: Document +} + +structure TimestampStructure { + member: Timestamp +} + +structure DoubleStructure { + member: Double +} + +structure FloatStructure { + member: Float +} + +union BigDecimalUnion { + member: BigDecimal +} + +union BigIntegerUnion { + member: BigInteger +} + +union BlobUnion { + member: Blob +} + +union DocumentUnion { + member: Document +} + +union TimestampUnion { + member: Timestamp +} + +union DoubleUnion { + member: Double +} + +union FloatUnion { + member: Float +} + +map BigDecimalMap { + key: String + value: BigDecimal +} + +map BigIntegerMap { + key: String + value: BigInteger +} + +map BlobMap { + key: String + value: Blob +} + +map DocumentMap { + key: String + value: Document +} + +map TimestampMap { + key: String + value: Timestamp +} + +map DoubleMap { + key: String + value: Double +} + +map FloatMap { + key: String + value: Float +} + +structure NestedJsonName { + bar: JsonNameStructure +} + +structure JsonNameStructure { + @jsonName("foo") + foo: A +} + +structure NestedEventStream { + eventStream: EventStreamUnion +} + +@streaming +union EventStreamUnion { + message: Event +} + +structure Event {} + +structure NestedMediaType { + foo: MediaTypeString +} + +@mediaType("video/quicktime") +string MediaTypeString + +structure NestedSparseList { + foo: SparseList +} + +@sparse +list SparseList { + member: String +} + +structure NestedSparseMap { + foo: SparseMap +} + +@sparse +map SparseMap { + key: String + value: String +} + +structure NestedIdempotencyToken { + foo: IdempotencyTokenStructure +} + +structure IdempotencyTokenStructure { + @idempotencyToken + foo: String +} + +string A