From cd5588ba2435698c996638884898831b9a4eb450 Mon Sep 17 00:00:00 2001 From: Thiago Arrais Date: Wed, 9 Oct 2019 11:53:06 -0300 Subject: [PATCH 1/9] First draft for missing property report --- .../openapi/diff/output/ConsoleRender.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java index 6cbd9e6be..e457886f8 100644 --- a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java +++ b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.lang3.StringUtils; @@ -168,9 +169,54 @@ private String itemContent( .append("Schema: ") .append(changedMediaType.isCompatible() ? "Backward compatible" : "Broken compatibility") .append(System.lineSeparator()); + if (!changedMediaType.isCompatible()) { + sb.append(incompatibility(changedMediaType)); + } return sb.toString(); } + private String incompatibility(ComposedChanged changed) { + if (changed.isCoreChanged() == DiffResult.INCOMPATIBLE) { + if (changed instanceof ChangedSchema) { + ChangedSchema cs = (ChangedSchema) changed; + + if (cs.getMissingProperties().size() > 0) { + return cs.getMissingProperties().keySet().stream().collect(Collectors.joining()); + } + } + + return ""; + } else { + if (changed instanceof ChangedSchema) { + ChangedSchema cs = (ChangedSchema) changed; + + return cs.getChangedProperties().keySet().stream().collect(Collectors.joining()) + + "." + + cs.getChangedElements().stream() + .map( + (c) -> + c != null && c instanceof ComposedChanged + ? incompatibility((ComposedChanged) c) + : "") + .collect(Collectors.joining()); + } + + StringBuilder sb = new StringBuilder(); + for (Changed child : changed.getChangedElements()) { + if (child instanceof ComposedChanged) { + String childIncompatibility = incompatibility((ComposedChanged) child); + if (!"".equals(childIncompatibility)) { + sb.append(StringUtils.repeat(' ', 10)) + .append("Missing property: ") + .append(childIncompatibility) + .append(System.lineSeparator()); + } + } + } + return sb.toString(); + } + } + private String ul_param(ChangedParameters changedParameters) { List addParameters = changedParameters.getIncreased(); List delParameters = changedParameters.getMissing(); From 0fdaa1d31f99912700022edbd383b796fe5f30a5 Mon Sep 17 00:00:00 2001 From: Thiago Arrais Date: Thu, 24 Oct 2019 15:08:42 -0300 Subject: [PATCH 2/9] Include array nav in missing prop report --- .../com/qdesrame/openapi/diff/output/ConsoleRender.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java index e457886f8..bf01f11b6 100644 --- a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java +++ b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java @@ -190,7 +190,13 @@ private String incompatibility(ComposedChanged changed) { if (changed instanceof ChangedSchema) { ChangedSchema cs = (ChangedSchema) changed; - return cs.getChangedProperties().keySet().stream().collect(Collectors.joining()) + String description = null; + if (!cs.getChangedProperties().isEmpty()) { + description = cs.getChangedProperties().keySet().stream().collect(Collectors.joining()); + } else if (cs.getItems() != null) { + description = "[n]"; + } + return description + "." + cs.getChangedElements().stream() .map( From 996870294a9b3f155b86682ef92d0899db275905 Mon Sep 17 00:00:00 2001 From: Thiago Arrais Date: Thu, 24 Oct 2019 15:49:59 -0300 Subject: [PATCH 3/9] Report multiple missing properties --- .../openapi/diff/output/ConsoleRender.java | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java index bf01f11b6..855caec4b 100644 --- a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java +++ b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java @@ -170,22 +170,28 @@ private String itemContent( .append(changedMediaType.isCompatible() ? "Backward compatible" : "Broken compatibility") .append(System.lineSeparator()); if (!changedMediaType.isCompatible()) { - sb.append(incompatibility(changedMediaType)); + incompatibility(sb, changedMediaType, ""); } return sb.toString(); } - private String incompatibility(ComposedChanged changed) { + private void incompatibility( + final StringBuilder output, final ComposedChanged changed, final String propPrefix) { if (changed.isCoreChanged() == DiffResult.INCOMPATIBLE) { if (changed instanceof ChangedSchema) { ChangedSchema cs = (ChangedSchema) changed; - if (cs.getMissingProperties().size() > 0) { - return cs.getMissingProperties().keySet().stream().collect(Collectors.joining()); - } + cs.getMissingProperties().keySet().stream() + .forEach( + (propName) -> { + output + .append(StringUtils.repeat(' ', 10)) + .append("Missing property: ") + .append(propPrefix) + .append(propName) + .append(System.lineSeparator()); + }); } - - return ""; } else { if (changed instanceof ChangedSchema) { ChangedSchema cs = (ChangedSchema) changed; @@ -196,30 +202,22 @@ private String incompatibility(ComposedChanged changed) { } else if (cs.getItems() != null) { description = "[n]"; } - return description - + "." - + cs.getChangedElements().stream() - .map( - (c) -> - c != null && c instanceof ComposedChanged - ? incompatibility((ComposedChanged) c) - : "") - .collect(Collectors.joining()); + final String prefix = propPrefix + description + "."; + cs.getChangedElements().stream() + .forEach( + (c) -> { + if (c != null && c instanceof ComposedChanged) { + incompatibility(output, (ComposedChanged) c, prefix); + } + }); + return; } - StringBuilder sb = new StringBuilder(); for (Changed child : changed.getChangedElements()) { if (child instanceof ComposedChanged) { - String childIncompatibility = incompatibility((ComposedChanged) child); - if (!"".equals(childIncompatibility)) { - sb.append(StringUtils.repeat(' ', 10)) - .append("Missing property: ") - .append(childIncompatibility) - .append(System.lineSeparator()); - } + incompatibility(output, (ComposedChanged) child, ""); } } - return sb.toString(); } } From ae1d05ba9b97ab3d5155b9a7dcf90f8a000c4d9f Mon Sep 17 00:00:00 2001 From: Thiago Arrais Date: Thu, 24 Oct 2019 16:01:06 -0300 Subject: [PATCH 4/9] Report changes in parent's props alongside with the child's --- .../openapi/diff/output/ConsoleRender.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java index 855caec4b..3229ed74d 100644 --- a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java +++ b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java @@ -192,31 +192,31 @@ private void incompatibility( .append(System.lineSeparator()); }); } - } else { - if (changed instanceof ChangedSchema) { - ChangedSchema cs = (ChangedSchema) changed; + } - String description = null; - if (!cs.getChangedProperties().isEmpty()) { - description = cs.getChangedProperties().keySet().stream().collect(Collectors.joining()); - } else if (cs.getItems() != null) { - description = "[n]"; - } - final String prefix = propPrefix + description + "."; - cs.getChangedElements().stream() - .forEach( - (c) -> { - if (c != null && c instanceof ComposedChanged) { - incompatibility(output, (ComposedChanged) c, prefix); - } - }); - return; + if (changed instanceof ChangedSchema) { + ChangedSchema cs = (ChangedSchema) changed; + + String description = null; + if (!cs.getChangedProperties().isEmpty()) { + description = cs.getChangedProperties().keySet().stream().collect(Collectors.joining()); + } else if (cs.getItems() != null) { + description = "[n]"; } + final String prefix = propPrefix + description + "."; + cs.getChangedElements().stream() + .forEach( + (c) -> { + if (c != null && c instanceof ComposedChanged) { + incompatibility(output, (ComposedChanged) c, prefix); + } + }); + return; + } - for (Changed child : changed.getChangedElements()) { - if (child instanceof ComposedChanged) { - incompatibility(output, (ComposedChanged) child, ""); - } + for (Changed child : changed.getChangedElements()) { + if (child instanceof ComposedChanged) { + incompatibility(output, (ComposedChanged) child, ""); } } } From 3d28e8d63c50e6971052846f5653f4e99f1515ff Mon Sep 17 00:00:00 2001 From: Thiago Arrais Date: Thu, 24 Oct 2019 16:21:03 -0300 Subject: [PATCH 5/9] Avoid including prefixes for unknown changes --- .../openapi/diff/output/ConsoleRender.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java index 3229ed74d..6fd4ac350 100644 --- a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java +++ b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java @@ -8,7 +8,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.lang3.StringUtils; @@ -188,6 +187,7 @@ private void incompatibility( .append(StringUtils.repeat(' ', 10)) .append("Missing property: ") .append(propPrefix) + .append(".") .append(propName) .append(System.lineSeparator()); }); @@ -199,18 +199,18 @@ private void incompatibility( String description = null; if (!cs.getChangedProperties().isEmpty()) { - description = cs.getChangedProperties().keySet().stream().collect(Collectors.joining()); + cs.getChangedProperties().entrySet().stream() + .forEach( + (entry) -> { + incompatibility( + output, + entry.getValue(), + propPrefix + (propPrefix.isEmpty() ? "" : ".") + entry.getKey()); + }); } else if (cs.getItems() != null) { - description = "[n]"; + incompatibility(output, cs.getItems(), propPrefix + "[n]"); } - final String prefix = propPrefix + description + "."; - cs.getChangedElements().stream() - .forEach( - (c) -> { - if (c != null && c instanceof ComposedChanged) { - incompatibility(output, (ComposedChanged) c, prefix); - } - }); + return; } From 2d84aaafb601f675744a200510698a52352b6961 Mon Sep 17 00:00:00 2001 From: Thiago Arrais Date: Thu, 24 Oct 2019 16:50:52 -0300 Subject: [PATCH 6/9] Report changed types --- .../com/qdesrame/openapi/diff/output/ConsoleRender.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java index 6fd4ac350..311b0357a 100644 --- a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java +++ b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java @@ -191,6 +191,14 @@ private void incompatibility( .append(propName) .append(System.lineSeparator()); }); + + if (cs.isChangedType()) { + output + .append(StringUtils.repeat(' ', 10)) + .append("Changed property type: ") + .append(propPrefix) + .append(System.lineSeparator()); + } } } From 4af1e6fa3bf5929c4a50864ed4845dd7d66565d2 Mon Sep 17 00:00:00 2001 From: Thiago Arrais Date: Thu, 24 Oct 2019 16:52:42 -0300 Subject: [PATCH 7/9] Do not prefix first prop --- .../java/com/qdesrame/openapi/diff/output/ConsoleRender.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java index 311b0357a..01a76a92d 100644 --- a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java +++ b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java @@ -187,7 +187,7 @@ private void incompatibility( .append(StringUtils.repeat(' ', 10)) .append("Missing property: ") .append(propPrefix) - .append(".") + .append(propPrefix.isEmpty() ? "" : ".") .append(propName) .append(System.lineSeparator()); }); From fdf1072f6ae3f16f3d93a9fe2c016a30f4d9465b Mon Sep 17 00:00:00 2001 From: Thiago Arrais Date: Fri, 25 Oct 2019 11:50:58 -0300 Subject: [PATCH 8/9] Report incompatibilities in HTML also --- .../openapi/diff/output/HtmlRender.java | 62 ++++++++++++++++++- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/qdesrame/openapi/diff/output/HtmlRender.java b/src/main/java/com/qdesrame/openapi/diff/output/HtmlRender.java index b2c0d3516..f93d0360c 100644 --- a/src/main/java/com/qdesrame/openapi/diff/output/HtmlRender.java +++ b/src/main/java/com/qdesrame/openapi/diff/output/HtmlRender.java @@ -214,16 +214,72 @@ private ContainerTag li_missingRequest(String name, MediaType request) { } private ContainerTag li_changedRequest(String name, ChangedMediaType request) { - return li().withText(String.format("Changed body: '%s'", name)) - .with(div_changedSchema(request.getSchema())); + ContainerTag li = + li().with(div_changedSchema(request.getSchema())) + .withText(String.format("Changed body: '%s'", name)); + if (request.isIncompatible()) { + li = incompatibility(li, request, ""); + } + return li; } private ContainerTag div_changedSchema(ChangedSchema schema) { ContainerTag div = div(); - div.with(h3("Schema")); + div.with(h3("Schema" + (schema.isIncompatible() ? " incompatible" : ""))); return div; } + private ContainerTag incompatibility( + final ContainerTag output, final ComposedChanged changed, final String propPrefix) { + if (changed.isCoreChanged() == DiffResult.INCOMPATIBLE) { + if (changed instanceof ChangedSchema) { + ChangedSchema cs = (ChangedSchema) changed; + + cs.getMissingProperties().keySet().stream() + .forEach( + (propName) -> { + output.with( + p(String.format( + "Missing property: %s%s%s", + propPrefix, propPrefix.isEmpty() ? "" : ".", propName)) + .withClass("missing")); + }); + + if (cs.isChangedType()) { + output.with(p("Changed property type: " + propPrefix).withClass("missing")); + } + } + } + + if (changed instanceof ChangedSchema) { + ChangedSchema cs = (ChangedSchema) changed; + + String description = null; + if (!cs.getChangedProperties().isEmpty()) { + cs.getChangedProperties().entrySet().stream() + .forEach( + (entry) -> { + incompatibility( + output, + entry.getValue(), + propPrefix + (propPrefix.isEmpty() ? "" : ".") + entry.getKey()); + }); + } else if (cs.getItems() != null) { + incompatibility(output, cs.getItems(), propPrefix + "[n]"); + } + + return output; + } + + for (Changed child : changed.getChangedElements()) { + if (child instanceof ComposedChanged) { + incompatibility(output, (ComposedChanged) child, ""); + } + } + + return output; + } + private ContainerTag ul_param(ChangedParameters changedParameters) { List addParameters = changedParameters.getIncreased(); List delParameters = changedParameters.getMissing(); From 856b4260ddfed58db6216cc8f99d927e655a7823 Mon Sep 17 00:00:00 2001 From: Thiago Arrais Date: Thu, 19 Dec 2019 16:41:28 -0300 Subject: [PATCH 9/9] Avoid checking concrete classes (refactor) Code inspired on MarkdownRender's implementation --- .../openapi/diff/output/ConsoleRender.java | 109 ++++++++++-------- .../openapi/diff/output/HtmlRender.java | 104 ++++++++++------- 2 files changed, 121 insertions(+), 92 deletions(-) diff --git a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java index 01a76a92d..7fe4d8a08 100644 --- a/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java +++ b/src/main/java/com/qdesrame/openapi/diff/output/ConsoleRender.java @@ -3,6 +3,10 @@ import static com.qdesrame.openapi.diff.model.Changed.result; import com.qdesrame.openapi.diff.model.*; +import com.qdesrame.openapi.diff.utils.RefPointer; +import com.qdesrame.openapi.diff.utils.RefType; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.responses.ApiResponse; import java.util.List; @@ -13,11 +17,14 @@ public class ConsoleRender implements Render { private static final int LINE_LENGTH = 74; + protected static RefPointer refPointer = new RefPointer<>(RefType.SCHEMAS); + protected ChangedOpenApi diff; private StringBuilder output; @Override public String render(ChangedOpenApi diff) { + this.diff = diff; output = new StringBuilder(); if (diff.isUnchanged()) { output.append("No differences. Specifications are equivalents"); @@ -169,64 +176,70 @@ private String itemContent( .append(changedMediaType.isCompatible() ? "Backward compatible" : "Broken compatibility") .append(System.lineSeparator()); if (!changedMediaType.isCompatible()) { - incompatibility(sb, changedMediaType, ""); + sb.append(incompatibilities(changedMediaType.getSchema())); } return sb.toString(); } - private void incompatibility( - final StringBuilder output, final ComposedChanged changed, final String propPrefix) { - if (changed.isCoreChanged() == DiffResult.INCOMPATIBLE) { - if (changed instanceof ChangedSchema) { - ChangedSchema cs = (ChangedSchema) changed; - - cs.getMissingProperties().keySet().stream() - .forEach( - (propName) -> { - output - .append(StringUtils.repeat(' ', 10)) - .append("Missing property: ") - .append(propPrefix) - .append(propPrefix.isEmpty() ? "" : ".") - .append(propName) - .append(System.lineSeparator()); - }); - - if (cs.isChangedType()) { - output - .append(StringUtils.repeat(' ', 10)) - .append("Changed property type: ") - .append(propPrefix) - .append(System.lineSeparator()); - } - } + private String incompatibilities(final ChangedSchema schema) { + return incompatibilities("", schema); + } + + private String incompatibilities(String propName, final ChangedSchema schema) { + StringBuilder sb = new StringBuilder(); + if (schema.getItems() != null) { + sb.append(items(propName, schema.getItems())); + } + if (schema.isCoreChanged() == DiffResult.INCOMPATIBLE && schema.isChangedType()) { + String type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); + sb.append(property(propName, "Changed property type", type)); } + String prefix = propName.isEmpty() ? "" : propName + "."; + sb.append( + properties(prefix, "Missing property", schema.getMissingProperties(), schema.getContext())); + schema + .getChangedProperties() + .forEach((name, property) -> sb.append(incompatibilities(prefix + name, property))); + return sb.toString(); + } - if (changed instanceof ChangedSchema) { - ChangedSchema cs = (ChangedSchema) changed; - - String description = null; - if (!cs.getChangedProperties().isEmpty()) { - cs.getChangedProperties().entrySet().stream() - .forEach( - (entry) -> { - incompatibility( - output, - entry.getValue(), - propPrefix + (propPrefix.isEmpty() ? "" : ".") + entry.getKey()); - }); - } else if (cs.getItems() != null) { - incompatibility(output, cs.getItems(), propPrefix + "[n]"); - } + private String items(String propName, ChangedSchema schema) { + StringBuilder sb = new StringBuilder(); + sb.append(incompatibilities(propName + "[n]", schema)); + return sb.toString(); + } - return; + private String properties( + String propPrefix, String title, Map properties, DiffContext context) { + StringBuilder sb = new StringBuilder(); + if (properties != null) { + properties.forEach( + (key, value) -> sb.append(property(propPrefix + key, title, resolve(value)))); } + return sb.toString(); + } - for (Changed child : changed.getChangedElements()) { - if (child instanceof ComposedChanged) { - incompatibility(output, (ComposedChanged) child, ""); - } + protected String property(String name, String title, Schema schema) { + return property(name, title, type(schema)); + } + + protected String property(String name, String title, String type) { + return String.format("%s%s: %s (%s)\n", StringUtils.repeat(' ', 10), title, name, type); + } + + protected Schema resolve(Schema schema) { + return refPointer.resolveRef( + diff.getNewSpecOpenApi().getComponents(), schema, schema.get$ref()); + } + + protected String type(Schema schema) { + String result = "object"; + if (schema instanceof ArraySchema) { + result = "array"; + } else if (schema.getType() != null) { + result = schema.getType(); } + return result; } private String ul_param(ChangedParameters changedParameters) { diff --git a/src/main/java/com/qdesrame/openapi/diff/output/HtmlRender.java b/src/main/java/com/qdesrame/openapi/diff/output/HtmlRender.java index f93d0360c..756bc045a 100644 --- a/src/main/java/com/qdesrame/openapi/diff/output/HtmlRender.java +++ b/src/main/java/com/qdesrame/openapi/diff/output/HtmlRender.java @@ -4,7 +4,11 @@ import static j2html.TagCreator.*; import com.qdesrame.openapi.diff.model.*; +import com.qdesrame.openapi.diff.utils.RefPointer; +import com.qdesrame.openapi.diff.utils.RefType; +import io.swagger.v3.oas.models.media.ArraySchema; import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.responses.ApiResponse; import j2html.tags.ContainerTag; @@ -16,6 +20,8 @@ public class HtmlRender implements Render { private String title; private String linkCss; + protected static RefPointer refPointer = new RefPointer<>(RefType.SCHEMAS); + protected ChangedOpenApi diff; public HtmlRender() { this("Api Change Log", "http://deepoove.com/swagger-diff/stylesheets/demo.css"); @@ -27,6 +33,8 @@ public HtmlRender(String title, String linkCss) { } public String render(ChangedOpenApi diff) { + this.diff = diff; + List newEndpoints = diff.getNewEndpoints(); ContainerTag ol_newEndpoint = ol_newEndpoint(newEndpoints); @@ -218,7 +226,7 @@ private ContainerTag li_changedRequest(String name, ChangedMediaType request) { li().with(div_changedSchema(request.getSchema())) .withText(String.format("Changed body: '%s'", name)); if (request.isIncompatible()) { - li = incompatibility(li, request, ""); + incompatibilities(li, request.getSchema()); } return li; } @@ -229,55 +237,63 @@ private ContainerTag div_changedSchema(ChangedSchema schema) { return div; } - private ContainerTag incompatibility( - final ContainerTag output, final ComposedChanged changed, final String propPrefix) { - if (changed.isCoreChanged() == DiffResult.INCOMPATIBLE) { - if (changed instanceof ChangedSchema) { - ChangedSchema cs = (ChangedSchema) changed; - - cs.getMissingProperties().keySet().stream() - .forEach( - (propName) -> { - output.with( - p(String.format( - "Missing property: %s%s%s", - propPrefix, propPrefix.isEmpty() ? "" : ".", propName)) - .withClass("missing")); - }); - - if (cs.isChangedType()) { - output.with(p("Changed property type: " + propPrefix).withClass("missing")); - } - } + private void incompatibilities(final ContainerTag output, final ChangedSchema schema) { + incompatibilities(output, "", schema); + } + + private void incompatibilities( + final ContainerTag output, String propName, final ChangedSchema schema) { + if (schema.getItems() != null) { + items(output, propName, schema.getItems()); + } + if (schema.isCoreChanged() == DiffResult.INCOMPATIBLE && schema.isChangedType()) { + String type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); + property(output, propName, "Changed property type", type); } + String prefix = propName.isEmpty() ? "" : propName + "."; + properties( + output, prefix, "Missing property", schema.getMissingProperties(), schema.getContext()); + schema + .getChangedProperties() + .forEach((name, property) -> incompatibilities(output, prefix + name, property)); + } - if (changed instanceof ChangedSchema) { - ChangedSchema cs = (ChangedSchema) changed; - - String description = null; - if (!cs.getChangedProperties().isEmpty()) { - cs.getChangedProperties().entrySet().stream() - .forEach( - (entry) -> { - incompatibility( - output, - entry.getValue(), - propPrefix + (propPrefix.isEmpty() ? "" : ".") + entry.getKey()); - }); - } else if (cs.getItems() != null) { - incompatibility(output, cs.getItems(), propPrefix + "[n]"); - } + private void items(ContainerTag output, String propName, ChangedSchema schema) { + incompatibilities(output, propName + "[n]", schema); + } - return output; + private void properties( + ContainerTag output, + String propPrefix, + String title, + Map properties, + DiffContext context) { + if (properties != null) { + properties.forEach((key, value) -> property(output, propPrefix + key, title, resolve(value))); } + } - for (Changed child : changed.getChangedElements()) { - if (child instanceof ComposedChanged) { - incompatibility(output, (ComposedChanged) child, ""); - } - } + protected void property(ContainerTag output, String name, String title, Schema schema) { + property(output, name, title, type(schema)); + } + + protected void property(ContainerTag output, String name, String title, String type) { + output.with(p(String.format("%s: %s (%s)", title, name, type)).withClass("missing")); + } + + protected Schema resolve(Schema schema) { + return refPointer.resolveRef( + diff.getNewSpecOpenApi().getComponents(), schema, schema.get$ref()); + } - return output; + protected String type(Schema schema) { + String result = "object"; + if (schema instanceof ArraySchema) { + result = "array"; + } else if (schema.getType() != null) { + result = schema.getType(); + } + return result; } private ContainerTag ul_param(ChangedParameters changedParameters) {