Add @required and @default smithy-diff support

You can only add the @default trait to replace the @required trait. You
can only remove the @required trait if it's replaced by the @default
trait or if the containing structure has the @input trait.
This commit is contained in:
Michael Dowling 2021-12-17 15:07:56 -08:00 committed by Michael Dowling
parent f8968b9462
commit 4096eda0ec
5 changed files with 284 additions and 0 deletions

View File

@ -0,0 +1,49 @@
/*
* Copyright 2021 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.diff.evaluators;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import software.amazon.smithy.diff.Differences;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.traits.DefaultTrait;
import software.amazon.smithy.model.traits.RequiredTrait;
import software.amazon.smithy.model.validation.ValidationEvent;
/**
* The default trait can only be added to shape if it's replacing the
* required trait.
*/
public class AddedDefaultTrait extends AbstractDiffEvaluator {
@Override
public List<ValidationEvent> evaluate(Differences differences) {
return differences.changedShapes(MemberShape.class)
.map(change -> {
MemberShape oldShape = change.getOldShape();
MemberShape newShape = change.getNewShape();
if (newShape.hasTrait(DefaultTrait.class)
&& !oldShape.hasTrait(DefaultTrait.class)
&& !oldShape.hasTrait(RequiredTrait.class)) {
return error(newShape, "Added the @default trait. This is only backward compatible if "
+ "the @default trait is used to replace the @required trait.");
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2021 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.diff.evaluators;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import software.amazon.smithy.diff.Differences;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.traits.DefaultTrait;
import software.amazon.smithy.model.traits.InputTrait;
import software.amazon.smithy.model.traits.RequiredTrait;
import software.amazon.smithy.model.validation.ValidationEvent;
/**
* When removing the required trait, it has to be replaced with the default
* trait, unless the containing structure is marked with the input trait.
*/
public class RemovedRequiredTrait extends AbstractDiffEvaluator {
@Override
public List<ValidationEvent> evaluate(Differences differences) {
return differences.changedShapes(MemberShape.class)
.map(change -> {
MemberShape oldShape = change.getOldShape();
MemberShape newShape = change.getNewShape();
if (oldShape.hasTrait(RequiredTrait.class)
&& !newShape.hasTrait(RequiredTrait.class)
&& !newShape.hasTrait(DefaultTrait.class)
&& !containerHasInputTrait(differences.getNewModel(), newShape)) {
return error(newShape, "Removed the @required trait without replacing it with the @default "
+ "trait. Code generated for this structure will change in a backward "
+ "incompatible way in many languages, including Rust, Kotlin, Swift, "
+ "and many others.");
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
private boolean containerHasInputTrait(Model model, MemberShape member) {
return model.getShape(member.getContainer())
.filter(container -> container.hasTrait(InputTrait.class))
.isPresent();
}
}

View File

@ -1,3 +1,4 @@
software.amazon.smithy.diff.evaluators.AddedDefaultTrait
software.amazon.smithy.diff.evaluators.AddedEntityBinding
software.amazon.smithy.diff.evaluators.AddedMetadata
software.amazon.smithy.diff.evaluators.AddedOperationError
@ -19,6 +20,7 @@ software.amazon.smithy.diff.evaluators.RemovedAuthenticationScheme
software.amazon.smithy.diff.evaluators.RemovedEntityBinding
software.amazon.smithy.diff.evaluators.RemovedMetadata
software.amazon.smithy.diff.evaluators.RemovedOperationError
software.amazon.smithy.diff.evaluators.RemovedRequiredTrait
software.amazon.smithy.diff.evaluators.RemovedServiceError
software.amazon.smithy.diff.evaluators.RemovedShape
software.amazon.smithy.diff.evaluators.RemovedTraitDefinition

View File

@ -0,0 +1,75 @@
/*
* Copyright 2021 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.diff.evaluators;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.diff.ModelDiff;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.StringShape;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.traits.DefaultTrait;
import software.amazon.smithy.model.traits.RequiredTrait;
import software.amazon.smithy.model.validation.Severity;
public class AddedDefaultTraitTest {
@Test
public void replacingRequiredTraitWithDefaultIsOk() {
StringShape s = StringShape.builder().id("smithy.example#Str").build();
StructureShape a = StructureShape.builder()
.id("smithy.example#A")
.addMember("foo", s.getId(), b1 -> b1.addTrait(new RequiredTrait()))
.build();
StructureShape b = StructureShape.builder()
.id("smithy.example#A")
.addMember("foo", s.getId(), b2 -> b2.addTrait(new DefaultTrait()))
.build();
Model model1 = Model.builder().addShapes(s, a).build();
Model model2 = Model.builder().addShapes(s, b).build();
ModelDiff.Result result = ModelDiff.builder().oldModel(model1).newModel(model2).compare();
assertThat(result.getDiffEvents().stream()
.filter(event -> event.getId().equals("AddedDefaultTrait"))
.count(), equalTo(0L));
}
@Test
public void detectsInvalidAdditionOfDefaultTrait() {
StringShape s = StringShape.builder().id("smithy.example#Str").build();
StructureShape a = StructureShape.builder()
.id("smithy.example#A")
.addMember("foo", s.getId())
.build();
StructureShape b = StructureShape.builder()
.id("smithy.example#A")
.addMember("foo", s.getId(), builder -> builder.addTrait(new DefaultTrait()))
.build();
Model model1 = Model.builder().addShapes(s, a).build();
Model model2 = Model.builder().addShapes(s, b).build();
ModelDiff.Result result = ModelDiff.builder().oldModel(model1).newModel(model2).compare();
assertThat(result.isDiffBreaking(), is(true));
assertThat(result.getDiffEvents().stream()
.filter(event -> event.getSeverity() == Severity.ERROR)
.filter(event -> event.getId().equals("AddedDefaultTrait"))
.filter(event -> event.getShapeId().get().equals(a.getAllMembers().get("foo").getId()))
.filter(event -> event.getMessage().contains("Added the @default trait"))
.count(), equalTo(1L));
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2021 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.diff.evaluators;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.diff.ModelDiff;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.StringShape;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.traits.DefaultTrait;
import software.amazon.smithy.model.traits.InputTrait;
import software.amazon.smithy.model.traits.RequiredTrait;
import software.amazon.smithy.model.validation.Severity;
public class RemovedRequiredTraitTest {
@Test
public void replacingRequiredTraitWithDefaultIsOk() {
StringShape s = StringShape.builder().id("smithy.example#Str").build();
StructureShape a = StructureShape.builder()
.id("smithy.example#A")
.addMember("foo", s.getId(), b1 -> b1.addTrait(new RequiredTrait()))
.build();
StructureShape b = StructureShape.builder()
.id("smithy.example#A")
.addMember("foo", s.getId(), b2 -> b2.addTrait(new DefaultTrait()))
.build();
Model model1 = Model.builder().addShapes(s, a).build();
Model model2 = Model.builder().addShapes(s, b).build();
ModelDiff.Result result = ModelDiff.builder().oldModel(model1).newModel(model2).compare();
assertThat(result.getDiffEvents().stream()
.filter(event -> event.getId().equals("RemovedRequiredTrait"))
.count(), equalTo(0L));
}
@Test
public void removingTheRequiredTraitOnInputStructureIsOk() {
StringShape s = StringShape.builder().id("smithy.example#Str").build();
StructureShape a = StructureShape.builder()
.addTrait(new InputTrait())
.id("smithy.example#A")
.addMember("foo", s.getId(), b1 -> b1.addTrait(new RequiredTrait()))
.build();
StructureShape b = StructureShape.builder()
.addTrait(new InputTrait())
.id("smithy.example#A")
.addMember("foo", s.getId())
.build();
Model model1 = Model.builder().addShapes(s, a).build();
Model model2 = Model.builder().addShapes(s, b).build();
ModelDiff.Result result = ModelDiff.builder().oldModel(model1).newModel(model2).compare();
assertThat(result.getDiffEvents().stream()
.filter(event -> event.getId().equals("RemovedRequiredTrait"))
.count(), equalTo(0L));
}
@Test
public void detectsInvalidRemovalOfRequired() {
StringShape s = StringShape.builder().id("smithy.example#Str").build();
StructureShape a = StructureShape.builder()
.id("smithy.example#A")
.addMember("foo", s.getId(), b1 -> b1.addTrait(new RequiredTrait()))
.build();
StructureShape b = StructureShape.builder()
.id("smithy.example#A")
.addMember("foo", s.getId())
.build();
Model model1 = Model.builder().addShapes(s, a).build();
Model model2 = Model.builder().addShapes(s, b).build();
ModelDiff.Result result = ModelDiff.builder().oldModel(model1).newModel(model2).compare();
assertThat(result.isDiffBreaking(), is(true));
assertThat(result.getDiffEvents().stream()
.filter(event -> event.getSeverity() == Severity.ERROR)
.filter(event -> event.getId().equals("RemovedRequiredTrait"))
.filter(event -> event.getShapeId().get().equals(a.getAllMembers().get("foo").getId()))
.filter(event -> event.getMessage().contains("Removed the @required trait"))
.count(), equalTo(1L));
}
}