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
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,50 @@ Add openapi-diff to your POM to show diffs when you test your Maven project. You
</plugin>
```

### Maven Site Report

The `report` goal adds an *OpenAPI Diff* page to the *Project Reports* of your [Maven site](https://maven.apache.org/plugins/maven-site-plugin/). The page shows the overall result and the new, deleted, deprecated and changed endpoints, and explains which changes break backward compatibility. It accepts the same `oldSpec`, `newSpec`, `configFiles`, `configProps` and `skip` parameters as the `diff` goal. The report only documents changes and never fails the build; use the `diff` goal for that.

```xml
<build>
<pluginManagement>
<plugins>
<!-- The report needs maven-site-plugin 3.21.0 or newer (Doxia 2) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.22.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>

<reporting>
<plugins>
<plugin>
<groupId>org.openapitools.openapidiff</groupId>
<artifactId>openapi-diff-maven</artifactId>
<version>${openapi-diff-version}</version>
<configuration>
<oldSpec>https://petstore3.swagger.io/api/v3/openapi.json</oldSpec>
<newSpec>${project.basedir}/target/openapi.yaml</newSpec>
</configuration>
<reportSets>
<reportSet>
<reports>
<report>report</report>
</reports>
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>
```

If the new specification is generated during the build, run `mvn verify site` so it exists when the site is rendered.

The page can also be generated on its own with `mvn -DoldSpec=<old spec> -DnewSpec=<new spec> org.openapitools.openapidiff:openapi-diff-maven:report`, which writes `target/reports/openapi-diff.html`. Maven only applies the `<reporting>` configuration when it renders the site, so a direct run needs the specifications as properties, or in the plugin's `<configuration>` under `<build>`.

## Direct Invocation

```java
Expand Down
33 changes: 33 additions & 0 deletions maven-example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@
</properties>

<build>
<pluginManagement>
<plugins>
<!-- The openapi-diff report needs maven-site-plugin 3.21.0 or newer -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.22.0</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.openapitools.openapidiff</groupId>
Expand Down Expand Up @@ -49,4 +59,27 @@
</plugins>
</build>

<!-- `mvn site` adds an "OpenAPI Diff" page under Project Reports -->
<reporting>
<excludeDefaults>true</excludeDefaults>
<plugins>
<plugin>
<groupId>org.openapitools.openapidiff</groupId>
<artifactId>openapi-diff-maven</artifactId>
<version>${project.version}</version>
<configuration>
<oldSpec>${project.basedir}/../maven/src/test/resources/oldspec.yaml</oldSpec>
<newSpec>${project.basedir}/../maven/src/test/resources/newspec.yaml</newSpec>
</configuration>
<reportSets>
<reportSet>
<reports>
<report>report</report>
</reports>
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>

</project>
11 changes: 11 additions & 0 deletions maven/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@
<version>3.4</version>
<scope>provided</scope>
</dependency>
<!-- Maven Reporting API 4 / Doxia 2: used by the `report` goal (maven-site-plugin 3.21.0+) -->
<dependency>
<groupId>org.apache.maven.reporting</groupId>
<artifactId>maven-reporting-api</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.reporting</groupId>
<artifactId>maven-reporting-impl</artifactId>
<version>4.0.0</version>
</dependency>

<dependency>
<groupId>org.openapitools.openapidiff</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
}

