Skip to content

Commit 2ee5f18

Browse files
committed
Option to allow response enum additions
- Addresses issue OpenAPITools#303 - Default behavior unchanged to not allow response enum additions. - Adds option to allow response enum additions, both in CLI and programmatically - Tests added for new behavior related to lenient vs strict response enum behavior - Tests also added for request enum behavior (which has been left unchanged).
1 parent e1d436a commit 2ee5f18

File tree

12 files changed

+292
-14
lines changed

12 files changed

+292
-14
lines changed

Diff for: cli/src/main/java/org/openapitools/openapidiff/cli/Main.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.apache.commons.io.FileUtils;
1818
import org.apache.commons.lang3.exception.ExceptionUtils;
1919
import org.openapitools.openapidiff.core.OpenApiCompare;
20+
import org.openapitools.openapidiff.core.compare.OpenApiDiffOptions;
2021
import org.openapitools.openapidiff.core.model.ChangedOpenApi;
2122
import org.openapitools.openapidiff.core.output.ConsoleRender;
2223
import org.openapitools.openapidiff.core.output.HtmlRender;
@@ -49,6 +50,12 @@ public static void main(String... args) {
4950
.longOpt("fail-on-changed")
5051
.desc("Fail if API changed but is backward compatible")
5152
.build());
53+
options.addOption(
54+
Option.builder()
55+
.longOpt("allow-response-enum-additions")
56+
.desc(
57+
"Do not fail backward compatibility check when enum values are added to responses")
58+
.build());
5259
options.addOption(Option.builder().longOpt("trace").desc("be extra verbose").build());
5360
options.addOption(
5461
Option.builder().longOpt("debug").desc("Print debugging information").build());
@@ -172,7 +179,11 @@ public static void main(String... args) {
172179
auths = Collections.singletonList(new AuthorizationValue(headers[0], headers[1], "header"));
173180
}
174181

