From d76bb8c99e02b1e42c3cfb3ab20c5a5f6c1a7e06 Mon Sep 17 00:00:00 2001 From: dlwlrma <965810157@qq.com> Date: Mon, 28 Sep 2026 13:59:15 +0800 Subject: [PATCH 1/3] fix: show media type example changes in reports --- .../core/output/AsciidocRender.java | 7 + .../core/output/ConsoleRender.java | 11 + .../openapidiff/core/output/HtmlRender.java | 3 + .../core/output/MarkdownRender.java | 12 ++ .../core/output/MediaTypeExampleChanges.java | 53 +++++ .../output/MediaTypeExampleRenderTest.java | 188 ++++++++++++++++++ .../issue-872-media-type-examples.yaml | 25 +++ 7 files changed, 299 insertions(+) create mode 100644 core/src/main/java/org/openapitools/openapidiff/core/output/MediaTypeExampleChanges.java create mode 100644 core/src/test/java/org/openapitools/openapidiff/core/output/MediaTypeExampleRenderTest.java create mode 100644 core/src/test/resources/issue-872-media-type-examples.yaml diff --git a/core/src/main/java/org/openapitools/openapidiff/core/output/AsciidocRender.java b/core/src/main/java/org/openapitools/openapidiff/core/output/AsciidocRender.java index 89583847..edf442ce 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/output/AsciidocRender.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/AsciidocRender.java @@ -207,6 +207,13 @@ private String itemContent( if (!changedMediaType.isCompatible() && changedMediaType.getSchema() != null) { sb.append(incompatibilities(changedMediaType.getSchema())); } + MediaTypeExampleChanges.render(changedMediaType) + .forEach( + (label, value) -> + sb.append(itemContent(label + ":", "", indent + 1)) + .append("+\n[source,json]\n----\n") + .append(value) + .append("\n----\n")); return sb.toString(); } diff --git a/core/src/main/java/org/openapitools/openapidiff/core/output/ConsoleRender.java b/core/src/main/java/org/openapitools/openapidiff/core/output/ConsoleRender.java index 2b860723..3f3b0ee5 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/output/ConsoleRender.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/ConsoleRender.java @@ -181,6 +181,17 @@ private String itemContent( if (!changedMediaType.isCompatible() && changedMediaType.getSchema() != null) { sb.append(incompatibilities(changedMediaType.getSchema())); } + MediaTypeExampleChanges.render(changedMediaType) + .forEach( + (label, value) -> + sb.append(StringUtils.repeat(' ', 10)) + .append(label) + .append(":") + .append(System.lineSeparator()) + .append(StringUtils.repeat(' ', 12)) + .append( + value.replace("\n", System.lineSeparator() + StringUtils.repeat(' ', 12))) + .append(System.lineSeparator())); return sb.toString(); } diff --git a/core/src/main/java/org/openapitools/openapidiff/core/output/HtmlRender.java b/core/src/main/java/org/openapitools/openapidiff/core/output/HtmlRender.java index 441d1c9d..4ca94ff8 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/output/HtmlRender.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/HtmlRender.java @@ -16,6 +16,7 @@ import static j2html.TagCreator.meta; import static j2html.TagCreator.ol; import static j2html.TagCreator.p; +import static j2html.TagCreator.pre; import static j2html.TagCreator.span; import static j2html.TagCreator.title; import static j2html.TagCreator.ul; @@ -357,6 +358,8 @@ private LiTag li_changedRequest(String name, ChangedMediaType request) { allChanges(li, schema); } } + MediaTypeExampleChanges.render(request) + .forEach((label, value) -> li.with(div(h3(label), pre(value)))); return li; } diff --git a/core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRender.java b/core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRender.java index 531fd380..3b7aad26 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRender.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRender.java @@ -283,6 +283,18 @@ protected String itemContent(int deepness, String mediaType, ChangedMediaType co if (content.getSchema() != null) { result += schema(deepness, content.getSchema()); } + String prefix = indent(deepness); + for (Map.Entry detail : MediaTypeExampleChanges.render(content).entrySet()) { + result += + format( + "%s%s:\n\n%s```json\n%s%s\n%s```\n\n", + prefix, + detail.getKey(), + prefix, + prefix, + detail.getValue().replace("\n", "\n" + prefix), + prefix); + } return result; } diff --git a/core/src/main/java/org/openapitools/openapidiff/core/output/MediaTypeExampleChanges.java b/core/src/main/java/org/openapitools/openapidiff/core/output/MediaTypeExampleChanges.java new file mode 100644 index 00000000..c0ef9ba5 --- /dev/null +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/MediaTypeExampleChanges.java @@ -0,0 +1,53 @@ +package org.openapitools.openapidiff.core.output; + +import static org.openapitools.openapidiff.core.model.Changed.result; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.swagger.v3.core.util.Json; +import java.util.LinkedHashMap; +import java.util.Map; +import org.openapitools.openapidiff.core.exception.RendererException; +import org.openapitools.openapidiff.core.model.ChangedExample; +import org.openapitools.openapidiff.core.model.ChangedExamples; +import org.openapitools.openapidiff.core.model.ChangedMediaType; + +/** Example details shared by the text and HTML reports; markup is handled by each renderer. */ +final class MediaTypeExampleChanges { + private MediaTypeExampleChanges() {} + + static Map render(ChangedMediaType mediaType) { + Map details = new LinkedHashMap<>(); + ChangedExample example = mediaType.getExample(); + if (result(example).isDifferent()) { + add(details, "example", example.getLeftExample(), example.getRightExample()); + } + ChangedExamples examples = mediaType.getExamples(); + if (result(examples).isDifferent()) { + add(details, "examples", examples.getLeftExamples(), examples.getRightExamples()); + } + return details; + } + + private static void add( + Map details, String name, Object oldValue, Object newValue) { + if (oldValue == null) { + details.put("Added " + name, json(newValue)); + } else if (newValue == null) { + details.put("Removed " + name, json(oldValue)); + } else { + details.put("Old " + name, json(oldValue)); + details.put("New " + name, json(newValue)); + } + } + + private static String json(Object value) { + try { + return Json.mapper() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(value) + .replace("\r\n", "\n"); + } catch (JsonProcessingException e) { + throw new RendererException("Could not serialize media type example", e); + } + } +} diff --git a/core/src/test/java/org/openapitools/openapidiff/core/output/MediaTypeExampleRenderTest.java b/core/src/test/java/org/openapitools/openapidiff/core/output/MediaTypeExampleRenderTest.java new file mode 100644 index 00000000..bb594120 --- /dev/null +++ b/core/src/test/java/org/openapitools/openapidiff/core/output/MediaTypeExampleRenderTest.java @@ -0,0 +1,188 @@ +package org.openapitools.openapidiff.core.output; + +import static io.swagger.v3.oas.models.PathItem.HttpMethod.POST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.openapitools.openapidiff.core.ChangesResolver.getChangedOperation; + +import io.swagger.parser.OpenAPIParser; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.examples.Example; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.parser.core.models.ParseOptions; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.openapitools.openapidiff.core.OpenApiCompare; +import org.openapitools.openapidiff.core.model.ChangedMediaType; +import org.openapitools.openapidiff.core.model.ChangedOpenApi; +import org.openapitools.openapidiff.core.model.ChangedOperation; +import org.openapitools.openapidiff.core.model.DiffResult; + +class MediaTypeExampleRenderTest { + private static final String FIXTURE = "issue-872-media-type-examples.yaml"; + + static Stream exampleChanges() { + return Stream.of("markdown", "console", "asciidoc", "html") + .flatMap( + renderer -> + Stream.of(false, true) + .flatMap( + response -> + Stream.of("example", "examples") + .flatMap( + kind -> + Stream.of("added", "removed", "updated") + .map( + change -> + Arguments.of( + renderer, response, kind, change))))); + } + + @ParameterizedTest(name = "{0}: response={1}, {2} {3}") + @MethodSource("exampleChanges") + void rendersMediaTypeExampleChangesWithoutSchemaChanges( + String renderer, boolean response, String kind, String change) { + OpenAPI oldSpec = readSpec(); + OpenAPI newSpec = readSpec(); + if (!"added".equals(change)) { + setExample(mediaType(oldSpec, response), kind, "old-message"); + } + if (!"removed".equals(change)) { + setExample(mediaType(newSpec, response), kind, "new-message"); + } + ChangedOpenApi diff = OpenApiCompare.fromSpecifications(oldSpec, newSpec); + assertThat(diff.isChanged()).isEqualTo(DiffResult.METADATA); + assertThat(changedMediaType(diff, response).getSchema()).isNull(); + + String output = renderer(renderer).render(diff); + if ("added".equals(change)) { + assertThat(output).contains("Added " + kind, "new-message").doesNotContain("old-message"); + } else if ("removed".equals(change)) { + assertThat(output).contains("Removed " + kind, "old-message").doesNotContain("new-message"); + } else { + assertThat(output).contains("Old " + kind, "New " + kind, "old-message", "new-message"); + } + if ("examples".equals(kind)) { + assertThat(output).contains("sample", "Sample payload"); + } + } + + @ParameterizedTest + @ValueSource(strings = {"markdown", "console", "asciidoc", "html"}) + void rendersExamplesAlongsideSchemaChanges(String renderer) { + OpenAPI oldSpec = readSpec(); + OpenAPI newSpec = readSpec(); + setExample(mediaType(oldSpec, false), "example", "old-message"); + setExample(mediaType(newSpec, false), "example", "new-message"); + mediaType(newSpec, false).getSchema().addProperty("extra", new StringSchema()); + ChangedOpenApi diff = OpenApiCompare.fromSpecifications(oldSpec, newSpec); + assertThat(changedMediaType(diff, false).getSchema()).isNotNull(); + assertThat(renderer(renderer).render(diff)) + .contains("Old example", "New example", "old-message", "new-message"); + } + + @ParameterizedTest + @ValueSource(strings = {"markdown", "console", "asciidoc", "html"}) + void doesNotRenderUnchangedExamplesWhenSchemaChanges(String renderer) { + OpenAPI oldSpec = readSpec(); + OpenAPI newSpec = readSpec(); + setExample(mediaType(oldSpec, false), "example", "unchanged-message"); + setExample(mediaType(newSpec, false), "example", "unchanged-message"); + mediaType(newSpec, false).getSchema().addProperty("extra", new StringSchema()); + ChangedOpenApi diff = OpenApiCompare.fromSpecifications(oldSpec, newSpec); + assertThat(changedMediaType(diff, false).getSchema()).isNotNull(); + assertThat(renderer(renderer).render(diff)) + .doesNotContain("Added example", "Removed example", "Old example", "unchanged-message"); + } + + @Test + void htmlEscapesExampleValuesAndNames() { + OpenAPI oldSpec = readSpec(); + OpenAPI newSpec = readSpec(); + mediaType(newSpec, false) + .addExamples( + "&"))); + String output = new HtmlRender().render(OpenApiCompare.fromSpecifications(oldSpec, newSpec)); + assertThat(output).contains("<script>", "&").doesNotContain("