Skip to content

Commit 15a850f

Browse files
Enable running ModelDiff on specific changes
This introduces a builder to the Differences class that allows it to be constructed with only a specific, limited set of changes. This enables comparison of two separate shapes.
1 parent 45673b3 commit 15a850f

File tree

3 files changed

+289
-31
lines changed

3 files changed

+289
-31
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "feature",
3+
"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.",
4+
"pull_requests": [
5+
"[#2796](https://github.com/smithy-lang/smithy/pull/2796/)"
6+
]
7+
}

smithy-diff/src/main/java/software/amazon/smithy/diff/Differences.java

Lines changed: 270 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,60 @@
44
*/
55
package software.amazon.smithy.diff;
66

7-
import java.util.ArrayList;
7+
import java.util.Collection;
88
import java.util.List;
9+
import java.util.Map;
910
import java.util.Objects;
11+
import java.util.Optional;
1012
import java.util.stream.Stream;
1113
import software.amazon.smithy.model.Model;
1214
import software.amazon.smithy.model.node.Node;
1315
import software.amazon.smithy.model.shapes.Shape;
16+
import software.amazon.smithy.utils.BuilderRef;
1417
import software.amazon.smithy.utils.Pair;
18+
import software.amazon.smithy.utils.SmithyBuilder;
19+
import software.amazon.smithy.utils.ToSmithyBuilder;
1520