175-
ChangedOpenApi result = OpenApiCompare.fromLocations(oldPath, newPath, auths);
182+
OpenApiDiffOptions compareOpts =
183+
OpenApiDiffOptions.builder()
184+
.allowResponseEnumAdditions(line.hasOption("allow-response-enum-additions"))
185+
.build();
186+
ChangedOpenApi result = OpenApiCompare.fromLocations(oldPath, newPath, auths, compareOpts);
176187
ConsoleRender consoleRender = new ConsoleRender();
177188
if (!logLevel.equals("OFF")) {
178189
System.out.println(consoleRender.render(result));

Diff for: core/src/main/java/org/openapitools/openapidiff/core/OpenApiCompare.java

+68-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.io.File;
88
import java.util.List;
99
import org.openapitools.openapidiff.core.compare.OpenApiDiff;
10+
import org.openapitools.openapidiff.core.compare.OpenApiDiffOptions;
1011
import org.openapitools.openapidiff.core.model.ChangedOpenApi;
1112

1213
public class OpenApiCompare {
@@ -40,7 +41,25 @@ public static ChangedOpenApi fromContents(String oldContent, String newContent)
4041
*/
4142
public static ChangedOpenApi fromContents(
4243
String oldContent, String newContent, List<AuthorizationValue> auths) {
43-
return fromSpecifications(readContent(oldContent, auths), readContent(newContent, auths));
44+
return fromContents(oldContent, newContent, auths, OpenApiDiffOptions.builder().build());
45+
}
46+
47+
/**
48+
* compare two openapi doc
49+
*
50+
* @param oldContent old api-doc location:Json or Http
51+
* @param newContent new api-doc location:Json or Http
52+
* @param auths
53+
* @param options
54+
* @return Comparison result
55+
*/
56+
public static ChangedOpenApi fromContents(
57+
String oldContent,
58+
String newContent,
59+
List<AuthorizationValue> auths,
60+
OpenApiDiffOptions options) {
61+
return fromSpecifications(
62+
readContent(oldContent, auths), readContent(newContent, auths), options);
4463
}
4564

4665
/**
@@ -64,7 +83,21 @@ public static ChangedOpenApi fromFiles(File oldFile, File newFile) {
6483
*/
6584
public static ChangedOpenApi fromFiles(
6685
File oldFile, File newFile, List<AuthorizationValue> auths) {
67-
return fromLocations(oldFile.getAbsolutePath(), newFile.getAbsolutePath(), auths);
86+
return fromFiles(oldFile, newFile, auths, OpenApiDiffOptions.builder().build());
87+
}
88+
89+
/**
90+
* compare two openapi doc
91+
*
92+
* @param oldFile old api-doc file
93+
* @param newFile new api-doc file
94+
* @param auths
95+
* @param options
96+
* @return Comparison result
97+
*/
98+
public static ChangedOpenApi fromFiles(
99+
File oldFile, File newFile, List<AuthorizationValue> auths, OpenApiDiffOptions options) {
100+
return fromLocations(oldFile.getAbsolutePath(), newFile.getAbsolutePath(), auths, options);
68101
}
69102

70103
/**
@@ -88,7 +121,25 @@ public static ChangedOpenApi fromLocations(String oldLocation, String newLocatio
88121
*/
89122
public static ChangedOpenApi fromLocations(
90123
String oldLocation, String newLocation, List<AuthorizationValue> auths) {
91-
return fromSpecifications(readLocation(oldLocation, auths), readLocation(newLocation, auths));
124+
return fromLocations(oldLocation, newLocation, auths, OpenApiDiffOptions.builder().build());
125+
}
126+
127+
/**
128+
* compare two openapi doc
129+
*
130+
* @param oldLocation old api-doc location (local or http)
131+
* @param newLocation new api-doc location (local or http)
132+
* @param auths
133+
* @param options
134+
* @return Comparison result
135+
*/
136+
public static ChangedOpenApi fromLocations(
137+
String oldLocation,
138+
String newLocation,
139+
List<AuthorizationValue> auths,
140+
OpenApiDiffOptions options) {
141+
return fromSpecifications(
142+
readLocation(oldLocation, auths), readLocation(newLocation, auths), options);
92143
}
93144

94145
/**
@@ -99,7 +150,20 @@ public static ChangedOpenApi fromLocations(
99150
* @return Comparison result
100151
*/
101152
public static ChangedOpenApi fromSpecifications(OpenAPI oldSpec, OpenAPI newSpec) {
102-
return OpenApiDiff.compare(notNull(oldSpec, "old"), notNull(newSpec, "new"));
153+
return fromSpecifications(oldSpec, newSpec, OpenApiDiffOptions.builder().build());
154+
}
155+
156+
/**
157+
* compare two openapi doc
158+
*
159+
* @param oldSpec old api-doc specification
160+
* @param newSpec new api-doc specification
161+
* @param options
162+
* @return Comparison result
163+
*/
164+
public static ChangedOpenApi fromSpecifications(
165+
OpenAPI oldSpec, OpenAPI newSpec, OpenApiDiffOptions options) {
166+
return OpenApiDiff.compare(notNull(oldSpec, "old"), notNull(newSpec, "new"), options);
103167
}
104168

105169
private static OpenAPI notNull(OpenAPI spec, String type) {

Diff for: core/src/main/java/org/openapitools/openapidiff/core/compare/OpenApiDiff.java

+14-3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class OpenApiDiff {
4141
private MetadataDiff metadataDiff;
4242
private final OpenAPI oldSpecOpenApi;
4343
private final OpenAPI newSpecOpenApi;
44+
private final OpenApiDiffOptions options;
4445
private List<Endpoint> newEndpoints;
4546
private List<Endpoint> missingEndpoints;
4647
private List<ChangedOperation> changedOperations;
@@ -50,18 +51,24 @@ public class OpenApiDiff {
5051
/*
5152
* @param oldSpecOpenApi
5253
* @param newSpecOpenApi
54+
* @param diffOptions
5355
*/
54-
private OpenApiDiff(OpenAPI oldSpecOpenApi, OpenAPI newSpecOpenApi) {
56+
private OpenApiDiff(OpenAPI oldSpecOpenApi, OpenAPI newSpecOpenApi, OpenApiDiffOptions options) {
5557
this.oldSpecOpenApi = oldSpecOpenApi;
5658
this.newSpecOpenApi = newSpecOpenApi;
59+
this.options = options;
5760
if (null == oldSpecOpenApi || null == newSpecOpenApi) {
5861
throw new RuntimeException("one of the old or new object is null");
5962
}
63+
if (null == options) {
64+
throw new IllegalArgumentException("options parameter is null but is required");
65+
}
6066
initializeFields();
6167
}
6268

63-
public static ChangedOpenApi compare(OpenAPI oldSpec, OpenAPI newSpec) {
64-
return new OpenApiDiff(oldSpec, newSpec).compare();
69+
public static ChangedOpenApi compare(
70+
OpenAPI oldSpec, OpenAPI newSpec, OpenApiDiffOptions diffOptions) {
71+
return new OpenApiDiff(oldSpec, newSpec, diffOptions).compare();
6572
}
6673

6774
private void initializeFields() {
@@ -87,6 +94,10 @@ private void initializeFields() {
8794
this.deferredSchemaCache = new DeferredSchemaCache(this);
8895
}
8996

97+
public OpenApiDiffOptions getOptions() {
98+
return options;
99+
}
100+
90101
private ChangedOpenApi compare() {
91102
preProcess(oldSpecOpenApi);
92103
preProcess(newSpecOpenApi);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.openapitools.openapidiff.core.compare;
2+
3+
public class OpenApiDiffOptions {
4+
// Whether to fail backward compatibility check when enum values are added to responses
5+
private final boolean allowResponseEnumAdditions;
6+
7+
private OpenApiDiffOptions(boolean allowResponseEnumAdditions) {
8+
this.allowResponseEnumAdditions = allowResponseEnumAdditions;
9+
}
10+
11+
public boolean isAllowResponseEnumAdditions() {
12+
return allowResponseEnumAdditions;
13+
}
14+
15+
public static Builder builder() {
16+
return new Builder();
17+
}
18+
19+
public static class Builder {
20+
private OpenApiDiffOptions built = new OpenApiDiffOptions(false);
21+
22+
public Builder allowResponseEnumAdditions(boolean allowResponseEnumAdditions) {
23+
built = new OpenApiDiffOptions(allowResponseEnumAdditions);
24+
return this;
25+
}
26+
27+
public OpenApiDiffOptions build() {
28+
return built;
29+
}
30+
}
31+
}

Diff for: core/src/main/java/org/openapitools/openapidiff/core/compare/PathsDiff.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public DeferredChanged<ChangedPaths> diff(
8282
params.put(oldParams.get(i), newParams.get(i));
8383
}
8484
}
85-
DiffContext context = new DiffContext();
85+
DiffContext context = new DiffContext(openApiDiff.getOptions());
8686
context.setUrl(url);
8787
context.setParameters(params);
8888
context.setLeftAndRightUrls(url, rightUrl);

Diff for: core/src/main/java/org/openapitools/openapidiff/core/model/DiffContext.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
import java.util.Map;
66
import org.apache.commons.lang3.builder.EqualsBuilder;
77
import org.apache.commons.lang3.builder.HashCodeBuilder;
8+
import org.openapitools.openapidiff.core.compare.OpenApiDiffOptions;
89

910
public class DiffContext {
1011

12+
private final OpenApiDiffOptions options;
1113
private String url;
1214
private Map<String, String> parameters;
1315
private PathItem.HttpMethod method;
@@ -17,7 +19,8 @@ public class DiffContext {
1719
private String leftUrl;
1820
private String rightUrl;
1921

20-
public DiffContext() {
22+
public DiffContext(OpenApiDiffOptions options) {
23+
this.options = options;
2124
parameters = new HashMap<>();
2225
response = false;
2326
request = true;
@@ -43,6 +46,10 @@ public DiffContext copyWithLeftRightUrls(String leftUrl, String rightUrl) {
4346
return copy().setLeftAndRightUrls(leftUrl, rightUrl);
4447
}
4548

49+
public OpenApiDiffOptions getOptions() {
50+
return options;
51+
}
52+
4653
private DiffContext setRequest() {
4754
this.request = true;
4855
this.response = false;
@@ -82,7 +89,7 @@ private DiffContext setMethod(PathItem.HttpMethod method) {
8289
}
8390

8491
private DiffContext copy() {
85-
DiffContext context = new DiffContext();
92+
DiffContext context = new DiffContext(options);
8693
context.url = this.url;
8794
context.parameters = this.parameters;
8895
context.method = this.method;

Diff for: core/src/main/java/org/openapitools/openapidiff/core/model/schema/ChangedEnum.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ public ChangedEnum(List<T> oldValue, List<T> newValue, DiffContext context) {
1313

1414
@Override
1515
public DiffResult isItemsChanged() {
16-
if (context.isRequest() && getMissing().isEmpty()
17-
|| context.isResponse() && getIncreased().isEmpty()) {
16+
if (context.isRequest() && getMissing().isEmpty()) {
17+
return DiffResult.COMPATIBLE;
18+
}
19+
if (context.isResponse()
20+
&& (context.getOptions().isAllowResponseEnumAdditions() || getIncreased().isEmpty())) {
1821
return DiffResult.COMPATIBLE;
1922
}
2023
return DiffResult.INCOMPATIBLE;

Diff for: core/src/test/java/org/openapitools/openapidiff/core/BackwardCompatibilityTest.java

+32
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44
import static org.openapitools.openapidiff.core.TestUtils.assertOpenApiBackwardIncompatible;
55

66
import org.junit.jupiter.api.Test;
7+
import org.openapitools.openapidiff.core.compare.OpenApiDiffOptions;
78

89
public class BackwardCompatibilityTest {
910
private final String OPENAPI_DOC1 = "backwardCompatibility/bc_1.yaml";
1011
private final String OPENAPI_DOC2 = "backwardCompatibility/bc_2.yaml";
1112
private final String OPENAPI_DOC3 = "backwardCompatibility/bc_3.yaml";
1213
private final String OPENAPI_DOC4 = "backwardCompatibility/bc_4.yaml";
1314
private final String OPENAPI_DOC5 = "backwardCompatibility/bc_5.yaml";
15+
private final String OPENAPI_DOC_ENUM_BASE = "backwardCompatibility/bc_enum_base.yaml";
16+
private final String OPENAPI_DOC_ENUM_REQ_ADDED = "backwardCompatibility/bc_enum_req_added.yaml";
17+
private final String OPENAPI_DOC_ENUM_RESP_ADDED =
18+
"backwardCompatibility/bc_enum_resp_added.yaml";
1419

1520
@Test
1621
public void testNoChange() {
@@ -46,4 +51,31 @@ public void testApiOperationChanged() {
4651
public void testApiReadWriteOnlyPropertiesChanged() {
4752
assertOpenApiBackwardCompatible(OPENAPI_DOC1, OPENAPI_DOC5, true);
4853
}
54+
55+
@Test
56+
public void testEnumRequestValuesAdded() {
57+
assertOpenApiBackwardCompatible(OPENAPI_DOC_ENUM_BASE, OPENAPI_DOC_ENUM_REQ_ADDED, true);
58+
}
59+
60+
@Test
61+
public void testEnumRequestValuesRemoved() {
62+
assertOpenApiBackwardIncompatible(OPENAPI_DOC_ENUM_REQ_ADDED, OPENAPI_DOC_ENUM_BASE);
63+
}
64+
65+
@Test
66+
public void testEnumResponseValuesAdded_lenient() {
67+
OpenApiDiffOptions options =
68+
OpenApiDiffOptions.builder().allowResponseEnumAdditions(true).build();
69+
assertOpenApiBackwardCompatible(OPENAPI_DOC_ENUM_BASE, OPENAPI_DOC_ENUM_RESP_ADDED, options);
70+
}
71+
72+
@Test
73+
public void testEnumResponseValuesAdded_strict() {
74+
assertOpenApiBackwardIncompatible(OPENAPI_DOC_ENUM_BASE, OPENAPI_DOC_ENUM_RESP_ADDED);
75+
}
76+
77+
@Test
78+
public void testEnumResponseValuesRemoved() {
79+
assertOpenApiBackwardCompatible(OPENAPI_DOC_ENUM_RESP_ADDED, OPENAPI_DOC_ENUM_BASE, true);
80+
}
4981
}

Diff for: core/src/test/java/org/openapitools/openapidiff/core/TestUtils.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.slf4j.LoggerFactory.getLogger;
55

6+
import org.openapitools.openapidiff.core.compare.OpenApiDiffOptions;
67
import org.openapitools.openapidiff.core.model.ChangedOpenApi;
78
import org.slf4j.Logger;
89

@@ -27,7 +28,12 @@ public static void assertOpenApiChangedEndpoints(String oldSpec, String newSpec)
2728

2829
public static void assertOpenApiBackwardCompatible(
2930
String oldSpec, String newSpec, boolean isDiff) {
30-
ChangedOpenApi changedOpenApi = OpenApiCompare.fromLocations(oldSpec, newSpec);
31+
assertOpenApiBackwardCompatible(oldSpec, newSpec, OpenApiDiffOptions.builder().build());
32+
}
33+
34+
public static void assertOpenApiBackwardCompatible(
35+
String oldSpec, String newSpec, OpenApiDiffOptions options) {
36+
ChangedOpenApi changedOpenApi = OpenApiCompare.fromLocations(oldSpec, newSpec, null, options);
3137
LOG.info("Result: {}", changedOpenApi.isChanged().getValue());
3238
assertThat(changedOpenApi.isCompatible()).isTrue();
3339
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
openapi: 3.0.0
2+
info:
3+
description: test backward compatibility for enums
4+
version: 1.0.0
5+
title: test backward compatibility for enums
6+
paths:
7+
/test-back-compat-enums:
8+
get:
9+
operationId: search
10+
parameters:
11+
- name: param-inline-enum
12+
in: query
13+
required: true
14+
schema:
15+
type: string
16+
enum:
17+
- param-inline-enum-val-1
18+
- param-inline-enum-val-2
19+
default: param-inline-enum-val-1
20+
responses:
21+
'200':
22+
description: successful operation
23+
content:
24+
application/json:
25+
schema:
26+
$ref: '#/components/schemas/SchemaWithEnum'
27+
components:
28+
schemas:
29+
SchemaWithEnum:
30+
type: object
31+
properties:
32+
enum-prop:
33+
type: string
34+
enum:
35+
- enum-prop-val-1
36+
- enum-prop-val-2
37+
default: enum-prop-val-1

0 commit comments

Comments
 (0)