try {
OpenApiDiffOptions.Builder optionBuilder = OpenApiDiffOptions.builder();
if (configFiles != null) {
configFiles.forEach(optionBuilder::configYaml);
}
if (configProps != null) {
configProps.forEach(optionBuilder::configProperty);
}
OpenApiDiffOptions options = optionBuilder.build();
OpenApiDiffOptions options = OpenApiDiffOptionsFactory.create(configFiles, configProps);

final ChangedOpenApi diff = OpenApiCompare.fromLocations(oldSpec, newSpec, null, options);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.openapitools.openapidiff.maven;

import java.io.File;
import java.util.List;
import java.util.Map;
import org.openapitools.openapidiff.core.compare.OpenApiDiffOptions;

/** Builds the comparison options shared by the {@code diff} and {@code report} goals. */
final class OpenApiDiffOptionsFactory {

private OpenApiDiffOptionsFactory() {}

/**
* Creates the options from the plugin configuration. Config files are applied first and config
* properties are applied last, so properties override values from the files.
*
* @param configFiles YAML config files, may be {@code null}
* @param configProps config properties, may be {@code null}
* @return the comparison options
*/
static OpenApiDiffOptions create(List<File> configFiles, Map<String, String> configProps) {
OpenApiDiffOptions.Builder builder = OpenApiDiffOptions.builder();
if (configFiles != null) {
configFiles.forEach(builder::configYaml);
}
if (configProps != null) {
configProps.forEach(builder::configProperty);
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package org.openapitools.openapidiff.maven;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.reporting.AbstractMavenReport;
import org.apache.maven.reporting.MavenReportException;
import org.openapitools.openapidiff.core.OpenApiCompare;
import org.openapitools.openapidiff.core.model.ChangedOpenApi;

/**
* Adds an "OpenAPI Diff" page to the Maven site, listing the differences between two OpenAPI
* specifications and whether they are backward compatible.
*
* <p>Configure the plugin in the {@code <reporting>} section of the POM to get the page under
* "Project Reports" when running {@code mvn site}. Unlike the {@code diff} goal, this goal never
* fails the build because of API changes: it only documents them.
*/
@Mojo(name = "report", defaultPhase = LifecyclePhase.SITE)
public class OpenApiDiffReport extends AbstractMavenReport {

/** Location of the reference specification (file path or URL), e.g. the released API. */
@Parameter(property = "oldSpec")
String oldSpec;

/** Location of the specification to compare with {@code oldSpec} (file path or URL). */
@Parameter(property = "newSpec")
String newSpec;

/** Config files to override the default incompatibility checks. Later files win. */
@Parameter(property = "configFiles")
List<File> configFiles;

/** Config properties to override the default incompatibility checks and config files. */
@Parameter(property = "configProps")
Map<String, String> configProps;

/** Skips the report. */
@Parameter(property = "skip", defaultValue = "false")
Boolean skip = false;

@Override
public String getOutputName() {
return "openapi-diff";
}

@Override
public String getName(Locale locale) {
return OpenApiDiffReportRenderer.TITLE;
}

@Override
public String getDescription(Locale locale) {
return "Changes between two OpenAPI specifications and whether they are backward compatible.";
}

@Override
public boolean canGenerateReport() {
if (Boolean.TRUE.equals(skip)) {
getLog().info("Skipping openapi-diff report");
return false;
}
if (isBlank(oldSpec) || isBlank(newSpec)) {
getLog().warn("Skipping openapi-diff report: both oldSpec and newSpec must be configured");
return false;
}
return true;
}

@Override
protected void executeReport(Locale locale) throws MavenReportException {
final ChangedOpenApi diff;
try {
diff =
OpenApiCompare.fromLocations(
oldSpec, newSpec, null, OpenApiDiffOptionsFactory.create(configFiles, configProps));
} catch (RuntimeException e) {
throw new MavenReportException(
String.format(
"Unable to compare %s with %s: %s. If a specification is generated during the"
+ " build, generate the site with 'mvn verify site'.",
displayLocation(oldSpec), displayLocation(newSpec), e.getMessage()),
e);
}
new OpenApiDiffReportRenderer(
getSink(), diff, displayLocation(oldSpec), displayLocation(newSpec))
.render();
}

/**
* Returns the location to show on the page without exposing the build machine or secrets. Files,
* including {@code file:} URIs, are shown relative to the project directory, or by name when they
* are outside of it. URLs are shown without user info, query and fragment, which may hold
* credentials.
*/
String displayLocation(String location) {
URI uri = toUri(location);
if (uri != null && "file".equalsIgnoreCase(uri.getScheme())) {
try {
return displayPath(Paths.get(uri));
} catch (IllegalArgumentException e) {
// e.g. an opaque URI such as file:relative.yaml
return lastSegment(location);
}
}
if (uri != null && uri.getHost() != null) {
String port = uri.getPort() != -1 ? ":" + uri.getPort() : "";
String path = uri.getRawPath() != null ? uri.getRawPath() : "";
return uri.getScheme() + "://" + uri.getHost() + port + path;
}
try {
return displayPath(Paths.get(location));
} catch (InvalidPathException e) {
return lastSegment(location);
}
}

private String displayPath(Path location) {
Path path = location.toAbsolutePath().normalize();
MavenProject project = getProject();
if (project != null && project.getBasedir() != null) {
Path basedir = project.getBasedir().toPath().toAbsolutePath().normalize();
if (path.startsWith(basedir)) {
return basedir.relativize(path).toString().replace(File.separatorChar, '/');
}
}
Path fileName = path.getFileName();
return fileName != null ? fileName.toString() : "";
}

/** Parses a location as a URI with a scheme, or returns {@code null} for plain paths. */
private static URI toUri(String location) {
try {
URI uri = new URI(location);
// a one-letter scheme is a Windows drive, as in C:/specs/openapi.yaml
return uri.getScheme() != null && uri.getScheme().length() > 1 ? uri : null;
} catch (URISyntaxException e) {
return null;
}
}

/** Returns the part after the last slash, without query and fragment. */
private static String lastSegment(String location) {
String withoutQuery = location.split("[?#]", 2)[0];
int separator = Math.max(withoutQuery.lastIndexOf('/'), withoutQuery.lastIndexOf('\\'));
return withoutQuery.substring(separator + 1);
}

private static boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
}
Loading