diff --git a/README.md b/README.md index 60d722e7f..6340438d3 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Available on [Docker Hub](https://hub.docker.com/r/openapitools/openapi-diff/) a ```bash # docker run openapitools/openapi-diff:latest usage: openapi-diff + --asciidoc export diff as asciidoc in given file --debug Print debugging information --error Print error information --fail-on-changed Fail if API changed but is backward @@ -101,6 +102,7 @@ openapi-diff can read OpenAPI specs from JSON files or HTTP URLs. ```bash $ openapi-diff --help usage: openapi-diff + --asciidoc export diff as asciidoc in given file --debug Print debugging information --error Print error information -h,--help print this message @@ -151,6 +153,8 @@ Add openapi-diff to your POM to show diffs when you test your Maven project. You ${project.basedir}/../maven/target/diff.json ${project.basedir}/../maven/target/diff.md + + ${project.basedir}/../maven/target/diff.adoc diff --git a/cli/src/main/java/org/openapitools/openapidiff/cli/Main.java b/cli/src/main/java/org/openapitools/openapidiff/cli/Main.java index f19826a93..567221267 100644 --- a/cli/src/main/java/org/openapitools/openapidiff/cli/Main.java +++ b/cli/src/main/java/org/openapitools/openapidiff/cli/Main.java @@ -17,10 +17,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.openapitools.openapidiff.core.OpenApiCompare; import org.openapitools.openapidiff.core.model.ChangedOpenApi; -import org.openapitools.openapidiff.core.output.ConsoleRender; -import org.openapitools.openapidiff.core.output.HtmlRender; -import org.openapitools.openapidiff.core.output.JsonRender; -import org.openapitools.openapidiff.core.output.MarkdownRender; +import org.openapitools.openapidiff.core.output.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -109,6 +106,13 @@ public static void main(String... args) { .argName("file") .desc("export diff as json in given file") .build()); + options.addOption( + Option.builder() + .longOpt("asciidoc") + .hasArg() + .argName("file") + .desc("export diff as asciidoc in given file") + .build()); // create the parser CommandLineParser parser = new DefaultParser(); @@ -191,6 +195,12 @@ public static void main(String... args) { OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); mdRender.render(result, outputStreamWriter); } + if (line.hasOption("asciidoc")) { + AsciidocRender asciidocRender = new AsciidocRender(); + FileOutputStream outputStream = new FileOutputStream(line.getOptionValue("asciidoc")); + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); + asciidocRender.render(result, outputStreamWriter); + } if (line.hasOption("text")) { FileOutputStream outputStream = new FileOutputStream(line.getOptionValue("text")); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); 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 new file mode 100644 index 000000000..9b3b51edc --- /dev/null +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/AsciidocRender.java @@ -0,0 +1,62 @@ +package org.openapitools.openapidiff.core.output; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AsciidocRender extends MarkdownRenderBase { + private static final Logger LOGGER = LoggerFactory.getLogger(AsciidocRender.class); + private static final String H4 = "==== "; + private static final String H5 = "===== "; + private static final String H6 = "====== "; + private static final String CODE = "`"; + private static final String PRE_LI = "*"; + private static final String LI = "* "; + private static final String HR = "\n'''\n"; + + @Override + public String getH4() { + return H4; + } + + @Override + public String getH5() { + return H5; + } + + @Override + public String getH6() { + return H6; + } + + @Override + public String getBlockQuote() { + throw new UnsupportedOperationException( + "Asciidoc does not use blockquote marker;" + + "instead it uses blocks like this: \n\n----\nQUOTED TEXT\n----\n"); + } + + @Override + public String getCode() { + return CODE; + } + + @Override + public String getPreListItem() { + return PRE_LI; + } + + @Override + public String getListItem() { + return LI; + } + + @Override + public String getHorizontalRule() { + return HR; + } + + @Override + protected String blockquote(String beginning, String text) { + return beginning + "\n----\n" + text.trim() + "\n----\n"; + } +} 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 5e0d0dd76..489ec6012 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 @@ -1,575 +1,56 @@ package org.openapitools.openapidiff.core.output; -import static java.lang.String.format; -import static org.openapitools.openapidiff.core.model.Changed.result; -import static org.openapitools.openapidiff.core.utils.ChangedUtils.isUnchanged; - -import io.swagger.v3.oas.models.headers.Header; -import io.swagger.v3.oas.models.media.ArraySchema; -import io.swagger.v3.oas.models.media.ComposedSchema; -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 java.io.IOException; -import java.io.OutputStreamWriter; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.apache.commons.lang3.StringUtils; -import org.openapitools.openapidiff.core.exception.RendererException; -import org.openapitools.openapidiff.core.model.*; -import org.openapitools.openapidiff.core.utils.RefPointer; -import org.openapitools.openapidiff.core.utils.RefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class MarkdownRender implements Render { +public class MarkdownRender extends MarkdownRenderBase { private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownRender.class); - private static final String H3 = "### "; private static final String H4 = "#### "; private static final String H5 = "##### "; private static final String H6 = "###### "; private static final String BLOCKQUOTE = "> "; private static final String CODE = "`"; - private static final String PRE_CODE = " "; private static final String PRE_LI = " "; private static final String LI = "* "; private static final String HR = "---\n"; - protected RefPointer> refPointer = new RefPointer<>(RefType.SCHEMAS); - protected ChangedOpenApi diff; - protected Set> handledSchemas = new HashSet<>(); - /** - * A parameter which indicates whether or not metadata (summary and metadata) changes should be - * logged in the changelog file. - */ - protected boolean showChangedMetadata; - - public void render(ChangedOpenApi diff, OutputStreamWriter outputStreamWriter) { - this.diff = diff; - this.handledSchemas.clear(); - listEndpoints("What's New", diff.getNewEndpoints(), outputStreamWriter); - listEndpoints("What's Deleted", diff.getMissingEndpoints(), outputStreamWriter); - listEndpoints("What's Deprecated", diff.getDeprecatedEndpoints(), outputStreamWriter); - listEndpoints(diff.getChangedOperations(), outputStreamWriter); - try { - outputStreamWriter.close(); - } catch (IOException e) { - throw new RendererException(e); - } - } - - protected String sectionTitle(String title) { - return H4 + title + '\n' + HR + '\n'; - } - - protected void listEndpoints( - String title, List endpoints, OutputStreamWriter outputStreamWriter) { - if (null == endpoints || endpoints.isEmpty()) { - return; - } - safelyAppend(outputStreamWriter, sectionTitle(title)); - endpoints.stream() - .map(e -> itemEndpoint(e.getMethod().toString(), e.getPathUrl(), e.getSummary())) - .forEach(csq -> safelyAppend(outputStreamWriter, csq)); - } - - protected String itemEndpoint(String method, String path, String summary) { - return H5 + CODE + method + CODE + " " + path + "\n\n" + metadata(summary) + "\n"; - } - - protected String itemEndpoint(String method, String path, ChangedMetadata summary) { - return H5 + CODE + method + CODE + " " + path + "\n\n" + metadata("summary", summary) + "\n"; - } - - protected String titleH5(String title) { - return H6 + title + '\n'; - } - - protected void listEndpoints( - List changedOperations, OutputStreamWriter outputStreamWriter) { - if (null == changedOperations || changedOperations.isEmpty()) { - return; - } - safelyAppend(outputStreamWriter, sectionTitle("What's Changed")); - changedOperations.forEach( - operation -> { - safelyAppend( - outputStreamWriter, - itemEndpoint( - operation.getHttpMethod().toString(), - operation.getPathUrl(), - operation.getSummary())); - if (result(operation.getParameters()).isDifferent()) { - safelyAppend(outputStreamWriter, titleH5("Parameters:")); - safelyAppend(outputStreamWriter, parameters(operation.getParameters())); - } - if (operation.resultRequestBody().isDifferent()) { - safelyAppend(outputStreamWriter, titleH5("Request:")); - safelyAppend( - outputStreamWriter, - metadata("Description", operation.getRequestBody().getDescription())); - safelyAppend(outputStreamWriter, bodyContent(operation.getRequestBody().getContent())); - } - if (operation.resultApiResponses().isDifferent()) { - safelyAppend(outputStreamWriter, titleH5("Return Type:")); - safelyAppend(outputStreamWriter, responses(operation.getApiResponses())); - } - }); - } - - protected String responses(ChangedApiResponse changedApiResponse) { - StringBuilder sb = new StringBuilder("\n"); - sb.append(listResponse("New response", changedApiResponse.getIncreased())); - sb.append(listResponse("Deleted response", changedApiResponse.getMissing())); - changedApiResponse.getChanged().entrySet().stream() - .map(e -> this.itemResponse(e.getKey(), e.getValue())) - .forEach(sb::append); - return sb.toString(); - } - - protected String listResponse(String title, Map responses) { - StringBuilder sb = new StringBuilder(); - responses.entrySet().stream() - .map(e -> this.itemResponse(title, e.getKey(), e.getValue())) - .forEach(sb::append); - return sb.toString(); - } - - protected String itemResponse(String title, String code, ApiResponse response) { - return this.itemResponse(title, code, response.getDescription()); - } - - protected String itemResponse(String code, ChangedResponse response) { - StringBuilder sb = new StringBuilder(); - sb.append( - this.itemResponse( - "Changed response", - code, - null == response.getNewApiResponse() - ? "" - : response.getNewApiResponse().getDescription())); - sb.append(headers(response.getHeaders())); - if (response.getContent() != null) { - sb.append(this.bodyContent(LI, response.getContent())); - } - return sb.toString(); - } - - protected String itemResponse(String title, String code, String description) { - StringBuilder sb = new StringBuilder(); - String status = ""; - if (!code.equals("default") && !code.matches("[1-5]XX")) { - status = HttpStatus.getReasonPhrase(Integer.parseInt(code)); - } - sb.append(format("%s : **%s %s**\n", title, code, status)); - sb.append(metadata(description)); - return sb.toString(); - } - - protected String headers(ChangedHeaders headers) { - StringBuilder sb = new StringBuilder(); - if (headers != null) { - sb.append(listHeader("New header", headers.getIncreased())) - .append(listHeader("Deleted header", headers.getMissing())); - headers.getChanged().entrySet().stream() - .map(e -> this.itemHeader(e.getKey(), e.getValue())) - .forEach(sb::append); - } - return sb.toString(); - } - - protected String listHeader(String title, Map headers) { - StringBuilder sb = new StringBuilder(); - headers.entrySet().stream() - .map(e -> this.itemHeader(title, e.getKey(), e.getValue())) - .forEach(sb::append); - return sb.toString(); - } - - protected String itemHeader(String title, String name, Header header) { - return this.itemHeader(title, name, header.getDescription()); - } - - protected String itemHeader(String code, ChangedHeader header) { - return this.itemHeader( - "Changed header", - code, - null == header.getNewHeader() ? "" : header.getNewHeader().getDescription()); - } - - protected String itemHeader(String title, String mediaType, String description) { - return format("%s : `%s`\n\n", title, mediaType) + metadata(description) + '\n'; - } - - protected String bodyContent(String prefix, ChangedContent changedContent) { - if (changedContent == null) { - return ""; - } - StringBuilder sb = new StringBuilder("\n"); - sb.append(listContent(prefix, "New content type", changedContent.getIncreased())); - sb.append(listContent(prefix, "Deleted content type", changedContent.getMissing())); - final int deepness; - if (StringUtils.isNotBlank(prefix)) { - deepness = 1; - } else { - deepness = 0; - } - changedContent.getChanged().entrySet().stream() - .map(e -> this.itemContent(deepness, e.getKey(), e.getValue())) - .forEach(e -> sb.append(prefix).append(e)); - return sb.toString(); - } - - protected String bodyContent(ChangedContent changedContent) { - return bodyContent("", changedContent); - } - - protected String listContent(String prefix, String title, Map mediaTypes) { - StringBuilder sb = new StringBuilder(); - mediaTypes.entrySet().stream() - .map(e -> this.itemContent(title, e.getKey(), e.getValue())) - .forEach(e -> sb.append(prefix).append(e)); - return sb.toString(); - } - - protected String itemContent(String title, String mediaType) { - return format("%s : `%s`\n\n", title, mediaType); - } - - protected String itemContent(String title, String mediaType, MediaType content) { - return itemContent(title, mediaType); - } - - protected String itemContent(int deepness, String mediaType, ChangedMediaType content) { - return itemContent("Changed content type", mediaType) + schema(deepness, content.getSchema()); - } - - protected String schema(ChangedSchema schema) { - return schema(1, schema); - } - - protected String oneOfSchema(int deepness, ChangedOneOfSchema schema, String discriminator) { - StringBuilder sb = new StringBuilder(); - schema - .getMissing() - .keySet() - .forEach( - key -> sb.append(format("%sDeleted '%s' %s\n", indent(deepness), key, discriminator))); - schema - .getIncreased() - .forEach( - (key, sub) -> - sb.append(format("%sAdded '%s' %s:\n", indent(deepness), key, discriminator)) - .append(schema(deepness, sub, schema.getContext()))); - schema - .getChanged() - .forEach( - (key, sub) -> - sb.append(format("%sUpdated `%s` %s:\n", indent(deepness), key, discriminator)) - .append(schema(deepness, sub))); - return sb.toString(); - } - - protected String required(int deepness, String title, List required) { - StringBuilder sb = new StringBuilder(); - if (!required.isEmpty()) { - sb.append(format("%s%s:\n", indent(deepness), title)); - required.forEach(s -> sb.append(format("%s- `%s`\n", indent(deepness), s))); - sb.append("\n"); - } - return sb.toString(); - } - - protected String schema(int deepness, ChangedSchema schema) { - StringBuilder sb = new StringBuilder(); - if (schema.isDiscriminatorPropertyChanged()) { - LOGGER.debug("Discriminator property changed"); - } - if (schema.getOneOfSchema() != null) { - String discriminator = - schema.getNewSchema().getDiscriminator() != null - ? schema.getNewSchema().getDiscriminator().getPropertyName() - : ""; - sb.append(oneOfSchema(deepness, schema.getOneOfSchema(), discriminator)); - } - if (schema.getRequired() != null) { - sb.append(required(deepness, "New required properties", schema.getRequired().getIncreased())); - sb.append(required(deepness, "New optional properties", schema.getRequired().getMissing())); - } - if (schema.getItems() != null) { - sb.append(items(deepness, schema.getItems())); - } - sb.append(listDiff(deepness, "enum", schema.getEnumeration())); - sb.append( - properties( - deepness, - "Added property", - schema.getIncreasedProperties(), - true, - schema.getContext())); - sb.append( - properties( - deepness, - "Deleted property", - schema.getMissingProperties(), - false, - schema.getContext())); - schema - .getChangedProperties() - .forEach((name, property) -> sb.append(property(deepness, name, property))); - return sb.toString(); - } - - protected String schema(int deepness, ComposedSchema schema, DiffContext context) { - StringBuilder sb = new StringBuilder(); - if (schema.getAllOf() != null) { - LOGGER.debug("All of schema"); - schema.getAllOf().stream() - .map(this::resolve) - .forEach(composedChild -> sb.append(schema(deepness, composedChild, context))); - } - if (schema.getOneOf() != null) { - LOGGER.debug("One of schema"); - sb.append(format("%sOne of:\n\n", indent(deepness))); - schema.getOneOf().stream() - .map(this::resolve) - .forEach(composedChild -> sb.append(schema(deepness + 1, composedChild, context))); - } - return sb.toString(); - } - - protected String schema(int deepness, Schema schema, DiffContext context) { - if (handledSchemas.contains(schema)) return ""; - handledSchemas.add(schema); - StringBuilder sb = new StringBuilder(); - sb.append(listItem(deepness, "Enum", schema.getEnum())); - sb.append(properties(deepness, "Property", schema.getProperties(), true, context)); - if (schema instanceof ComposedSchema) { - sb.append(schema(deepness, (ComposedSchema) schema, context)); - } else if (schema instanceof ArraySchema) { - sb.append(items(deepness, resolve(((ArraySchema) schema).getItems()), context)); - } - return sb.toString(); - } - - protected String items(int deepness, ChangedSchema schema) { - StringBuilder sb = new StringBuilder(); - String type = type(schema.getNewSchema()); - if (schema.isChangedType()) { - type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); - } - sb.append(items(deepness, "Changed items", type, schema.getNewSchema().getDescription())); - sb.append(schema(deepness, schema)); - return sb.toString(); - } - - protected String items(int deepness, Schema schema, DiffContext context) { - return items(deepness, "Items", type(schema), schema.getDescription()) - + schema(deepness, schema, context); - } - - protected String items(int deepness, String title, String type, String description) { - return format( - "%s%s (%s):" + "\n%s\n", - indent(deepness), title, type, metadata(indent(deepness + 1), description)); - } - - protected String properties( - final int deepness, - String title, - Map> properties, - boolean showContent, - DiffContext context) { - StringBuilder sb = new StringBuilder(); - if (properties != null) { - properties.forEach( - (key, value) -> { - sb.append(resolveProperty(deepness, value, key, title)); - if (showContent) { - sb.append(schema(deepness + 1, resolve(value), context)); - } - }); - } - return sb.toString(); - } - - private String resolveProperty(int deepness, Schema value, String key, String title) { - try { - return property(deepness, title, key, resolve(value)); - } catch (Exception e) { - return property(deepness, title, key, type(value), ""); - } - } - - protected String property(int deepness, String name, ChangedSchema schema) { - StringBuilder sb = new StringBuilder(); - String type = type(schema.getNewSchema()); - if (schema.isChangedType()) { - type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); - } - sb.append( - property(deepness, "Changed property", name, type, schema.getNewSchema().getDescription())); - sb.append(schema(++deepness, schema)); - return sb.toString(); - } - - protected String property(int deepness, String title, String name, Schema schema) { - return property(deepness, title, name, type(schema), schema.getDescription()); - } - - protected String property( - int deepness, String title, String name, String type, String description) { - return format( - "%s* %s `%s` (%s)\n%s\n", - indent(deepness), title, name, type, metadata(indent(deepness + 1), description)); - } - - protected String listDiff(int deepness, String name, ChangedList listDiff) { - if (listDiff == null) { - return ""; - } - return listItem(deepness, "Added " + name, listDiff.getIncreased()) - + listItem(deepness, "Removed " + name, listDiff.getMissing()); - } - - protected String listItem(int deepness, String name, List list) { - StringBuilder sb = new StringBuilder(); - if (list != null && !list.isEmpty()) { - sb.append(format("%s%s value%s:\n\n", indent(deepness), name, list.size() > 1 ? "s" : "")); - list.forEach(p -> sb.append(format("%s* `%s`\n", indent(deepness), p))); - } - return sb.toString(); - } - - protected String parameters(ChangedParameters changedParameters) { - List changed = changedParameters.getChanged(); - StringBuilder sb = new StringBuilder("\n"); - sb.append(listParameter("Added", changedParameters.getIncreased())) - .append(listParameter("Deleted", changedParameters.getMissing())); - changed.stream().map(this::itemParameter).forEach(sb::append); - return sb.toString(); - } - - protected String listParameter(String title, List parameters) { - StringBuilder sb = new StringBuilder(); - parameters.stream().map(p -> itemParameter(title, p)).forEach(sb::append); - return sb.toString(); - } - - protected String itemParameter(String title, Parameter parameter) { - return this.itemParameter( - title, parameter.getName(), parameter.getIn(), parameter.getDescription()); - } - - protected String itemParameter(String title, String name, String in, String description) { - return format("%s: ", title) - + code(name) - + " in " - + code(in) - + '\n' - + metadata(description) - + '\n'; - } - - protected String itemParameter(ChangedParameter param) { - Parameter rightParam = param.getNewParameter(); - if (param.isDeprecated()) { - return itemParameter( - "Deprecated", rightParam.getName(), rightParam.getIn(), rightParam.getDescription()); - } - return itemParameter( - "Changed", rightParam.getName(), rightParam.getIn(), rightParam.getDescription()); - } - - protected String code(String string) { - return CODE + string + CODE; - } - - protected String metadata(String name, ChangedMetadata changedMetadata) { - return metadata("", name, changedMetadata); - } - - protected String metadata(String beginning, String name, ChangedMetadata changedMetadata) { - if (changedMetadata == null) { - return ""; - } - if (!isUnchanged(changedMetadata) && showChangedMetadata) { - return format( - "Changed %s:\n%s\nto:\n%s\n\n", - name, - metadata(beginning, changedMetadata.getLeft()), - metadata(beginning, changedMetadata.getRight())); - } else { - return metadata(beginning, name, changedMetadata.getRight()); - } - } - - protected String metadata(String metadata) { - return metadata("", metadata); - } - - protected String metadata(String beginning, String name, String metadata) { - if (StringUtils.isBlank(metadata)) { - return ""; - } - return blockquote(beginning, metadata); - } - - protected String metadata(String beginning, String metadata) { - if (StringUtils.isBlank(metadata)) { - return ""; - } - return blockquote(beginning, metadata); + @Override + public String getH4() { + return H4; } - protected String blockquote(String beginning) { - return beginning + BLOCKQUOTE; + @Override + public String getH5() { + return H5; } - protected String blockquote(String beginning, String text) { - String blockquote = blockquote(beginning); - return blockquote + text.trim().replace("\n", "\n" + blockquote) + "\n\n"; + @Override + public String getH6() { + return H6; } - protected String type(Schema schema) { - String result = "object"; - if (schema instanceof ArraySchema) { - result = "array"; - } else if (schema.getType() != null) { - result = schema.getType(); - } - return result; + @Override + public String getBlockQuote() { + return BLOCKQUOTE; } - protected String indent(int deepness) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < deepness; i++) { - sb.append(PRE_LI); - } - return sb.toString(); + @Override + public String getCode() { + return CODE; } - protected Schema resolve(Schema schema) { - return refPointer.resolveRef( - diff.getNewSpecOpenApi().getComponents(), schema, schema.get$ref()); + @Override + public String getPreListItem() { + return PRE_LI; } - /** - * A parameter which indicates whether or not metadata (summary and metadata) changes should be - * logged in the changelog file. - */ - public boolean isShowChangedMetadata() { - return this.showChangedMetadata; + @Override + public String getListItem() { + return LI; } - /** - * A parameter which indicates whether or not metadata (summary and metadata) changes should be - * logged in the changelog file. - */ - public void setShowChangedMetadata(final boolean showChangedMetadata) { - this.showChangedMetadata = showChangedMetadata; + @Override + public String getHorizontalRule() { + return HR; } } diff --git a/core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRenderBase.java b/core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRenderBase.java new file mode 100644 index 000000000..237f5ba10 --- /dev/null +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRenderBase.java @@ -0,0 +1,597 @@ +package org.openapitools.openapidiff.core.output; + +import static java.lang.String.format; +import static org.openapitools.openapidiff.core.model.Changed.result; +import static org.openapitools.openapidiff.core.utils.ChangedUtils.isUnchanged; + +import io.swagger.v3.oas.models.headers.Header; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.ComposedSchema; +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 java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.openapitools.openapidiff.core.exception.RendererException; +import org.openapitools.openapidiff.core.model.*; +import org.openapitools.openapidiff.core.utils.RefPointer; +import org.openapitools.openapidiff.core.utils.RefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +abstract class MarkdownRenderBase implements Render { + private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownRenderBase.class); + + protected RefPointer> refPointer = new RefPointer<>(RefType.SCHEMAS); + protected ChangedOpenApi diff; + protected Set> handledSchemas = new HashSet<>(); + /** + * A parameter which indicates whether or not metadata (summary and metadata) changes should be + * logged in the changelog file. + */ + protected boolean showChangedMetadata; + + public abstract String getH4(); + + public abstract String getH5(); + + public abstract String getH6(); + + public abstract String getBlockQuote(); + + public abstract String getCode(); + + public abstract String getPreListItem(); + + public abstract String getListItem(); + + public abstract String getHorizontalRule(); + + public void render(ChangedOpenApi diff, OutputStreamWriter outputStreamWriter) { + this.diff = diff; + this.handledSchemas.clear(); + listEndpoints("What's New", diff.getNewEndpoints(), outputStreamWriter); + listEndpoints("What's Deleted", diff.getMissingEndpoints(), outputStreamWriter); + listEndpoints("What's Deprecated", diff.getDeprecatedEndpoints(), outputStreamWriter); + listEndpoints(diff.getChangedOperations(), outputStreamWriter); + try { + outputStreamWriter.close(); + } catch (IOException e) { + throw new RendererException(e); + } + } + + protected String sectionTitle(String title) { + return getH4() + title + '\n' + getHorizontalRule() + '\n'; + } + + protected void listEndpoints( + String title, List endpoints, OutputStreamWriter outputStreamWriter) { + if (null == endpoints || endpoints.isEmpty()) { + return; + } + safelyAppend(outputStreamWriter, sectionTitle(title)); + endpoints.stream() + .map(e -> itemEndpoint(e.getMethod().toString(), e.getPathUrl(), e.getSummary())) + .forEach(csq -> safelyAppend(outputStreamWriter, csq)); + } + + protected String itemEndpoint(String method, String path, String summary) { + return getH5() + + getCode() + + method + + getCode() + + " " + + path + + "\n\n" + + metadata(summary) + + "\n"; + } + + protected String itemEndpoint(String method, String path, ChangedMetadata summary) { + return getH5() + + getCode() + + method + + getCode() + + " " + + path + + "\n\n" + + metadata("summary", summary) + + "\n"; + } + + protected String titleH5(String title) { + return getH6() + title + '\n'; + } + + protected void listEndpoints( + List changedOperations, OutputStreamWriter outputStreamWriter) { + if (null == changedOperations || changedOperations.isEmpty()) { + return; + } + safelyAppend(outputStreamWriter, sectionTitle("What's Changed")); + changedOperations.forEach( + operation -> { + safelyAppend( + outputStreamWriter, + itemEndpoint( + operation.getHttpMethod().toString(), + operation.getPathUrl(), + operation.getSummary())); + if (result(operation.getParameters()).isDifferent()) { + safelyAppend(outputStreamWriter, titleH5("Parameters:")); + safelyAppend(outputStreamWriter, parameters(operation.getParameters())); + } + if (operation.resultRequestBody().isDifferent()) { + safelyAppend(outputStreamWriter, titleH5("Request:")); + safelyAppend( + outputStreamWriter, + metadata("Description", operation.getRequestBody().getDescription())); + safelyAppend(outputStreamWriter, bodyContent(operation.getRequestBody().getContent())); + } + if (operation.resultApiResponses().isDifferent()) { + safelyAppend(outputStreamWriter, titleH5("Return Type:")); + safelyAppend(outputStreamWriter, responses(operation.getApiResponses())); + } + }); + } + + protected String responses(ChangedApiResponse changedApiResponse) { + StringBuilder sb = new StringBuilder("\n"); + sb.append(listResponse("New response", changedApiResponse.getIncreased())); + sb.append(listResponse("Deleted response", changedApiResponse.getMissing())); + changedApiResponse.getChanged().entrySet().stream() + .map(e -> this.itemResponse(e.getKey(), e.getValue())) + .forEach(sb::append); + return sb.toString(); + } + + protected String listResponse(String title, Map responses) { + StringBuilder sb = new StringBuilder(); + responses.entrySet().stream() + .map(e -> this.itemResponse(title, e.getKey(), e.getValue())) + .forEach(sb::append); + return sb.toString(); + } + + protected String itemResponse(String title, String code, ApiResponse response) { + return this.itemResponse(title, code, response.getDescription()); + } + + protected String itemResponse(String code, ChangedResponse response) { + StringBuilder sb = new StringBuilder(); + sb.append( + this.itemResponse( + "Changed response", + code, + null == response.getNewApiResponse() + ? "" + : response.getNewApiResponse().getDescription())); + sb.append(headers(response.getHeaders())); + if (response.getContent() != null) { + sb.append(this.bodyContent(getListItem(), response.getContent())); + } + return sb.toString(); + } + + protected String itemResponse(String title, String code, String description) { + StringBuilder sb = new StringBuilder(); + String status = ""; + if (!code.equals("default") && !code.matches("[1-5]XX")) { + status = HttpStatus.getReasonPhrase(Integer.parseInt(code)); + } + sb.append(format("%s : **%s %s**\n", title, code, status)); + sb.append(metadata(description)); + return sb.toString(); + } + + protected String headers(ChangedHeaders headers) { + StringBuilder sb = new StringBuilder(); + if (headers != null) { + sb.append(listHeader("New header", headers.getIncreased())) + .append(listHeader("Deleted header", headers.getMissing())); + headers.getChanged().entrySet().stream() + .map(e -> this.itemHeader(e.getKey(), e.getValue())) + .forEach(sb::append); + } + return sb.toString(); + } + + protected String listHeader(String title, Map headers) { + StringBuilder sb = new StringBuilder(); + headers.entrySet().stream() + .map(e -> this.itemHeader(title, e.getKey(), e.getValue())) + .forEach(sb::append); + return sb.toString(); + } + + protected String itemHeader(String title, String name, Header header) { + return this.itemHeader(title, name, header.getDescription()); + } + + protected String itemHeader(String code, ChangedHeader header) { + return this.itemHeader( + "Changed header", + code, + null == header.getNewHeader() ? "" : header.getNewHeader().getDescription()); + } + + protected String itemHeader(String title, String mediaType, String description) { + return format("%s : `%s`\n\n", title, mediaType) + metadata(description) + '\n'; + } + + protected String bodyContent(String prefix, ChangedContent changedContent) { + if (changedContent == null) { + return ""; + } + StringBuilder sb = new StringBuilder("\n"); + sb.append(listContent(prefix, "New content type", changedContent.getIncreased())); + sb.append(listContent(prefix, "Deleted content type", changedContent.getMissing())); + final int deepness; + if (StringUtils.isNotBlank(prefix)) { + deepness = 1; + } else { + deepness = 0; + } + changedContent.getChanged().entrySet().stream() + .map(e -> this.itemContent(deepness, e.getKey(), e.getValue())) + .forEach(e -> sb.append(prefix).append(e)); + return sb.toString(); + } + + protected String bodyContent(ChangedContent changedContent) { + return bodyContent("", changedContent); + } + + protected String listContent(String prefix, String title, Map mediaTypes) { + StringBuilder sb = new StringBuilder(); + mediaTypes.entrySet().stream() + .map(e -> this.itemContent(title, e.getKey(), e.getValue())) + .forEach(e -> sb.append(prefix).append(e)); + return sb.toString(); + } + + protected String itemContent(String title, String mediaType) { + return format("%s : `%s`\n\n", title, mediaType); + } + + protected String itemContent(String title, String mediaType, MediaType content) { + return itemContent(title, mediaType); + } + + protected String itemContent(int deepness, String mediaType, ChangedMediaType content) { + return itemContent("Changed content type", mediaType) + schema(deepness, content.getSchema()); + } + + protected String schema(ChangedSchema schema) { + return schema(1, schema); + } + + protected String oneOfSchema(int deepness, ChangedOneOfSchema schema, String discriminator) { + StringBuilder sb = new StringBuilder(); + schema + .getMissing() + .keySet() + .forEach( + key -> sb.append(format("%sDeleted '%s' %s\n", indent(deepness), key, discriminator))); + schema + .getIncreased() + .forEach( + (key, sub) -> + sb.append(format("%sAdded '%s' %s:\n", indent(deepness), key, discriminator)) + .append(schema(deepness, sub, schema.getContext()))); + schema + .getChanged() + .forEach( + (key, sub) -> + sb.append(format("%sUpdated `%s` %s:\n", indent(deepness), key, discriminator)) + .append(schema(deepness, sub))); + return sb.toString(); + } + + protected String required(int deepness, String title, List required) { + StringBuilder sb = new StringBuilder(); + if (!required.isEmpty()) { + sb.append(format("%s%s:\n", indent(deepness), title)); + required.forEach(s -> sb.append(format("%s- `%s`\n", indent(deepness), s))); + sb.append("\n"); + } + return sb.toString(); + } + + protected String schema(int deepness, ChangedSchema schema) { + StringBuilder sb = new StringBuilder(); + if (schema.isDiscriminatorPropertyChanged()) { + LOGGER.debug("Discriminator property changed"); + } + if (schema.getOneOfSchema() != null) { + String discriminator = + schema.getNewSchema().getDiscriminator() != null + ? schema.getNewSchema().getDiscriminator().getPropertyName() + : ""; + sb.append(oneOfSchema(deepness, schema.getOneOfSchema(), discriminator)); + } + if (schema.getRequired() != null) { + sb.append(required(deepness, "New required properties", schema.getRequired().getIncreased())); + sb.append(required(deepness, "New optional properties", schema.getRequired().getMissing())); + } + if (schema.getItems() != null) { + sb.append(items(deepness, schema.getItems())); + } + sb.append(listDiff(deepness, "enum", schema.getEnumeration())); + sb.append( + properties( + deepness, + "Added property", + schema.getIncreasedProperties(), + true, + schema.getContext())); + sb.append( + properties( + deepness, + "Deleted property", + schema.getMissingProperties(), + false, + schema.getContext())); + schema + .getChangedProperties() + .forEach((name, property) -> sb.append(property(deepness, name, property))); + return sb.toString(); + } + + protected String schema(int deepness, ComposedSchema schema, DiffContext context) { + StringBuilder sb = new StringBuilder(); + if (schema.getAllOf() != null) { + LOGGER.debug("All of schema"); + schema.getAllOf().stream() + .map(this::resolve) + .forEach(composedChild -> sb.append(schema(deepness, composedChild, context))); + } + if (schema.getOneOf() != null) { + LOGGER.debug("One of schema"); + sb.append(format("%sOne of:\n\n", indent(deepness))); + schema.getOneOf().stream() + .map(this::resolve) + .forEach(composedChild -> sb.append(schema(deepness + 1, composedChild, context))); + } + return sb.toString(); + } + + protected String schema(int deepness, Schema schema, DiffContext context) { + if (handledSchemas.contains(schema)) return ""; + handledSchemas.add(schema); + StringBuilder sb = new StringBuilder(); + sb.append(listItem(deepness, "Enum", schema.getEnum())); + sb.append(properties(deepness, "Property", schema.getProperties(), true, context)); + if (schema instanceof ComposedSchema) { + sb.append(schema(deepness, (ComposedSchema) schema, context)); + } else if (schema instanceof ArraySchema) { + sb.append(items(deepness, resolve(((ArraySchema) schema).getItems()), context)); + } + return sb.toString(); + } + + protected String items(int deepness, ChangedSchema schema) { + StringBuilder sb = new StringBuilder(); + String type = type(schema.getNewSchema()); + if (schema.isChangedType()) { + type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); + } + sb.append(items(deepness, "Changed items", type, schema.getNewSchema().getDescription())); + sb.append(schema(deepness, schema)); + return sb.toString(); + } + + protected String items(int deepness, Schema schema, DiffContext context) { + return items(deepness, "Items", type(schema), schema.getDescription()) + + schema(deepness, schema, context); + } + + protected String items(int deepness, String title, String type, String description) { + return format( + "%s%s (%s):" + "\n%s\n", + indent(deepness), title, type, metadata(indent(deepness + 1), description)); + } + + protected String properties( + final int deepness, + String title, + Map> properties, + boolean showContent, + DiffContext context) { + StringBuilder sb = new StringBuilder(); + if (properties != null) { + properties.forEach( + (key, value) -> { + sb.append(resolveProperty(deepness, value, key, title)); + if (showContent) { + sb.append(schema(deepness + 1, resolve(value), context)); + } + }); + } + return sb.toString(); + } + + private String resolveProperty(int deepness, Schema value, String key, String title) { + try { + return property(deepness, title, key, resolve(value)); + } catch (Exception e) { + return property(deepness, title, key, type(value), ""); + } + } + + protected String property(int deepness, String name, ChangedSchema schema) { + StringBuilder sb = new StringBuilder(); + String type = type(schema.getNewSchema()); + if (schema.isChangedType()) { + type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); + } + sb.append( + property(deepness, "Changed property", name, type, schema.getNewSchema().getDescription())); + sb.append(schema(++deepness, schema)); + return sb.toString(); + } + + protected String property(int deepness, String title, String name, Schema schema) { + return property(deepness, title, name, type(schema), schema.getDescription()); + } + + protected String property( + int deepness, String title, String name, String type, String description) { + return format( + "%s* %s `%s` (%s)\n%s\n", + indent(deepness), title, name, type, metadata(indent(deepness + 1), description)); + } + + protected String listDiff(int deepness, String name, ChangedList listDiff) { + if (listDiff == null) { + return ""; + } + return listItem(deepness, "Added " + name, listDiff.getIncreased()) + + listItem(deepness, "Removed " + name, listDiff.getMissing()); + } + + protected String listItem(int deepness, String name, List list) { + StringBuilder sb = new StringBuilder(); + if (list != null && !list.isEmpty()) { + sb.append(format("%s%s value%s:\n\n", indent(deepness), name, list.size() > 1 ? "s" : "")); + list.forEach(p -> sb.append(format("%s* `%s`\n", indent(deepness), p))); + } + return sb.toString(); + } + + protected String parameters(ChangedParameters changedParameters) { + List changed = changedParameters.getChanged(); + StringBuilder sb = new StringBuilder("\n"); + sb.append(listParameter("Added", changedParameters.getIncreased())) + .append(listParameter("Deleted", changedParameters.getMissing())); + changed.stream().map(this::itemParameter).forEach(sb::append); + return sb.toString(); + } + + protected String listParameter(String title, List parameters) { + StringBuilder sb = new StringBuilder(); + parameters.stream().map(p -> itemParameter(title, p)).forEach(sb::append); + return sb.toString(); + } + + protected String itemParameter(String title, Parameter parameter) { + return this.itemParameter( + title, parameter.getName(), parameter.getIn(), parameter.getDescription()); + } + + protected String itemParameter(String title, String name, String in, String description) { + return format("%s: ", title) + + code(name) + + " in " + + code(in) + + '\n' + + metadata(description) + + '\n'; + } + + protected String itemParameter(ChangedParameter param) { + Parameter rightParam = param.getNewParameter(); + if (param.isDeprecated()) { + return itemParameter( + "Deprecated", rightParam.getName(), rightParam.getIn(), rightParam.getDescription()); + } + return itemParameter( + "Changed", rightParam.getName(), rightParam.getIn(), rightParam.getDescription()); + } + + protected String code(String string) { + return getCode() + string + getCode(); + } + + protected String metadata(String name, ChangedMetadata changedMetadata) { + return metadata("", name, changedMetadata); + } + + protected String metadata(String beginning, String name, ChangedMetadata changedMetadata) { + if (changedMetadata == null) { + return ""; + } + if (!isUnchanged(changedMetadata) && showChangedMetadata) { + return format( + "Changed %s:\n%s\nto:\n%s\n\n", + name, + metadata(beginning, changedMetadata.getLeft()), + metadata(beginning, changedMetadata.getRight())); + } else { + return metadata(beginning, name, changedMetadata.getRight()); + } + } + + protected String metadata(String metadata) { + return metadata("", metadata); + } + + protected String metadata(String beginning, String name, String metadata) { + if (StringUtils.isBlank(metadata)) { + return ""; + } + return blockquote(beginning, metadata); + } + + protected String metadata(String beginning, String metadata) { + if (StringUtils.isBlank(metadata)) { + return ""; + } + return blockquote(beginning, metadata); + } + + protected String blockquote(String beginning) { + return beginning + getBlockQuote(); + } + + protected String blockquote(String beginning, String text) { + String blockquote = blockquote(beginning); + return blockquote + text.trim().replace("\n", "\n" + blockquote) + "\n\n"; + } + + protected String type(Schema schema) { + String result = "object"; + if (schema instanceof ArraySchema) { + result = "array"; + } else if (schema.getType() != null) { + result = schema.getType(); + } + return result; + } + + protected String indent(int deepness) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < deepness; i++) { + sb.append(getPreListItem()); + } + return sb.toString(); + } + + protected Schema resolve(Schema schema) { + return refPointer.resolveRef( + diff.getNewSpecOpenApi().getComponents(), schema, schema.get$ref()); + } + + /** + * A parameter which indicates whether or not metadata (summary and metadata) changes should be + * logged in the changelog file. + */ + public boolean isShowChangedMetadata() { + return this.showChangedMetadata; + } + + /** + * A parameter which indicates whether or not metadata (summary and metadata) changes should be + * logged in the changelog file. + */ + public void setShowChangedMetadata(final boolean showChangedMetadata) { + this.showChangedMetadata = showChangedMetadata; + } +} diff --git a/core/src/test/java/org/openapitools/openapidiff/core/AsciidocRenderTest.java b/core/src/test/java/org/openapitools/openapidiff/core/AsciidocRenderTest.java new file mode 100644 index 000000000..76b9cabc0 --- /dev/null +++ b/core/src/test/java/org/openapitools/openapidiff/core/AsciidocRenderTest.java @@ -0,0 +1,70 @@ +package org.openapitools.openapidiff.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import org.junit.jupiter.api.Test; +import org.openapitools.openapidiff.core.model.ChangedOpenApi; +import org.openapitools.openapidiff.core.output.AsciidocRender; + +public class AsciidocRenderTest { + @Test + public void renderDoesNotFailWhenPropertyHasBeenRemoved() { + AsciidocRender render = new AsciidocRender(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); + ChangedOpenApi diff = + OpenApiCompare.fromLocations("missing_property_1.yaml", "missing_property_2.yaml"); + render.render(diff, outputStreamWriter); + assertThat(outputStream.toString()).isNotBlank(); + } + + @Test + public void renderDoesNotCauseStackOverflowWithRecursiveDefinitions() { + AsciidocRender render = new AsciidocRender(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); + ChangedOpenApi diff = OpenApiCompare.fromLocations("recursive_old.yaml", "recursive_new.yaml"); + render.render(diff, outputStreamWriter); + assertThat(outputStream.toString()).isNotBlank(); + } + + @Test + public void renderDoesNotFailWhenHTTPStatusCodeIsRange() { + AsciidocRender render = new AsciidocRender(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); + ChangedOpenApi diff = + OpenApiCompare.fromLocations("range_statuscode_1.yaml", "range_statuscode_2.yaml"); + render.render(diff, outputStreamWriter); + assertThat(outputStream.toString()).isNotBlank(); + } + + @Test + public void rendersChangeCorrectly() { + AsciidocRender render = new AsciidocRender(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); + ChangedOpenApi diff = + OpenApiCompare.fromLocations("missing_property_1.yaml", "missing_property_2.yaml"); + render.render(diff, outputStreamWriter); + assertThat(outputStream.toString()) + .isEqualTo( + "==== What's Changed\n" + + "\n" + + "'''\n" + + "\n" + + "===== `GET` /\n" + + "\n" + + "\n" + + "====== Return Type:\n" + + "\n" + + "Changed response : **default **\n" + + "\n" + + "* Changed content type : `application/json`\n" + + "\n" + + "** Deleted property `childProperty` (object)\n" + + "\n"); + } +} diff --git a/maven/src/main/java/org/openapitools/openapidiff/maven/OpenApiDiffMojo.java b/maven/src/main/java/org/openapitools/openapidiff/maven/OpenApiDiffMojo.java index 7e0e82fb7..c9aec95d1 100644 --- a/maven/src/main/java/org/openapitools/openapidiff/maven/OpenApiDiffMojo.java +++ b/maven/src/main/java/org/openapitools/openapidiff/maven/OpenApiDiffMojo.java @@ -14,6 +14,7 @@ import org.apache.maven.plugins.annotations.Parameter; import org.openapitools.openapidiff.core.OpenApiCompare; import org.openapitools.openapidiff.core.model.ChangedOpenApi; +import org.openapitools.openapidiff.core.output.AsciidocRender; import org.openapitools.openapidiff.core.output.ConsoleRender; import org.openapitools.openapidiff.core.output.JsonRender; import org.openapitools.openapidiff.core.output.MarkdownRender; @@ -46,6 +47,9 @@ public class OpenApiDiffMojo extends AbstractMojo { @Parameter(property = "markdownOutputFileName") String markdownOutputFileName; + @Parameter(property = "asciidocOutputFileName") + String asciidocOutputFileName; + @Override public void execute() throws MojoExecutionException, MojoFailureException { if (Boolean.TRUE.equals(skip)) { @@ -67,6 +71,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { writeDiffAsTextToFile(diff); writeDiffAsJsonToFile(diff); writeDiffAsMarkdownToFile(diff); + writeDiffAsAsciidocToFile(diff); if (failOnIncompatible && diff.isIncompatible()) { throw new BackwardIncompatibilityException("The API changes broke backward compatibility"); @@ -91,4 +96,8 @@ private void writeDiffAsJsonToFile(final ChangedOpenApi diff) { private void writeDiffAsMarkdownToFile(final ChangedOpenApi diff) { writeToFile(new MarkdownRender(), diff, markdownOutputFileName); } + + private void writeDiffAsAsciidocToFile(final ChangedOpenApi diff) { + writeToFile(new AsciidocRender(), diff, asciidocOutputFileName); + } } diff --git a/maven/src/test/java/org/openapitools/openapidiff/maven/OpenApiDiffMojoTest.java b/maven/src/test/java/org/openapitools/openapidiff/maven/OpenApiDiffMojoTest.java index 10bd186e5..2fc008490 100644 --- a/maven/src/test/java/org/openapitools/openapidiff/maven/OpenApiDiffMojoTest.java +++ b/maven/src/test/java/org/openapitools/openapidiff/maven/OpenApiDiffMojoTest.java @@ -25,6 +25,7 @@ class OpenApiDiffMojoTest { private final File consoleOutputfile = new File("target/diff.txt"); private final File markdownOutputfile = new File("target/diff.md"); private final File jsonOutputfile = new File("target/diff.json"); + private final File asciidocOutputfile = new File("target/diff.adoc"); @BeforeEach void setup() { @@ -158,6 +159,20 @@ void Should_outputToJsonFile_When_SpecIsDifferent() { assertTrue(Files.exists(jsonOutputfile.toPath())); } + @Test + void Should_outputToAsciidocFile_When_SpecIsDifferent() { + final OpenApiDiffMojo mojo = new OpenApiDiffMojo(); + mojo.oldSpec = oldSpecFile.getAbsolutePath(); + mojo.newSpec = newSpecFile.getAbsolutePath(); + + mojo.asciidocOutputFileName = asciidocOutputfile.getAbsolutePath(); + mojo.failOnChanged = true; + + assertThrows(ApiChangedException.class, mojo::execute); + + assertTrue(Files.exists(asciidocOutputfile.toPath())); + } + private void cleanupGeneratedFiles() { try { Files.deleteIfExists(Paths.get(consoleOutputfile.getPath()));