44 */
55package software .amazon .smithy .diff ;
66
7- import java .util .ArrayList ;
7+ import java .util .Collection ;
88import java .util .List ;
9+ import java .util .Map ;
910import java .util .Objects ;
11+ import java .util .Optional ;
1012import java .util .stream .Stream ;
1113import software .amazon .smithy .model .Model ;
1214import software .amazon .smithy .model .node .Node ;
1315import software .amazon .smithy .model .shapes .Shape ;
16+ import software .amazon .smithy .utils .BuilderRef ;
1417import 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}
0 commit comments