Skip to content

Commit

Permalink
Add OpenAPI compatibility checker (#5106)
Browse files Browse the repository at this point in the history
* add openapi compatibility checker

* formatting

* fix native build

* formatting

---------

Co-authored-by: simon.wagner <[email protected]>
  • Loading branch information
simonnepomuk and simon.wagner authored Sep 10, 2024
1 parent 6155ced commit c5acb15
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ a| Full
a| Mapping detection not supported
|*OpenAPI*
a| Full
a| None
a| Full
a| Full
|*AsyncAPI*
a| Syntax Only
Expand Down
17 changes: 17 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@
<jackson-datatype-json-org.version>2.17.2</jackson-datatype-json-org.version>
<jackson-dataformat-yaml.version>2.15.2</jackson-dataformat-yaml.version>

<!-- OpenAPI -->
<openapi-diff.version>2.0.1</openapi-diff.version>
<!-- We need to manually include the version because of dependency issues with snakeyaml -->
<swagger-parser-v3.version>2.1.22</swagger-parser-v3.version>

<!-- Dependency versions -->
<lombok.version>1.18.34</lombok.version>
<h2.version>1.4.199</h2.version>
Expand Down Expand Up @@ -610,6 +615,18 @@
<version>${graphql.version}</version>
</dependency>

<!-- OpenAPI -->
<dependency>
<groupId>org.openapitools.openapidiff</groupId>
<artifactId>openapi-diff-core</artifactId>
<version>${openapi-diff.version}</version>
</dependency>
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser-v3</artifactId>
<version>${swagger-parser-v3.version}</version>
</dependency>

<dependency>
<groupId>com.github.everit-org.json-schema</groupId>
<artifactId>org.everit.json.schema</artifactId>
Expand Down
16 changes: 16 additions & 0 deletions schema-util/openapi/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,27 @@
<artifactId>apicurio-data-models</artifactId>
</dependency>

<dependency>
<groupId>org.openapitools.openapidiff</groupId>
<artifactId>openapi-diff-core</artifactId>
</dependency>

<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser-v3</artifactId>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.apicurio.registry.rules.compatibility;

import io.apicurio.registry.content.TypedContent;
import org.openapitools.openapidiff.core.OpenApiCompare;
import org.openapitools.openapidiff.core.model.Changed;
import org.openapitools.openapidiff.core.model.ChangedOpenApi;
import org.openapitools.openapidiff.core.model.ChangedOperation;
import org.openapitools.openapidiff.core.model.ChangedSchema;
import org.openapitools.openapidiff.core.model.Endpoint;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class OpenApiCompatibilityChecker extends AbstractCompatibilityChecker<Object> {

@Override
protected Set<Object> isBackwardsCompatibleWith(final String existing, final String proposed,
final Map<String, TypedContent> resolvedReferences) {
ChangedOpenApi diff = OpenApiCompare.fromContents(existing, proposed);

List<ChangedOperation> incompatibleOperations = diff.getChangedOperations().stream()
.filter(Objects::nonNull).filter(Changed::isIncompatible).toList();

List<ChangedSchema> incompatibleSchemas = diff.getChangedSchemas().stream().filter(Objects::nonNull)
.filter(Changed::isIncompatible).toList();

List<Endpoint> missingEndpoints = diff.getMissingEndpoints().stream().filter(Objects::nonNull)
.toList();

return Stream.of(incompatibleOperations, incompatibleSchemas, missingEndpoints).flatMap(List::stream)
.collect(Collectors.toSet());
}

@Override
protected CompatibilityDifference transform(final Object original) {
if (original instanceof ChangedOperation) {
return new SimpleCompatibilityDifference("Incompatible operation",
((ChangedOperation) original).getPathUrl());
} else if (original instanceof ChangedSchema) {
return new SimpleCompatibilityDifference("Incompatible schema",
((ChangedSchema) original).getNewSchema().getName());
} else if (original instanceof Endpoint) {
return new SimpleCompatibilityDifference("Missing endpoint", ((Endpoint) original).getPathUrl());
}

throw new IllegalArgumentException("Unsupported type: " + original.getClass().getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.apicurio.registry.rules.compatibility;

import io.apicurio.registry.content.ContentHandle;
import io.apicurio.registry.content.TypedContent;
import io.apicurio.registry.types.ContentTypes;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Collections;

class OpenApiCompatibilityCheckerTest {
private TypedContent toTypedContent(String content) {
return TypedContent.create(ContentHandle.create(content), ContentTypes.APPLICATION_JSON);
}

private static final String BEFORE = """
{
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"paths": {
"/test": {
"get": {
"responses": {
"200": {
"description": "OK"
}
}
}
}
}
}
""";
private static final String AFTER_VALID = """
{
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"paths": {
"/test": {
"get": {
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/test2": {
"get": {
"responses": {
"200": {
"description": "OK"
}
}
}
}
}
}
""";
private static final String AFTER_INVALID = """
{
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"paths": {}
}
""";

@Test
void givenBackwardCompatibleChange_whenCheckingCompatibility_thenReturnCompatibleResult() {
OpenApiCompatibilityChecker checker = new OpenApiCompatibilityChecker();
TypedContent existing = toTypedContent(BEFORE);
TypedContent proposed = toTypedContent(AFTER_VALID);
CompatibilityExecutionResult result = checker.testCompatibility(CompatibilityLevel.BACKWARD,
Collections.singletonList(existing), proposed, Collections.emptyMap());

Assertions.assertTrue(result.isCompatible());
}

@Test
void givenIncompatibleChange_whenCheckingCompatibility_thenReturnIncompatibleResult() {
OpenApiCompatibilityChecker checker = new OpenApiCompatibilityChecker();
TypedContent existing = toTypedContent(BEFORE);
TypedContent proposed = toTypedContent(AFTER_INVALID);
CompatibilityExecutionResult result = checker.testCompatibility(CompatibilityLevel.BACKWARD,
Collections.singletonList(existing), proposed, Collections.emptyMap());

Assertions.assertFalse(result.isCompatible());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import io.apicurio.registry.content.refs.ReferenceFinder;
import io.apicurio.registry.content.util.ContentTypeUtil;
import io.apicurio.registry.rules.compatibility.CompatibilityChecker;
import io.apicurio.registry.rules.compatibility.NoopCompatibilityChecker;
import io.apicurio.registry.rules.compatibility.OpenApiCompatibilityChecker;
import io.apicurio.registry.rules.validity.ContentValidator;
import io.apicurio.registry.rules.validity.OpenApiContentValidator;
import io.apicurio.registry.types.ArtifactType;
Expand Down Expand Up @@ -49,7 +49,7 @@ public String getArtifactType() {

@Override
protected CompatibilityChecker createCompatibilityChecker() {
return NoopCompatibilityChecker.INSTANCE;
return new OpenApiCompatibilityChecker();
}

@Override
Expand Down

0 comments on commit c5acb15

Please sign in to comment.