Skip to content

Commit e04cca6

Browse files
mtdowlingMichael Dowling
authored and
Michael Dowling
committed
Add test for synthetic box traits on mixins
This commit also fixes an issue where we would sometimes add empty apply statements to the JSON AST because a mixin introduces no traits. The AST parser now handles this without failing, and the model serializer no longer writes out empty apply blocks.
1 parent 6234f3d commit e04cca6

File tree

9 files changed

+137
-20
lines changed

9 files changed

+137
-20
lines changed

smithy-model/src/main/java/software/amazon/smithy/model/loader/AstModelLoader.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ private LoadOperation.DefineShape loadShape(ShapeId id, String type, ObjectNode
194194
return loadOperation(id, value);
195195
case "apply":
196196
LoaderUtils.checkForAdditionalProperties(value, id, APPLY_PROPERTIES).ifPresent(this::emit);
197-
applyTraits(id, value.expectObjectMember(TRAITS));
197+
value.getObjectMember(TRAITS).ifPresent(traits -> applyTraits(id, traits));
198198
return null;
199199
default:
200200
throw new SourceException("Invalid shape `type`: " + type, value);

smithy-model/src/main/java/software/amazon/smithy/model/shapes/ModelSerializer.java

+27-15
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.Collection;
20+
import java.util.Collections;
2021
import java.util.List;
2122
import java.util.Map;
2223
import java.util.Objects;
@@ -123,18 +124,20 @@ public ObjectNode serialize(Model model) {
123124
if (!shape.isMemberShape() && shapeFilter.test(shape)) {
124125
Node value = shape.accept(shapeSerializer);
125126
shapes.put(Node.from(shape.getId().toString()), value);
126-
// Add any necessary apply statements to inherited mixin members that added traits.
127-
// Apply statements are used here instead of redefining members on structures because
128-
// apply statements are more resilient to change over time if the shapes targeted by
127+
// Add any necessary apply statements to inherited mixin members that added traits, but only if there
128+
// are actually traits to serialize. Apply statements are used here instead of redefining members on
129+
// structures because apply statements are more resilient to change over time if the shapes targeted by
129130
// an inherited member changes.
130131
if (!shapeSerializer.mixinMemberTraits.isEmpty()) {
131132
for (MemberShape member : shapeSerializer.mixinMemberTraits) {
132-
ObjectNode.Builder applyBuilder = Node.objectNodeBuilder();
133-
applyBuilder.withMember("type", "apply");
134-
shapes.put(
135-
Node.from(member.getId().toString()),
136-
serializeTraits(applyBuilder, member.getIntroducedTraits().values()).build()
137-
);
133+
Map<StringNode, Node> introducedTraits = createIntroducedTraitsMap(
134+
member.getIntroducedTraits().values());
135+
if (!introducedTraits.isEmpty()) {
136+
ObjectNode.Builder applyBuilder = Node.objectNodeBuilder();
137+
applyBuilder.withMember("type", "apply");
138+
ObjectNode traits = serializeTraits(applyBuilder, introducedTraits).build();
139+
shapes.put(Node.from(member.getId().toString()), traits);
140+
}
138141
}
139142
}
140143
}
@@ -258,20 +261,29 @@ public ModelSerializer build() {
258261
}
259262

260263
private ObjectNode.Builder serializeTraits(ObjectNode.Builder builder, Collection<Trait> traits) {
264+
return serializeTraits(builder, createIntroducedTraitsMap(traits));
265+
}
266+
267+
private ObjectNode.Builder serializeTraits(ObjectNode.Builder builder, Map<StringNode, Node> traits) {
261268
if (!traits.isEmpty()) {
269+
builder.withMember("traits", new ObjectNode(traits, SourceLocation.none()));
270+
}
271+
272+
return builder;
273+
}
274+
275+
private Map<StringNode, Node> createIntroducedTraitsMap(Collection<Trait> traits) {
276+
if (traits.isEmpty()) {
277+
return Collections.emptyMap();
278+
} else {
262279
Map<StringNode, Node> traitsToAdd = new TreeMap<>();
263280
for (Trait trait : traits) {
264281
if (traitFilter.test(trait)) {
265282
traitsToAdd.put(Node.from(trait.toShapeId().toString()), trait.toNode());
266283
}
267284
}
268-
269-
if (!traitsToAdd.isEmpty()) {
270-
builder.withMember("traits", new ObjectNode(traitsToAdd, SourceLocation.none()));
271-
}
285+
return traitsToAdd;
272286
}
273-
274-
return builder;
275287
}
276288

277289
private final class ShapeSerializer extends ShapeVisitor.Default<Node> {

smithy-model/src/test/java/software/amazon/smithy/model/loader/AstModelLoaderTest.java

+13-4
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@
1515

1616
package software.amazon.smithy.model.loader;
1717

18-
import org.junit.jupiter.api.Test;
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
1920

21+
import org.junit.jupiter.api.Test;
2022
import software.amazon.smithy.model.Model;
2123
import software.amazon.smithy.model.validation.Severity;
2224
import software.amazon.smithy.model.validation.ValidatedResult;
2325

24-
import static org.junit.jupiter.api.Assertions.assertEquals;
25-
import static org.junit.jupiter.api.Assertions.assertTrue;
26-
2726
public class AstModelLoaderTest {
2827
@Test
2928
public void failsToLoadPropertiesFromV1() {
@@ -34,4 +33,14 @@ public void failsToLoadPropertiesFromV1() {
3433
assertTrue(model.getValidationEvents(Severity.ERROR).get(0).getMessage()
3534
.contains("Resource properties can only be used with Smithy version 2 or later."));
3635
}
36+
37+
@Test
38+
public void doesNotFailOnEmptyApply() {
39+
// Empty apply statements are pointless but shouldn't break the loader.
40+
Model.assembler()
41+
.addImport(getClass().getResource("ast-empty-apply-1.json"))
42+
.addImport(getClass().getResource("ast-empty-apply-2.json"))
43+
.assemble()
44+
.unwrap();
45+
}
3746
}

smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java

+29
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import software.amazon.smithy.model.node.ObjectNode;
5656
import software.amazon.smithy.model.node.StringNode;
5757
import software.amazon.smithy.model.shapes.MemberShape;
58+
import software.amazon.smithy.model.shapes.ModelSerializer;
5859
import software.amazon.smithy.model.shapes.ServiceShape;
5960
import software.amazon.smithy.model.shapes.SetShape;
6061
import software.amazon.smithy.model.shapes.Shape;
@@ -1079,4 +1080,32 @@ public void syntheticBoxingResultsInSameModelBetween1and2() {
10791080
assertThat(model1, equalTo(model2));
10801081
assertThat(model1, equalTo(model3));
10811082
}
1083+
1084+
@Test
1085+
public void appliesBoxTraitsToMixinsToo() {
1086+
Model model1 = Model.assembler()
1087+
.addImport(getClass().getResource("synthetic-boxing-mixins.smithy"))
1088+
.assemble()
1089+
.unwrap();
1090+
1091+
// MixedInteger and MixinInteger have synthetic box traits.
1092+
assertThat(model1.expectShape(ShapeId.from("smithy.example#MixinInteger")).hasTrait(BoxTrait.class), is(true));
1093+
assertThat(model1.expectShape(ShapeId.from("smithy.example#MixedInteger")).hasTrait(BoxTrait.class), is(true));
1094+
1095+
// MixinStruct$bar and MixedStruct$bar have synthetic box traits.
1096+
StructureShape mixinStruct = model1.expectShape(ShapeId.from("smithy.example#MixinStruct"),
1097+
StructureShape.class);
1098+
StructureShape mixedStruct = model1.expectShape(ShapeId.from("smithy.example#MixedStruct"),
1099+
StructureShape.class);
1100+
assertThat(mixinStruct.getAllMembers().get("bar").hasTrait(BoxTrait.class), is(true));
1101+
assertThat(mixinStruct.getAllMembers().get("bar").hasTrait(DefaultTrait.class), is(true));
1102+
assertThat(mixedStruct.getAllMembers().get("bar").hasTrait(BoxTrait.class), is(true));
1103+
assertThat(mixedStruct.getAllMembers().get("bar").hasTrait(DefaultTrait.class), is(true));
1104+
1105+
// Now ensure round-tripping results in the same model.
1106+
Node serialized = ModelSerializer.builder().build().serialize(model1);
1107+
Model model2 = Model.assembler().addDocumentNode(serialized).assemble().unwrap();
1108+
1109+
assertThat(model1, equalTo(model2));
1110+
}
10821111
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"smithy": "2.0",
3+
"shapes": {
4+
"smithy.example#Foo": {
5+
"type": "string"
6+
}
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"smithy": "2.0",
3+
"shapes": {
4+
"smithy.example#Foo": {
5+
"type": "apply"
6+
}
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
$version: "2.0"
2+
namespace smithy.example
3+
4+
@mixin
5+
integer MixinInteger
6+
7+
integer MixedInteger with [MixinInteger]
8+
9+
@mixin
10+
structure MixinStruct {
11+
bar: MixedInteger = null
12+
}
13+
14+
structure MixedStruct with [MixinStruct] {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"smithy": "2.0",
3+
"shapes": {
4+
"smithy.example#MixedStruct": {
5+
"type": "structure",
6+
"mixins": [
7+
{
8+
"target": "smithy.example#MixinStruct"
9+
}
10+
],
11+
"members": {}
12+
},
13+
"smithy.example#MixinStruct": {
14+
"type": "structure",
15+
"members": {
16+
"bar": {
17+
"target": "smithy.api#PrimitiveInteger",
18+
"traits": {
19+
"smithy.api#default": null
20+
}
21+
}
22+
},
23+
"traits": {
24+
"smithy.api#mixin": {}
25+
}
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
$version: "2.0"
2+
namespace smithy.example
3+
4+
@mixin
5+
structure MixinStruct {
6+
bar: PrimitiveInteger = null
7+
}
8+
9+
structure MixedStruct with [MixinStruct] {}

0 commit comments

Comments
 (0)