Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @required and @default smithy-diff support #1021

Merged
merged 1 commit into from
Dec 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}