Skip to content
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,7 @@
{
"type": "feature",
"description": "Added a builder to `Differences` in smithy-diff that allows constructing an instance that only contains a set of hand-curated changes. This enables making comparisons that weren't or wouldn't be auto-detected, such as comparing two shapes of the same type to see if they are compatible with each other.",
"pull_requests": [
"[#2796](https://github.com/smithy-lang/smithy/pull/2796/)"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "feature",
"description": "Updated `DiffEvaluator` interface to include an optional method that additionally accepts a `ClassLoader`. This ensures that the loader the evaluation is configured with can be used in any evaluators that may need it.",
"pull_requests": [
"[#2796](https://github.com/smithy-lang/smithy/pull/2796)"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "feature",
"description": "Loosened the `ChangedMemberTarget` diff evaluator to not report `ERROR` level events so often. Now it will always report an `ERROR` if the change is expected to result in codegen type errors, but otherwise it will default to `WARNING` unless differing traits between the targets would result in a higher severity event had those trait changes been applied to the original target.",
"pull_requests": [
"[#2796](https://github.com/smithy-lang/smithy/pull/2796)"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public SourceLocation getSourceLocation() {
* @return Return the shape ID.
*/
public ShapeId getShapeId() {
return newShape.getId();
return oldShape.getId();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ public interface DiffEvaluator {
* @return Returns validation events that are relative to the new model.
*/
List<ValidationEvent> evaluate(Differences differences);

default List<ValidationEvent> evaluate(ClassLoader classLoader, Differences differences) {
return evaluate(differences);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,60 @@
*/
package software.amazon.smithy.diff;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.utils.BuilderRef;
import software.amazon.smithy.utils.Pair;
import software.amazon.smithy.utils.SmithyBuilder;
import software.amazon.smithy.utils.ToSmithyBuilder;

/**
* Queryable container for detected structural differences between two models.
*/
public final class Differences {
public final class Differences implements ToSmithyBuilder<Differences> {
private final Model oldModel;
private final Model newModel;
private final List<ChangedShape<Shape>> changedShapes = new ArrayList<>();
private final List<ChangedMetadata> changedMetadata = new ArrayList<>();

private Differences(Model oldModel, Model newModel) {
this.oldModel = oldModel;
this.newModel = newModel;
detectMetadataChanges(oldModel, newModel, this);
detectShapeChanges(oldModel, newModel, this);
private final List<Shape> addedShapes;
private final List<Shape> removedShapes;
private final List<ChangedShape<Shape>> changedShapes;

private final List<Pair<String, Node>> addedMetadata;
private final List<Pair<String, Node>> removedMetadata;
private final List<ChangedMetadata> changedMetadata;

private Differences(Builder builder) {
this.oldModel = SmithyBuilder.requiredState("oldModel", builder.oldModel);
this.newModel = SmithyBuilder.requiredState("newModel", builder.newModel);
this.addedShapes = builder.addedShapes.copy();
this.removedShapes = builder.removedShapes.copy();
this.changedShapes = builder.changedShapes.copy();
this.addedMetadata = builder.addedMetadata.copy();
this.removedMetadata = builder.removedMetadata.copy();
this.changedMetadata = builder.changedMetadata.copy();
}

static Differences detect(Model oldModel, Model newModel) {
return new Differences(oldModel, newModel);
/**
* Detects all differences between two models.
*
* @param oldModel The previous state of the model.
* @param newModel The new state of the model.
* @return The set of differences between the two models.
*/
public static Differences detect(Model oldModel, Model newModel) {
return builder()
.oldModel(oldModel)
.newModel(newModel)
.detectShapeChanges()
.detectMetadataChanges()
.build();
}

/**
Expand All @@ -57,7 +84,7 @@ public Model getNewModel() {
* @return Returns a stream of each added shape.
*/
public Stream<Shape> addedShapes() {
return newModel.shapes().filter(shape -> !oldModel.getShape(shape.getId()).isPresent());
return addedShapes.stream();
}

/**
Expand Down Expand Up @@ -93,7 +120,7 @@ public Stream<Pair<String, Node>> addedMetadata() {
* @return Returns a stream of each removed shape.
*/
public Stream<Shape> removedShapes() {
return oldModel.shapes().filter(shape -> !newModel.getShape(shape.getId()).isPresent());
return removedShapes.stream();
}

/**
Expand All @@ -116,11 +143,7 @@ public <T extends Shape> Stream<T> removedShapes(Class<T> shapeType) {
* @return Returns a stream of removed metadata.
*/
public Stream<Pair<String, Node>> removedMetadata() {
return oldModel.getMetadata()
.entrySet()
.stream()
.filter(entry -> !newModel.getMetadata().containsKey(entry.getKey()))
.map(entry -> Pair.of(entry.getKey(), entry.getValue()));
return removedMetadata.stream();
}

/**
Expand Down Expand Up @@ -174,21 +197,237 @@ public int hashCode() {
return Objects.hash(getOldModel(), getNewModel());
}

private static void detectShapeChanges(Model oldModel, Model newModel, Differences differences) {
for (Shape oldShape : oldModel.toSet()) {
newModel.getShape(oldShape.getId()).ifPresent(newShape -> {
if (!oldShape.equals(newShape)) {
differences.changedShapes.add(new ChangedShape<>(oldShape, newShape));
/**
* Constructs a Builder for {@link Differences}.
*
* <p>For most uses of {@link Differences}, it should be constructed with {@link Differences#detect}.
*/
public static Builder builder() {
return new Builder();
}

@Override
public SmithyBuilder<Differences> toBuilder() {
return builder()
.oldModel(oldModel)
.newModel(newModel)
.addedShapes(addedShapes)
.removedShapes(removedShapes)
.changedShapes(changedShapes)
.addedMetadata(addedMetadata)
.removedMetadata(removedMetadata)
.changedMetadata(changedMetadata);
}

/**
* A Builder for {@link Differences}.
*
* <p>For most uses of {@link Differences}, it should be constructed with {@link Differences#detect}.
*
* <p>This is intended to be used for evaluating subsets of the models or synthetic
* differences between them. For example, two completely different shapes could be
* evaluated against each other to see what the differences are.
*/
public static final class Builder implements SmithyBuilder<Differences> {
private Model oldModel;
private Model newModel;

private final BuilderRef<List<Shape>> addedShapes = BuilderRef.forList();
private final BuilderRef<List<Shape>> removedShapes = BuilderRef.forList();
private final BuilderRef<List<ChangedShape<Shape>>> changedShapes = BuilderRef.forList();

private final BuilderRef<List<Pair<String, Node>>> addedMetadata = BuilderRef.forList();
private final BuilderRef<List<Pair<String, Node>>> removedMetadata = BuilderRef.forList();
private final BuilderRef<List<ChangedMetadata>> changedMetadata = BuilderRef.forList();

@Override
public Differences build() {
return new Differences(this);
}

/**
* Sets the model to be used as the base model.
*
* @param oldModel The model to base changes on.
* @return Returns the builder.
*/
public Builder oldModel(Model oldModel) {
this.oldModel = oldModel;
return this;
}

/**
* Sets the model to be used as the new model state.
*
* @param newModel The model to use as the new state.
* @return Returns the builder.
*/
public Builder newModel(Model newModel) {
this.newModel = newModel;
return this;
}

/**
* Sets what shapes have been added.
*
* <p>For most uses, {@link #detectShapeChanges()} or {@link Differences#detect(Model, Model)}
* should be used instead.
*
* @param addedShapes The shapes to consider as having been added.
* @return Returns the builder.
*/
public Builder addedShapes(Collection<Shape> addedShapes) {
this.addedShapes.clear();
this.addedShapes.get().addAll(addedShapes);
return this;
}

/**
* Sets what shapes have been removed.
*
* <p>For most uses, {@link #detectShapeChanges()} or {@link Differences#detect(Model, Model)}
* should be used instead.
*
* @param removedShapes The shapes to consider as having been removed.
* @return Returns the builder.
*/
public Builder removedShapes(Collection<Shape> removedShapes) {
this.removedShapes.clear();
this.removedShapes.get().addAll(removedShapes);
return this;
}

/**
* Sets what shapes have been changed.
*
* <p>For most uses, {@link #detectShapeChanges()} or {@link Differences#detect(Model, Model)}
* should be used instead.
*
* @param changedShapes The shapes to consider as having changed.
* @return Returns the builder.
*/
public Builder changedShapes(Collection<ChangedShape<Shape>> changedShapes) {
this.changedShapes.clear();
this.changedShapes.get().addAll(changedShapes);
return this;
}

/**
* Adds a shape to the set of shapes that have been changed.
*
* <p>For most uses, {@link #detectShapeChanges()} or {@link Differences#detect(Model, Model)}
* should be used instead.
*
* @param changedShape A shape to consider as having changed.
* @return Returns the builder.
*/
public Builder changedShape(ChangedShape<Shape> changedShape) {
this.changedShapes.get().add(changedShape);
return this;
}

/**
* Sets the metadata that is considered to have been added.
*
* <p>For most uses, {@link #detectMetadataChanges()} or {@link Differences#detect(Model, Model)}
* should be used instead.
*
* @param addedMetadata The metadata to consider as having been added.
* @return Returns the builder.
*/
public Builder addedMetadata(Collection<Pair<String, Node>> addedMetadata) {
this.addedMetadata.clear();
this.addedMetadata.get().addAll(addedMetadata);
return this;
}

/**
* Sets the metadata that is considered to have been removed.
*
* <p>For most uses, {@link #detectMetadataChanges()} or {@link Differences#detect(Model, Model)}
* should be used instead.
*
* @param removedMetadata The metadata to consider as having been removed.
* @return Returns the builder.
*/
public Builder removedMetadata(Collection<Pair<String, Node>> removedMetadata) {
this.removedMetadata.clear();
this.removedMetadata.get().addAll(removedMetadata);
return this;
}

/**
* Sets the metadata that is considered to have been changed.
*
* <p>For most uses, {@link #detectMetadataChanges()} or {@link Differences#detect(Model, Model)}
* should be used instead.
*
* @param changedMetadata The metadata to consider as having been changed.
* @return Returns the builder.
*/
public Builder changedMetadata(Collection<ChangedMetadata> changedMetadata) {
this.changedMetadata.clear();
this.changedMetadata.get().addAll(changedMetadata);
return this;
}

/**
* Detects all shape additions, removals, and changes.
*
* @return Returns the builder.
*/
public Builder detectShapeChanges() {
addedShapes.clear();
removedShapes.clear();
changedShapes.clear();
for (Shape oldShape : oldModel.toSet()) {
Optional<Shape> newShape = newModel.getShape(oldShape.getId());
if (newShape.isPresent()) {
if (!oldShape.equals(newShape.get())) {
changedShapes.get().add(new ChangedShape<>(oldShape, newShape.get()));
}
} else {
removedShapes.get().add(oldShape);
}
}

for (Shape newShape : newModel.toSet()) {
if (!oldModel.getShape(newShape.getId()).isPresent()) {
addedShapes.get().add(newShape);
}
});
}

return this;
}
}

private static void detectMetadataChanges(Model oldModel, Model newModel, Differences differences) {
oldModel.getMetadata().forEach((k, v) -> {
if (newModel.getMetadata().containsKey(k) && !newModel.getMetadata().get(k).equals(v)) {
differences.changedMetadata.add(new ChangedMetadata(k, v, newModel.getMetadata().get(k)));
/**
* Detects all metadata additions, removals, and changes.
*
* @return Returns the builder.
*/
public Builder detectMetadataChanges() {
addedMetadata.clear();
removedMetadata.clear();
changedMetadata.clear();
for (Map.Entry<String, Node> entry : oldModel.getMetadata().entrySet()) {
String k = entry.getKey();
Node v = entry.getValue();
if (newModel.getMetadata().containsKey(k)) {
if (!newModel.getMetadata().get(k).equals(v)) {
changedMetadata.get().add(new ChangedMetadata(k, v, newModel.getMetadata().get(k)));
}
} else {
removedMetadata.get().add(Pair.of(k, v));
}
}

for (Map.Entry<String, Node> entry : newModel.getMetadata().entrySet()) {
if (!oldModel.getMetadata().containsKey(entry.getKey())) {
addedMetadata.get().add(Pair.of(entry.getKey(), entry.getValue()));
}
}
});

return this;
}
}
}
Loading
Loading