Skip to content

Commit

Permalink
Improve member target diff checks for maps
Browse files Browse the repository at this point in the history
This commit updates the ChangedMemberTarget diff evaluator to properly
check changes to map keys and values the same way it checks changes to
list members.
  • Loading branch information
kstich committed Oct 28, 2024
1 parent ce7b765 commit 34d2396
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
import software.amazon.smithy.diff.Differences;
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.ShapeId;
import software.amazon.smithy.model.shapes.ShapeType;
import software.amazon.smithy.model.shapes.SimpleShape;
import software.amazon.smithy.model.traits.EnumTrait;
import software.amazon.smithy.model.traits.Trait;
Expand Down Expand Up @@ -99,7 +101,7 @@ private static List<String> areShapesCompatible(Shape oldShape, Shape newShape)
oldShape.getType(), newShape.getType()));
}

if (!(oldShape instanceof SimpleShape || oldShape instanceof CollectionShape)) {
if (!(oldShape instanceof SimpleShape || oldShape instanceof CollectionShape || oldShape instanceof MapShape)) {
return ListUtils.of(String.format("The name of a %s is significant", oldShape.getType()));
}

Expand All @@ -117,25 +119,42 @@ private static List<String> areShapesCompatible(Shape oldShape, Shape newShape)
}

if (oldShape instanceof CollectionShape) {
MemberShape oldMember = ((CollectionShape) oldShape).getMember();
MemberShape newMember = ((CollectionShape) newShape).getMember();
if (!oldMember.getTarget().equals(newMember.getTarget())) {
results.add(String.format("Both the old and new shapes are a %s, but the old shape targeted "
+ "`%s` while the new shape targets `%s`",
oldShape.getType(),
oldMember.getTarget(),
newMember.getTarget()));
} else if (!oldMember.getAllTraits().equals(newMember.getAllTraits())) {
results.add(String.format("Both the old and new shapes are a %s, but their members have "
+ "differing traits. %s",
oldShape.getType(),
createTraitDiffMessage(oldMember, newMember)));
}
evaluateMember(oldShape.getType(), results,
((CollectionShape) oldShape).getMember(),
((CollectionShape) newShape).getMember());
} else if (oldShape instanceof MapShape) {
MapShape oldMapShape = (MapShape) oldShape;
MapShape newMapShape = (MapShape) newShape;
// Both the key and value need to be evaluated for maps.
evaluateMember(oldShape.getType(), results,
oldMapShape.getKey(),
newMapShape.getKey());
evaluateMember(oldShape.getType(), results,
oldMapShape.getValue(),
newMapShape.getValue());
}

return results;
}

private static void evaluateMember(
ShapeType oldShapeType,
List<String> results,
MemberShape oldMember,
MemberShape newMember
) {
String memberSlug = oldShapeType == ShapeType.MAP ? oldMember.getMemberName() + " " : "";
if (!oldMember.getTarget().equals(newMember.getTarget())) {
results.add(String.format("Both the old and new shapes are a %s, but the old shape %stargeted "
+ "`%s` while the new shape targets `%s`",
oldShapeType, memberSlug, oldMember.getTarget(), newMember.getTarget()));
} else if (!oldMember.getAllTraits().equals(newMember.getAllTraits())) {
results.add(String.format("Both the old and new shapes are a %s, but their %smembers have "
+ "differing traits. %s",
oldShapeType, memberSlug, createTraitDiffMessage(oldMember, newMember)));
}
}

private static String createSimpleMessage(ChangedShape<MemberShape> change, Shape oldTarget, Shape newTarget) {
return String.format(
"The shape targeted by the member `%s` changed from `%s` (%s) to `%s` (%s). ",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,26 @@ public void detectsAcceptableListMemberChangesInNestedTargets() {
+ "backward compatible."));
}

@Test
public void detectsAcceptableMapMemberChangesInNestedTargets() {
Model modelA = Model.assembler()
.addImport(getClass().getResource("changed-member-target-valid-nested2-a.smithy"))
.assemble()
.unwrap();
Model modelB = Model.assembler()
.addImport(getClass().getResource("changed-member-target-valid-nested2-b.smithy"))
.assemble()
.unwrap();
List<ValidationEvent> events = ModelDiff.compare(modelA, modelB);

assertThat(TestHelper.findEvents(events, "ChangedMemberTarget").size(), equalTo(1));
assertThat(TestHelper.findEvents(events, Severity.WARNING).size(), equalTo(1));
assertThat(TestHelper.findEvents(events, "ChangedMemberTarget").get(0).getMessage(),
equalTo("The shape targeted by the member `smithy.example#A$member` changed from "
+ "`smithy.example#B1` (map) to `smithy.example#B2` (map). This was determined "
+ "backward compatible."));
}

@Test
public void detectsInvalidListMemberChangesInNestedTargets() {
Model modelA = Model.assembler()
Expand Down Expand Up @@ -264,4 +284,92 @@ public void detectsInvalidListMemberTargetChange() {
+ "shapes are a list, but the old shape targeted `smithy.example#MyString` while "
+ "the new shape targets `smithy.example#MyString2`."));
}

@Test
public void detectsInvalidMapKeyChangesInNestedTargets() {
Model modelA = Model.assembler()
.addImport(getClass().getResource("changed-member-target-invalid-nested-mapkey1-a.smithy"))
.assemble()
.unwrap();
Model modelB = Model.assembler()
.addImport(getClass().getResource("changed-member-target-invalid-nested-mapkey1-b.smithy"))
.assemble()
.unwrap();
List<ValidationEvent> events = ModelDiff.compare(modelA, modelB);

assertThat(TestHelper.findEvents(events, "ChangedMemberTarget").size(), equalTo(1));
ValidationEvent event = TestHelper.findEvents(events, "ChangedMemberTarget").get(0);
assertThat(event.getSeverity(), equalTo(Severity.ERROR));
assertThat(event.getMessage(),
equalTo("The shape targeted by the member `smithy.example#A$member` changed from "
+ "`smithy.example#B1` (map) to `smithy.example#B2` (map). Both the old and new "
+ "shapes are a map, but their key members have differing traits. The newly targeted "
+ "shape now has the following additional traits: [smithy.api#pattern]."));
}

