Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 22 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
# OpenAPI-diff
# OpenAPI-diff

Compare two OpenAPI specifications (3.x) and render the difference to HTML plain text, Markdown files, or JSON files.

[![Build](https://github.com/OpenAPITools/openapi-diff/workflows/Main%20Build/badge.svg)](https://github.com/OpenAPITools/openapi-diff/actions?query=branch%3Amaster+workflow%3A"Main+Build")
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=OpenAPITools_openapi-diff&metric=alert_status)](https://sonarcloud.io/dashboard?id=OpenAPITools_openapi-diff)
[Build](https://github.com/OpenAPITools/openapi-diff/actions?query=branch%3Amaster+workflow%3A"Main+Build")
[Quality Gate Status](https://sonarcloud.io/dashboard?id=OpenAPITools_openapi-diff)

[![Maven Central](https://img.shields.io/maven-central/v/org.openapitools.openapidiff/openapi-diff-core)](https://search.maven.org/artifact/org.openapitools.openapidiff/openapi-diff-core)
[Maven Central](https://search.maven.org/artifact/org.openapitools.openapidiff/openapi-diff-core)

[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/OpenAPITools/openapi-diff)
[![Join the Slack chat room](https://img.shields.io/badge/Slack-Join%20the%20chat%20room-orange)](https://join.slack.com/t/openapi-generator/shared_invite/zt-12jxxd7p2-XUeQM~4pzsU9x~eGLQqX2g)
[Contribute with Gitpod](https://gitpod.io/#https://github.com/OpenAPITools/openapi-diff)
[Join the Slack chat room](https://join.slack.com/t/openapi-generator/shared_invite/zt-12jxxd7p2-XUeQM~4pzsU9x~eGLQqX2g)

[![Docker Automated build](https://img.shields.io/docker/automated/openapitools/openapi-diff)](https://hub.docker.com/r/openapitools/openapi-diff)
[![Docker Image Version](https://img.shields.io/docker/v/openapitools/openapi-diff?sort=semver)](https://hub.docker.com/r/openapitools/openapi-diff/tags)
[Docker Automated build](https://hub.docker.com/r/openapitools/openapi-diff)
[Docker Image Version](https://hub.docker.com/r/openapitools/openapi-diff/tags)

# Requirements

* Java 8
- Java 8

# Feature

* Supports OpenAPI spec v3.0.
* In-depth comparison of parameters, responses, endpoint, http method (GET,POST,PUT,DELETE...)
* Supports swagger api Authorization
* Render difference of property with Expression Language
* HTML, Markdown, Asciidoc & JSON render
- Supports OpenAPI spec v3.0.
- In-depth comparison of parameters, responses, endpoint, http method (GET,POST,PUT,DELETE...)
- Supports swagger api Authorization
- Render difference of property with Expression Language
- HTML, Markdown, Asciidoc & JSON render

# Maven

Expand All @@ -38,11 +38,13 @@ Available on [Maven Central](https://search.maven.org/artifact/org.openapitools.
```

# Homebrew

Available for Mac users on [brew](https://formulae.brew.sh/formula/openapi-diff)

```bash
brew install openapi-diff
```

Usage instructions in [Usage -> Command line](#command-line)

# Docker
Expand Down Expand Up @@ -211,6 +213,7 @@ ChangedOpenApi diff = OpenApiCompare.fromLocations(oldSpec, newSpec, null, optio
### Render difference

---

#### HTML

```java
Expand Down Expand Up @@ -500,7 +503,8 @@ openapi-diff is released under the Apache License 2.0.

# Thanks

* Adarsh Sharma / [adarshsharma](https://github.com/adarshsharma)
* Quentin Desramé / [quen2404](https://github.com/quen2404)
* [Sayi](https://github.com/Sayi) for his project [swagger-diff](https://github.com/Sayi/swagger-diff)
which was a source of inspiration for this tool
- Adarsh Sharma / [adarshsharma](https://github.com/adarshsharma)
- Quentin Desramé / [quen2404](https://github.com/quen2404)
- [Sayi](https://github.com/Sayi) for his project [swagger-diff](https://github.com/Sayi/swagger-diff)
which was a source of inspiration for this tool

29 changes: 20 additions & 9 deletions cli/src/main/java/org/openapitools/openapidiff/cli/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.openapitools.openapidiff.core.output.AsciidocRender;
import org.openapitools.openapidiff.core.output.ConsoleRender;
import org.openapitools.openapidiff.core.output.HtmlRender;
import org.openapitools.openapidiff.core.output.I18n;
import org.openapitools.openapidiff.core.output.JsonRender;
import org.openapitools.openapidiff.core.output.MarkdownRender;
import org.slf4j.Logger;
Expand Down Expand Up @@ -143,6 +144,13 @@ public static void main(String... args) {
.argName("file")
.desc("export diff as json in given file")
.build());
options.addOption(
Option.builder()
.longOpt("lang")
.hasArg()
.argName("language")
.desc("output language (en, zh-Hant, zh-CN). Default: en")
.build());

// create the parser
CommandLineParser parser = new DefaultParser();
Expand All @@ -155,9 +163,13 @@ public static void main(String... args) {
}
if (line.hasOption("version") || line.hasOption("v")) {
String version = Main.class.getPackage().getImplementationVersion();
System.out.println("openapi-diff version: " + (version != null ? version : "DEV"));
System.out.println(
I18n.getMessage("cli.version.prefix") + " " + (version != null ? version : "DEV"));
System.exit(0);
}
if (line.hasOption("lang")) {
I18n.setLocale(I18n.parseLocale(line.getOptionValue("lang")));
}
String logLevel = "ERROR";
if (line.hasOption("off")) {
logLevel = "OFF";
Expand Down Expand Up @@ -185,10 +197,7 @@ public static void main(String... args) {
&& !logLevel.equalsIgnoreCase("WARN")
&& !logLevel.equalsIgnoreCase("ERROR")
&& !logLevel.equalsIgnoreCase("OFF")) {
throw new ParseException(
String.format(
"Invalid log level. Expected: [TRACE, DEBUG, INFO, WARN, ERROR, OFF]. Given: %s",
logLevel));
throw new ParseException(I18n.getMessage("cli.invalid.log.level", logLevel));
}
}
if (line.hasOption("state")) {
Expand All @@ -199,7 +208,7 @@ public static void main(String... args) {
root.setLevel(Level.toLevel(logLevel));

if (line.getArgList().size() < 2) {
throw new ParseException("Missing arguments");
throw new ParseException(I18n.getMessage("cli.missing.arguments"));
}
String oldPath = line.getArgList().get(0);
String newPath = line.getArgList().get(1);
Expand All @@ -223,7 +232,8 @@ public static void main(String... args) {
for (String propKeyAndVal : configProps) {
String[] split = propKeyAndVal.split(":");
if (split.length != 2 || split[0].isEmpty() || split[1].isEmpty()) {
throw new IllegalArgumentException("--config-prop unexpected format: " + propKeyAndVal);
throw new IllegalArgumentException(
I18n.getMessage("cli.config.prop.unexpected.format") + " " + propKeyAndVal);
}
optionBuilder.configProperty(split[0], split[1]);
}
Expand Down Expand Up @@ -283,12 +293,13 @@ public static void main(String... args) {
}
} catch (ParseException e) {
// oops, something went wrong
System.err.println("Parsing failed. Reason: " + e.getMessage());
System.err.println(I18n.getMessage("cli.parsing.failed") + " " + e.getMessage());
printHelp(options);
System.exit(2);
} catch (Exception e) {
System.err.println(
"Unexpected exception. Reason: "
I18n.getMessage("cli.unexpected.exception")
+ " "
+ e.getMessage()
+ "\n"
+ ExceptionUtils.getStackTrace(e));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void render(ChangedOpenApi diff, OutputStreamWriter outputStreamWriter) {
diff.getNewSpecOpenApi().getInfo().getVersion()));
safelyAppend(outputStreamWriter, System.lineSeparator());
safelyAppend(outputStreamWriter, System.lineSeparator());
safelyAppend(outputStreamWriter, "NOTE: No differences. Specifications are equivalent");
safelyAppend(outputStreamWriter, I18n.getMessage("note.no.differences"));
} else {
safelyAppend(
outputStreamWriter,
Expand All @@ -63,13 +63,13 @@ public void render(ChangedOpenApi diff, OutputStreamWriter outputStreamWriter) {
safelyAppend(outputStreamWriter, System.lineSeparator());

List<Endpoint> newEndpoints = diff.getNewEndpoints();
listEndpoints(newEndpoints, "What's New", outputStreamWriter);
listEndpoints(newEndpoints, I18n.getMessage("whats.new"), outputStreamWriter);

List<Endpoint> missingEndpoints = diff.getMissingEndpoints();
listEndpoints(missingEndpoints, "What's Deleted", outputStreamWriter);
listEndpoints(missingEndpoints, I18n.getMessage("whats.deleted"), outputStreamWriter);

List<Endpoint> deprecatedEndpoints = diff.getDeprecatedEndpoints();
listEndpoints(deprecatedEndpoints, "What's Deprecated", outputStreamWriter);
listEndpoints(deprecatedEndpoints, I18n.getMessage("whats.deprecated"), outputStreamWriter);

List<ChangedOperation> changedOperations = diff.getChangedOperations();
ol_changed(changedOperations, outputStreamWriter);
Expand All @@ -78,8 +78,8 @@ public void render(ChangedOpenApi diff, OutputStreamWriter outputStreamWriter) {
safelyAppend(
outputStreamWriter,
diff.isCompatible()
? "NOTE: API changes are backward compatible"
: "WARNING: API changes broke backward compatibility");
? I18n.getMessage("note.backward.compatible")
: I18n.getMessage("warning.broke.compatibility"));
safelyAppend(outputStreamWriter, System.lineSeparator());
}
try {
Expand All @@ -94,7 +94,7 @@ private void ol_changed(
if (null == operations || operations.isEmpty()) {
return;
}
safelyAppend(outputStreamWriter, title("What's Changed", 2));
safelyAppend(outputStreamWriter, title(I18n.getMessage("whats.changed"), 2));
safelyAppend(outputStreamWriter, System.lineSeparator());
for (ChangedOperation operation : operations) {
String pathUrl = operation.getPathUrl();
Expand All @@ -105,30 +105,33 @@ private void ol_changed(
safelyAppend(outputStreamWriter, itemEndpoint(method, pathUrl, desc));
safelyAppend(outputStreamWriter, System.lineSeparator());
if (result(operation.getOperationId()).isDifferent()) {
safelyAppend(outputStreamWriter, "* Operation ID:");
safelyAppend(outputStreamWriter, "* " + I18n.getMessage("operation.id") + ":");
safelyAppend(outputStreamWriter, System.lineSeparator());
safelyAppend(
outputStreamWriter,
String.format(
"** Changed %s to %s",
operation.getOperationId().getLeft(), operation.getOperationId().getRight()));
"** %s %s %s %s",
I18n.getMessage("action.changed"),
operation.getOperationId().getLeft(),
I18n.getMessage("to"),
operation.getOperationId().getRight()));
safelyAppend(outputStreamWriter, System.lineSeparator());
}
if (result(operation.getParameters()).isDifferent()) {
safelyAppend(outputStreamWriter, "* Parameter:");
safelyAppend(outputStreamWriter, "* " + I18n.getMessage("parameter") + ":");
safelyAppend(outputStreamWriter, System.lineSeparator());
safelyAppend(outputStreamWriter, ul_param(operation.getParameters()));
safelyAppend(outputStreamWriter, System.lineSeparator());
}
if (operation.resultRequestBody().isDifferent()) {
safelyAppend(outputStreamWriter, "* Request:");
safelyAppend(outputStreamWriter, "* " + I18n.getMessage("request") + ":");
safelyAppend(outputStreamWriter, System.lineSeparator());
safelyAppend(
outputStreamWriter, ul_content(operation.getRequestBody().getContent(), true, 2));
safelyAppend(outputStreamWriter, System.lineSeparator());
}
if (operation.resultApiResponses().isDifferent()) {
safelyAppend(outputStreamWriter, "* Return Type:");
safelyAppend(outputStreamWriter, "* " + I18n.getMessage("return.type") + ":");
safelyAppend(outputStreamWriter, System.lineSeparator());
safelyAppend(outputStreamWriter, ul_response(operation.getApiResponses()));
safelyAppend(outputStreamWriter, System.lineSeparator());
Expand All @@ -142,13 +145,15 @@ private String ul_response(ChangedApiResponse changedApiResponse) {
Map<String, ChangedResponse> changedResponses = changedApiResponse.getChanged();
StringBuilder sb = new StringBuilder();
for (String propName : addResponses.keySet()) {
sb.append(itemResponse("** Add ", propName));
sb.append(itemResponse("** " + I18n.getMessage("action.add") + " ", propName));
}
for (String propName : delResponses.keySet()) {
sb.append(itemResponse("** Deleted ", propName));
sb.append(itemResponse("** " + I18n.getMessage("action.deleted") + " ", propName));
}
for (Entry<String, ChangedResponse> entry : changedResponses.entrySet()) {
sb.append(itemChangedResponse("** Changed ", entry.getKey(), entry.getValue()));
sb.append(
itemChangedResponse(
"** " + I18n.getMessage("action.changed") + " ", entry.getKey(), entry.getValue()));
}
return sb.toString();
}
Expand All @@ -165,7 +170,9 @@ private String itemResponse(String title, String code) {

private String itemChangedResponse(String title, String contentType, ChangedResponse response) {
return itemResponse(title, contentType)
+ "** Media types:"
+ "** "
+ I18n.getMessage("media.types")
+ ":"
+ System.lineSeparator()
+ ul_content(response.getContent(), false, 3);
}
Expand All @@ -176,15 +183,19 @@ private String ul_content(ChangedContent changedContent, boolean isRequest, int
return sb.toString();
}
for (String propName : changedContent.getIncreased().keySet()) {
sb.append(itemContent("Added ", propName, indent));
sb.append(itemContent(I18n.getMessage("action.added") + " ", propName, indent));
}
for (String propName : changedContent.getMissing().keySet()) {
sb.append(itemContent("Deleted ", propName, indent));
sb.append(itemContent(I18n.getMessage("action.deleted") + " ", propName, indent));
}
for (String propName : changedContent.getChanged().keySet()) {
sb.append(
itemContent(
"Changed ", propName, indent, changedContent.getChanged().get(propName), isRequest));
I18n.getMessage("action.changed") + " ",
propName,
indent,
changedContent.getChanged().get(propName),
isRequest));
}
return sb.toString();
}
Expand All @@ -201,8 +212,11 @@ private String itemContent(
boolean isRequest) {
StringBuilder sb = new StringBuilder();
sb.append(itemContent(title, contentType, indent))
.append(itemContent("Schema:", "", indent))
.append(changedMediaType.isCompatible() ? "Backward compatible" : "Broken compatibility")
.append(itemContent(I18n.getMessage("schema") + ":", "", indent))
.append(
changedMediaType.isCompatible()
? I18n.getMessage("backward.compatible")
: I18n.getMessage("broken.compatibility"))
.append(System.lineSeparator());
if (!changedMediaType.isCompatible() && changedMediaType.getSchema() != null) {
sb.append(incompatibilities(changedMediaType.getSchema()));
Expand All @@ -221,13 +235,17 @@ private String incompatibilities(String propName, final ChangedSchema schema) {
}
if (schema.isCoreChanged() == DiffResult.INCOMPATIBLE && schema.isChangedType()) {
String type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema());
sb.append(property(propName, "Changed property type", type));
sb.append(property(propName, I18n.getMessage("changed.property.type"), type));
sb.append(System.lineSeparator());
sb.append(System.lineSeparator());
}
String prefix = propName.isEmpty() ? "" : propName + ".";
sb.append(
properties(prefix, "Missing property", schema.getMissingProperties(), schema.getContext()));
properties(
prefix,
I18n.getMessage("missing.property"),
schema.getMissingProperties(),
schema.getContext()));
schema
.getChangedProperties()
.forEach((name, property) -> sb.append(incompatibilities(prefix + name, property)));
Expand Down Expand Up @@ -286,26 +304,34 @@ private String ul_param(ChangedParameters changedParameters) {
List<ChangedParameter> changed = changedParameters.getChanged();
StringBuilder sb = new StringBuilder();
for (Parameter param : addParameters) {
sb.append(itemParam("** Add ", param));
sb.append(itemParam("** " + I18n.getMessage("action.add") + " ", param));
}
for (ChangedParameter param : changed) {
sb.append(li_changedParam(param));
}
for (Parameter param : delParameters) {
sb.append(itemParam("** Delete ", param));
sb.append(itemParam("** " + I18n.getMessage("action.delete") + " ", param));
}
return sb.toString();
}

private String itemParam(String title, Parameter param) {
return title + param.getName() + " in " + param.getIn() + System.lineSeparator();
return title
+ param.getName()
+ " "
+ I18n.getMessage("in")
+ " "
+ param.getIn()
+ System.lineSeparator();
}

private String li_changedParam(ChangedParameter changeParam) {
if (changeParam.isDeprecated()) {
return itemParam("** Deprecated ", changeParam.getNewParameter());
return itemParam(
"** " + I18n.getMessage("action.deprecated") + " ", changeParam.getNewParameter());
} else {
return itemParam("** Changed ", changeParam.getNewParameter());
return itemParam(
"** " + I18n.getMessage("action.changed") + " ", changeParam.getNewParameter());
}
}

Expand Down
Loading