1621
/**
1722
* Queryable container for detected structural differences between two models.
1823
*/
19-
public final class Differences {
24+
public final class Differences implements ToSmithyBuilder<Differences> {
2025
private final Model oldModel;
2126
private final Model newModel;
22-
private final List<ChangedShape<Shape>> changedShapes = new ArrayList<>();
23-
private final List<ChangedMetadata> changedMetadata = new ArrayList<>();
2427

25-
private Differences(Model oldModel, Model newModel) {
26-
this.oldModel = oldModel;
27-
this.newModel = newModel;
28-
detectMetadataChanges(oldModel, newModel, this);
29-
detectShapeChanges(oldModel, newModel, this);
28+
private final List<Shape> addedShapes;
29+
private final List<Shape> removedShapes;
30+
private final List<ChangedShape<Shape>> changedShapes;
31+
32+
private final List<Pair<String, Node>> addedMetadata;
33+
private final List<Pair<String, Node>> removedMetadata;
34+
private final List<ChangedMetadata> changedMetadata;
35+
36+
private Differences(Builder builder) {
37+
this.oldModel = SmithyBuilder.requiredState("oldModel", builder.oldModel);
38+
this.newModel = SmithyBuilder.requiredState("newModel", builder.newModel);
39+
this.addedShapes = builder.addedShapes.copy();
40+
this.removedShapes = builder.removedShapes.copy();
41+
this.changedShapes = builder.changedShapes.copy();
42+
this.addedMetadata = builder.addedMetadata.copy();
43+
this.removedMetadata = builder.removedMetadata.copy();
44+
this.changedMetadata = builder.changedMetadata.copy();
3045
}
3146

32-
static Differences detect(Model oldModel, Model newModel) {
33-
return new Differences(oldModel, newModel);
47+
/**
48+
* Detects all differences between two models.
49+
*
50+
* @param oldModel The previous state of the model.
51+
* @param newModel The new state of the model.
52+
* @return The set of differences between the two models.
53+
*/
54+
public static Differences detect(Model oldModel, Model newModel) {
55+
return builder()
56+
.oldModel(oldModel)
57+
.newModel(newModel)
58+
.detectShapeChanges()
59+
.detectMetadataChanges()
60+
.build();
3461
}
3562

3663
/**
@@ -57,7 +84,7 @@ public Model getNewModel() {
5784
* @return Returns a stream of each added shape.
5885
*/
5986
public Stream<Shape> addedShapes() {
60-
return newModel.shapes().filter(shape -> !oldModel.getShape(shape.getId()).isPresent());
87+
return addedShapes.stream();
6188
}
6289

6390
/**
@@ -93,7 +120,7 @@ public Stream<Pair<String, Node>> addedMetadata() {
93120
* @return Returns a stream of each removed shape.
94121
*/
95122
public Stream<Shape> removedShapes() {
96-
return oldModel.shapes().filter(shape -> !newModel.getShape(shape.getId()).isPresent());
123+
return removedShapes.stream();
97124
}
98125

99126
/**
@@ -116,11 +143,7 @@ public <T extends Shape> Stream<T> removedShapes(Class<T> shapeType) {
116143
* @return Returns a stream of removed metadata.
117144
*/
118145
public Stream<Pair<String, Node>> removedMetadata() {
119-
return oldModel.getMetadata()
120-
.entrySet()
121-
.stream()
122-
.filter(entry -> !newModel.getMetadata().containsKey(entry.getKey()))
123-
.map(entry -> Pair.of(entry.getKey(), entry.getValue()));
146+
return removedMetadata.stream();
124147
}
125148

126149
/**
@@ -174,21 +197,238 @@ public int hashCode() {
174197
return Objects.hash(getOldModel(), getNewModel());
175198
}
176199

177-
private static void detectShapeChanges(Model oldModel, Model newModel, Differences differences) {
178-
for (Shape oldShape : oldModel.toSet()) {
179-
newModel.getShape(oldShape.getId()).ifPresent(newShape -> {
180-
if (!oldShape.equals(newShape)) {
181-
differences.changedShapes.add(new ChangedShape<>(oldShape, newShape));
200+
201+
/**
202+
* Constructs a Builder for {@link Differences}.
203+
*
204+
* <p>For most uses of {@link Differences}, it should be constructed with {@link Differences#detect}.
205+
*/
206+
public static Builder builder() {
207+
return new Builder();
208+
}
209+
210+
@Override
211+
public SmithyBuilder<Differences> toBuilder() {
212+
return builder()
213+
.oldModel(oldModel)
214+
.newModel(newModel)
215+
.addedShapes(addedShapes)
216+
.removedShapes(removedShapes)
217+
.changedShapes(changedShapes)
218+
.addedMetadata(addedMetadata)
219+
.removedMetadata(removedMetadata)
220+
.changedMetadata(changedMetadata);
221+
}
222+
223+
/**
224+
* A Builder for {@link Differences}.
225+
*
226+
* <p>For most uses of {@link Differences}, it should be constructed with {@link Differences#detect}.
227+
*
228+
* <p>This is intended to be used for evaluating subsets of the models or synthetic
229+
* differences between them. For example, two completely different shapes could be
230+
* evaluated against each other to see what the differences are.
231+
*/
232+
public static final class Builder implements SmithyBuilder<Differences> {
233+
private Model oldModel;
234+
private Model newModel;
235+
236+
private final BuilderRef<List<Shape>> addedShapes = BuilderRef.forList();
237+
private final BuilderRef<List<Shape>> removedShapes = BuilderRef.forList();
238+
private final BuilderRef<List<ChangedShape<Shape>>> changedShapes = BuilderRef.forList();
239+
240+
private final BuilderRef<List<Pair<String, Node>>> addedMetadata = BuilderRef.forList();
241+
private final BuilderRef<List<Pair<String, Node>>> removedMetadata = BuilderRef.forList();
242+
private final BuilderRef<List<ChangedMetadata>> changedMetadata = BuilderRef.forList();
243+
244+
@Override
245+
public Differences build() {
246+
return new Differences(this);
247+
}
248+
249+
/**
250+
* Sets the model to be used as the base model.
251+
*
252+
* @param oldModel The model to base changes on.
253+
* @return Returns the builder.
254+
*/
255+
public Builder oldModel(Model oldModel) {
256+
this.oldModel = oldModel;
257+
return this;
258+
}
259+
260+
/**
261+
* Sets the model to be used as the new model state.
262+
*
263+
* @param newModel The model to use as the new state.
264+
* @return Returns the builder.
265+
*/
266+
public Builder newModel(Model newModel) {
267+
this.newModel = newModel;
268+
return this;
269+
}
270+
271+
/**
272+
* Sets what shapes have been added.
273+
*
274+
* <p>For most uses, {@link #detectShapeChanges()} or {@link Differences#detect(Model, Model)}
275+
* should be used instead.
276+
*
277+
* @param addedShapes The shapes to consider as having been added.
278+
* @return Returns the builder.
279+
*/
280+
public Builder addedShapes(Collection<Shape> addedShapes) {
281+
this.addedShapes.clear();
282+
this.addedShapes.get().addAll(addedShapes);
283+
return this;
284+
}
285+
286+
/**
287+
* Sets what shapes have been removed.
288+
*
289+
* <p>For most uses, {@link #detectShapeChanges()} or {@link Differences#detect(Model, Model)}
290+
* should be used instead.
291+
*
292+
* @param removedShapes The shapes to consider as having been removed.
293+
* @return Returns the builder.
294+
*/
295+
public Builder removedShapes(Collection<Shape> removedShapes) {
296+
this.removedShapes.clear();
297+
this.removedShapes.get().addAll(removedShapes);
298+
return this;
299+
}
300+
301+
/**
302+
* Sets what shapes have been changed.
303+
*
304+
* <p>For most uses, {@link #detectShapeChanges()} or {@link Differences#detect(Model, Model)}
305+
* should be used instead.
306+
*
307+
* @param changedShapes The shapes to consider as having changed.
308+
* @return Returns the builder.
309+
*/
310+
public Builder changedShapes(Collection<ChangedShape<Shape>> changedShapes) {
311+
this.changedShapes.clear();
312+
this.changedShapes.get().addAll(changedShapes);
313+
return this;
314+
}
315+
316+
/**
317+
* Adds a shape to the set of shapes that have been changed.
318+
*
319+
* <p>For most uses, {@link #detectShapeChanges()} or {@link Differences#detect(Model, Model)}
320+
* should be used instead.
321+
*
322+
* @param changedShape A shape to consider as having changed.
323+
* @return Returns the builder.
324+
*/
325+
public Builder changedShape(ChangedShape<Shape> changedShape) {
326+
this.changedShapes.get().add(changedShape);
327+
return this;
328+
}
329+
330+
/**
331+
* Sets the metadata that is considered to have been added.
332+
*
333+
* <p>For most uses, {@link #detectMetadataChanges()} or {@link Differences#detect(Model, Model)}
334+
* should be used instead.
335+
*
336+
* @param addedMetadata The metadata to consider as having been added.
337+
* @return Returns the builder.
338+
*/
339+
public Builder addedMetadata(Collection<Pair<String, Node>> addedMetadata) {
340+
this.addedMetadata.clear();
341+
this.addedMetadata.get().addAll(addedMetadata);
342+
return this;
343+
}
344+
345+
/**
346+
* Sets the metadata that is considered to have been removed.
347+
*
348+
* <p>For most uses, {@link #detectMetadataChanges()} or {@link Differences#detect(Model, Model)}
349+
* should be used instead.
350+
*
351+
* @param removedMetadata The metadata to consider as having been removed.
352+
* @return Returns the builder.
353+
*/
354+
public Builder removedMetadata(Collection<Pair<String, Node>> removedMetadata) {
355+
this.removedMetadata.clear();
356+
this.removedMetadata.get().addAll(removedMetadata);
357+
return this;
358+
}
359+
360+
/**
361+
* Sets the metadata that is considered to have been changed.
362+
*
363+
* <p>For most uses, {@link #detectMetadataChanges()} or {@link Differences#detect(Model, Model)}
364+
* should be used instead.
365+
*
366+
* @param changedMetadata The metadata to consider as having been changed.
367+
* @return Returns the builder.
368+
*/
369+
public Builder changedMetadata(Collection<ChangedMetadata> changedMetadata) {
370+
this.changedMetadata.clear();
371+
this.changedMetadata.get().addAll(changedMetadata);
372+
return this;
373+
}
374+
375+
/**
376+
* Detects all shape additions, removals, and changes.
377+
*
378+
* @return Returns the builder.
379+
*/
380+
public Builder detectShapeChanges() {
381+
addedShapes.clear();
382+
removedShapes.clear();
383+
changedShapes.clear();
384+
for (Shape oldShape : oldModel.toSet()) {
385+
Optional<Shape> newShape = newModel.getShape(oldShape.getId());
386+
if (newShape.isPresent()) {
387+
if (!oldShape.equals(newShape.get())) {
388+
changedShapes.get().add(new ChangedShape<>(oldShape, newShape.get()));
389+
}
390+
} else {
391+
removedShapes.get().add(oldShape);
182392
}
183-
});
393+
}
394+
395+
for (Shape newShape : newModel.toSet()) {
396+
if (!oldModel.getShape(newShape.getId()).isPresent()) {
397+
addedShapes.get().add(newShape);
398+
}
399+
}
400+
401+
return this;
184402
}
185-
}
186403

187-
private static void detectMetadataChanges(Model oldModel, Model newModel, Differences differences) {
188-
oldModel.getMetadata().forEach((k, v) -> {
189-
if (newModel.getMetadata().containsKey(k) && !newModel.getMetadata().get(k).equals(v)) {
190-
differences.changedMetadata.add(new ChangedMetadata(k, v, newModel.getMetadata().get(k)));
404+
/**
405+
* Detects all metadata additions, removals, and changes.
406+
*
407+
* @return Returns the builder.
408+
*/
409+
public Builder detectMetadataChanges() {
410+
addedMetadata.clear();
411+
removedMetadata.clear();
412+
changedMetadata.clear();
413+
for (Map.Entry<String, Node> entry : oldModel.getMetadata().entrySet()) {
414+
String k = entry.getKey();
415+
Node v = entry.getValue();
416+
if (newModel.getMetadata().containsKey(k)) {
417+
if (!newModel.getMetadata().get(k).equals(v)) {
418+
changedMetadata.get().add(new ChangedMetadata(k, v, newModel.getMetadata().get(k)));
419+
}
420+
} else {
421+
removedMetadata.get().add(Pair.of(k, v));
422+
}
191423
}
192-
});
424+
425+
for (Map.Entry<String, Node> entry : newModel.getMetadata().entrySet()) {
426+
if (!oldModel.getMetadata().containsKey(entry.getKey())) {
427+
addedMetadata.get().add(Pair.of(entry.getKey(), entry.getValue()));
428+
}
429+
}
430+
431+
return this;
432+
}
193433
}
194434
}

smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,12 +265,23 @@ public Builder newModel(ValidatedResult<Model> newModel) {
265265
* @throws IllegalStateException if {@code oldModel} and {@code newModel} are not set.
266266
*/
267267
public Result compare() {
268+
return compare(Differences.detect(oldModel, newModel));
269+
}
270+
271+
/**
272+
* Performs an evaluation of specific differences between models.
273+
*
274+
* @param differences A specific set of differences to evaluate.
275+
*
276+
* @return Returns the diff {@link Result}.
277+
* @throws IllegalStateException if {@code oldModel} and {@code newModel} are not set.
278+
*/
279+
public Result compare(Differences differences) {
268280
SmithyBuilder.requiredState("oldModel", oldModel);
269281
SmithyBuilder.requiredState("newModel", newModel);
270282

271283
List<DiffEvaluator> evaluators = new ArrayList<>();
272284
ServiceLoader.load(DiffEvaluator.class, classLoader).forEach(evaluators::add);
273-
Differences differences = Differences.detect(oldModel, newModel);
274285

275286
// Applies suppressions and elevates event severities.
276287
ValidationEventDecorator decoratorResult = new ModelBasedEventDecorator()

0 commit comments

Comments
 (0)