@Test
public void detectsInvalidMapKeyTargetChange() {
Model modelA = Model.assembler()
.addImport(getClass().getResource("changed-member-target-invalid-nested-mapkey2-a.smithy"))
.assemble()
.unwrap();
Model modelB = Model.assembler()
.addImport(getClass().getResource("changed-member-target-invalid-nested-mapkey2-b.smithy"))
.assemble()
.unwrap();
List<ValidationEvent> events = ModelDiff.compare(modelA, modelB);

assertThat(TestHelper.findEvents(events, "ChangedMemberTarget").size(), equalTo(1));
ValidationEvent event = TestHelper.findEvents(events, "ChangedMemberTarget").get(0);
assertThat(event.getSeverity(), equalTo(Severity.ERROR));
assertThat(event.getMessage(),
equalTo("The shape targeted by the member `smithy.example#A$member` changed from "
+ "`smithy.example#B1` (map) to `smithy.example#B2` (map). Both the old and new "
+ "shapes are a map, but the old shape key targeted `smithy.example#MyString` while "
+ "the new shape targets `smithy.example#MyString2`."));
}

@Test
public void detectsInvalidMapValueChangesInNestedTargets() {
Model modelA = Model.assembler()
.addImport(getClass().getResource("changed-member-target-invalid-nested-mapvalue1-a.smithy"))
.assemble()
.unwrap();
Model modelB = Model.assembler()
.addImport(getClass().getResource("changed-member-target-invalid-nested-mapvalue1-b.smithy"))
.assemble()
.unwrap();
List<ValidationEvent> events = ModelDiff.compare(modelA, modelB);

assertThat(TestHelper.findEvents(events, "ChangedMemberTarget").size(), equalTo(1));
ValidationEvent event = TestHelper.findEvents(events, "ChangedMemberTarget").get(0);
assertThat(event.getSeverity(), equalTo(Severity.ERROR));
assertThat(event.getMessage(),
equalTo("The shape targeted by the member `smithy.example#A$member` changed from "
+ "`smithy.example#B1` (map) to `smithy.example#B2` (map). Both the old and new "
+ "shapes are a map, but their value members have differing traits. The newly targeted "
+ "shape now has the following additional traits: [smithy.api#pattern]."));
}

@Test
public void detectsInvalidMapValueTargetChange() {
Model modelA = Model.assembler()
.addImport(getClass().getResource("changed-member-target-invalid-nested-mapvalue2-a.smithy"))
.assemble()
.unwrap();
Model modelB = Model.assembler()
.addImport(getClass().getResource("changed-member-target-invalid-nested-mapvalue2-b.smithy"))
.assemble()
.unwrap();
List<ValidationEvent> events = ModelDiff.compare(modelA, modelB);

assertThat(TestHelper.findEvents(events, "ChangedMemberTarget").size(), equalTo(1));
ValidationEvent event = TestHelper.findEvents(events, "ChangedMemberTarget").get(0);
assertThat(event.getSeverity(), equalTo(Severity.ERROR));
assertThat(event.getMessage(),
equalTo("The shape targeted by the member `smithy.example#A$member` changed from "
+ "`smithy.example#B1` (map) to `smithy.example#B2` (map). Both the old and new "
+ "shapes are a map, but the old shape value targeted `smithy.example#MyString` while "
+ "the new shape targets `smithy.example#MyString2`."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// See ChangedMemberTargetTest
$version: "2.0"

namespace smithy.example

structure A {
member: B1
}

map B1 {
key: MyString
value: MyString
}

string MyString
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// See ChangedMemberTargetTest
$version: "2.0"

namespace smithy.example

structure A {
member: B2
}

map B2 {
@pattern("^[a-z]+$")
key: MyString

value: MyString
}

string MyString
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// See ChangedMemberTargetTest
$version: "2.0"

namespace smithy.example

structure A {
member: B1
}

map B1 {
key: MyString
value: MyString
}

string MyString
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// See ChangedMemberTargetTest
$version: "2.0"

namespace smithy.example

structure A {
member: B2
}

map B2 {
key: MyString2
value: MyString
}

string MyString

string MyString2
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// See ChangedMemberTargetTest
$version: "2.0"

namespace smithy.example

structure A {
member: B1
}

map B1 {
key: MyString
value: MyString
}

string MyString
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// See ChangedMemberTargetTest
$version: "2.0"

namespace smithy.example

structure A {
member: B2
}

map B2 {
key: MyString

@pattern("^[a-z]+$")
value: MyString
}

string MyString
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// See ChangedMemberTargetTest
$version: "2.0"

namespace smithy.example

structure A {
member: B1
}

map B1 {
key: MyString
value: MyString
}

string MyString
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// See ChangedMemberTargetTest
$version: "2.0"

namespace smithy.example

structure A {
member: B2
}

map B2 {
key: MyString
value: MyString2
}

string MyString

string MyString2
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// See ChangedMemberTargetTest
$version: "2.0"

namespace smithy.example

structure A {
member: B1
}

map B1 {
key: MyString
value: MyString
}

string MyString
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// See ChangedMemberTargetTest
$version: "2.0"

namespace smithy.example

structure A {
member: B2
}

map B2 {
key: MyString
value: MyString
}

string MyString

0 comments on commit 34d2396

Please sign in to comment.