From 8a6974ee8b41057613fcd50b32de8c5543b7ad98 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 17 Aug 2023 11:34:54 -0400 Subject: [PATCH 1/2] Add support for outputting to Asciidoc format Asciidoc is very similar to Markdown, but it has a few differences: - It uses `=` for headers instead of `#` - It uses multiple `*` for nested lists instead of indents - It uses `\n'''\n` for horizontal rule instead of `---\n` - Blocks quotes in Asciidoc are done like this: ``` ---- QUOTED TEXT ACROSS MULTIPLE LINES ---- ``` versus how it done in Markdown ``` > QUOTED TEXT > ACROSS MULTIPLE LINES ``` Due to these similarities, they basically share the exact same render code! The core implementation of MarkdownRender was moved to an abstract base class MarkdownRenderBase (with abstract methods for getting the symbols for LI, HR, etc.), with MarkdownRender now just defining those abstract methods. AsciidocRender also extends the same base class, and also overrides the blockquote method since blockquoting is done quite differently and is not a simple symbol replacement. --- .../core/output/AsciidocRender.java | 62 ++ .../core/output/MarkdownRender.java | 569 +---------------- .../core/output/MarkdownRenderBase.java | 597 ++++++++++++++++++ .../openapidiff/core/AsciidocRenderTest.java | 70 ++ .../openapidiff/maven/OpenApiDiffMojo.java | 9 + .../maven/OpenApiDiffMojoTest.java | 15 + 6 files changed, 778 insertions(+), 544 deletions(-) create mode 100644 core/src/main/java/org/openapitools/openapidiff/core/output/AsciidocRender.java create mode 100644 core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRenderBase.java create mode 100644 core/src/test/java/org/openapitools/openapidiff/core/AsciidocRenderTest.java 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())); From 50aab7aa02131fa630ecaeb83aa1c27af3aa92df Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 23 Aug 2023 16:13:14 -0400 Subject: [PATCH 2/2] Add CLI option for Asciidoc and document it in the README --- README.md | 4 ++++ .../org/openapitools/openapidiff/cli/Main.java | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) 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);