Skip to content

Commit 5105f0b

Browse files
Parameter data detection fix when PathParams changed (#353)
Fixes #353
1 parent 17c8142 commit 5105f0b

10 files changed

+342
-11
lines changed

core/src/main/java/org/openapitools/openapidiff/core/compare/OperationDiff.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,15 @@ public DeferredChanged<ChangedOperation> diff(
7272
.ifPresent(changedOperation::setRequestBody);
7373
}
7474

75+
ParametersDiffResult parametersDiffResult =
76+
openApiDiff.getParametersDiff().diff(oldOperation.getParameters(), newOperation.getParameters(), context);
7577
builder
76-
.with(
77-
openApiDiff
78-
.getParametersDiff()
79-
.diff(oldOperation.getParameters(), newOperation.getParameters(), context))
78+
.with(parametersDiffResult.deferredChanged)
8079
.ifPresent(
8180
params -> {
82-
removePathParameters(context.getParameters(), params);
81+
if (!parametersDiffResult.sameOperationsDiffSchema) {
82+
removePathParameters(context.getParameters(), params);
83+
}
8384
changedOperation.setParameters(params);
8485
});
8586

core/src/main/java/org/openapitools/openapidiff/core/compare/ParametersDiff.java

+65-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import java.util.List;
77
import java.util.Objects;
88
import java.util.Optional;
9+
import java.util.regex.Matcher;
10+
import java.util.regex.Pattern;
911
import org.openapitools.openapidiff.core.model.Changed;
1012
import org.openapitools.openapidiff.core.model.ChangedParameters;
1113
import org.openapitools.openapidiff.core.model.DiffContext;
@@ -14,6 +16,15 @@
1416
import org.openapitools.openapidiff.core.utils.RefPointer;
1517
import org.openapitools.openapidiff.core.utils.RefType;
1618

19+
class ParametersDiffResult {
20+
protected DeferredChanged<ChangedParameters> deferredChanged;
21+
protected boolean sameOperationsDiffSchema;
22+
23+
public ParametersDiffResult(DeferredChanged<ChangedParameters> deferredChanged, boolean sameOperationsDiffSchema) {
24+
this.deferredChanged = deferredChanged;
25+
this.sameOperationsDiffSchema = sameOperationsDiffSchema;
26+
}
27+
}
1728
/** compare two parameter */
1829
public class ParametersDiff {
1930
private static final RefPointer<Parameter> refPointer = new RefPointer<>(RefType.PARAMETERS);
@@ -46,9 +57,7 @@ public static boolean same(Parameter left, Parameter right) {
4657
&& Objects.equals(left.getIn(), right.getIn());
4758
}
4859

49-
public DeferredChanged<ChangedParameters> diff(
50-
List<Parameter> left, List<Parameter> right, DiffContext context) {
51-
60+
public ParametersDiffResult diff(List<Parameter> left, List<Parameter> right, DiffContext context) {
5261
DeferredBuilder<Changed> builder = new DeferredBuilder<>();
5362
ChangedParameters changedParameters =
5463
new ChangedParameters(left, right != null ? new ArrayList<>(right) : null, context);
@@ -70,7 +79,59 @@ public DeferredChanged<ChangedParameters> diff(
7079
}
7180
}
7281
changedParameters.getIncreased().addAll(right);
82+
return new ParametersDiffResult(
83+
builder.buildIsChanged(changedParameters),
84+
pathUnchangedParametersChanged(changedParameters, context)
85+
);
86+
}
87+
88+
public boolean pathUnchangedParametersChanged(
89+
ChangedParameters changedParameters, DiffContext context) {
90+
if (!pathUnchanged(changedParameters, context)) return false;
91+
// If missing and increased parameter count is different, it's a new endpoint
92+
if (changedParameters.getMissing().size() != changedParameters.getIncreased().size())
93+
return false;
94+
// Go through each missing Parameter and compare it to newly added Parameters
95+
for (Parameter parameter : changedParameters.getMissing()) {
96+
// Speedy Check. Use the map already created in changedParameters to check if missing param is linked to newParam
97+
String newParameterName = context.getParameters().get(parameter.getName());
98+
if (newParameterName.isEmpty()) return false;
99+
100+
Optional<Parameter> newParameter =
101+
changedParameters.getIncreased().stream()
102+
.filter(p -> p.getName().equals(newParameterName))
103+
.findFirst();
104+
if (!newParameter.isPresent()) return false;
73105

74-
return builder.buildIsChanged(changedParameters);
106+
// Check if the old and new Parameters match . IF so, return TRUE
107+
Parameter newParameterRealized = newParameter.get();
108+
newParameterRealized.setName(parameter.getName()); // Make names similar
109+
boolean samePathDifferentParameter = !newParameterRealized.equals(parameter);
110+
newParameterRealized.setName(newParameterName); // Important:: MUST Reset the name as this is not a copy
111+
return samePathDifferentParameter;
112+
}
113+
return false;
114+
}
115+
116+
public boolean pathUnchanged(ChangedParameters changedParameters, DiffContext context) {
117+
final String REGEX_PATH = "\\{([^/]+)}";
118+
String oldUrl = context.getLeftUrl();
119+
String newUrl = context.getRightUrl();
120+
ArrayList<String> oldUrlPathParams = matchedItems(oldUrl, REGEX_PATH);
121+
ArrayList<String> newUrlPathParams = matchedItems(newUrl, REGEX_PATH);
122+
// Path Param count doesn't match or param-less path doesn't match or param is changed --> It's a new endpoint
123+
return oldUrlPathParams.size() == newUrlPathParams.size()
124+
&& changedParameters.getChanged().isEmpty()
125+
&& oldUrl.replaceAll(REGEX_PATH, "").equals(newUrl.replaceAll(REGEX_PATH, ""));
126+
}
127+
128+
public ArrayList<String> matchedItems(String string, String regex) {
129+
Matcher matcher = Pattern.compile(regex).matcher(string);
130+
ArrayList<String> matchedItems = new ArrayList<>();
131+
while (matcher.find()) {
132+
String item = matcher.group();
133+
matchedItems.add(item.substring(0, matcher.group().length() - 1).replaceFirst("\\{", ""));
134+
}
135+
return matchedItems;
75136
}
76137
}

core/src/main/java/org/openapitools/openapidiff/core/compare/PathDiff.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@ public DeferredChanged<ChangedPath> diff(PathItem left, PathItem right, DiffCont
3636
.with(
3737
openApiDiff
3838
.getOperationDiff()
39-
.diff(oldOperation, newOperation, context.copyWithMethod(method)))
40-
.ifPresent(changedPath.getChanged()::add);
39+
.diff(oldOperation, newOperation,
40+
context.copyWithMethod(method).copyWithLeftRightUrls(
41+
context.getLeftUrl(),
42+
context.getRightUrl()
43+
)
44+
)
45+
).ifPresent(changedPath.getChanged()::add);
4146
}
4247
builder
4348
.with(

core/src/main/java/org/openapitools/openapidiff/core/compare/PathsDiff.java

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public DeferredChanged<ChangedPaths> diff(
8585
DiffContext context = new DiffContext();
8686
context.setUrl(url);
8787
context.setParameters(params);
88+
context.setLeftAndRightUrls(url, rightUrl);
8889
builder
8990
.with(openApiDiff.getPathDiff().diff(leftPath, rightPath, context))
9091
.ifPresent(path -> changedPaths.getChanged().put(rightUrl, path));

core/src/main/java/org/openapitools/openapidiff/core/model/DiffContext.java

+18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public class DiffContext {
1414
private boolean response;
1515
private boolean request;
1616
private Boolean required;
17+
private String leftUrl;
18+
private String rightUrl;
1719

1820
public DiffContext() {
1921
parameters = new HashMap<>();
@@ -37,6 +39,10 @@ public DiffContext copyAsResponse() {
3739
return copy().setResponse();
3840
}
3941

42+
public DiffContext copyWithLeftRightUrls(String leftUrl, String rightUrl) {
43+
return copy().setLeftAndRightUrls(leftUrl, rightUrl);
44+
}
45+
4046
private DiffContext setRequest() {
4147
this.request = true;
4248
this.response = false;
@@ -104,6 +110,16 @@ private DiffContext setRequired(boolean required) {
104110
return this;
105111
}
106112

113+
public DiffContext setLeftAndRightUrls(String leftUrl, String rightUrl) {
114+
this.leftUrl = leftUrl;
115+
this.rightUrl = rightUrl;
116+
return this;
117+
}
118+
119+
public String getLeftUrl() { return this.leftUrl; }
120+
121+
public String getRightUrl() { return this.rightUrl; }
122+
107123
@Override
108124
public boolean equals(Object o) {
109125
if (this == o) return true;
@@ -131,6 +147,8 @@ public int hashCode() {
131147
.append(response)
132148
.append(request)
133149
.append(required)
150+
.append(leftUrl)
151+
.append(rightUrl)
134152
.toHashCode();
135153
}
136154
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.openapitools.openapidiff.core;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.DisplayName;
5+
import org.junit.jupiter.api.Test;
6+
import org.openapitools.openapidiff.core.model.ChangedOpenApi;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
10+
public class PathParameterSchemaDiffTest {
11+
final String TEST_MSG_1 =
12+
"Testing: \n"
13+
+ "1. Same path but different pathParameters\n"
14+
+ "2. different parameters in the parameters: section\n"
15+
+ "3. Parameters have different schema\n"
16+
+ "eg:\n"
17+
+ "old path -- students/{id}\n"
18+
+ "old schema -- id: integer\n"
19+
+ "new path -- students/{username}\n"
20+
+ "new schema -- username: string";
21+
22+
final String TEST_MSG_2 =
23+
"Testing: \n"
24+
+ "1. Same path but different pathParameters\n"
25+
+ "2. different parameters in the parameters: section\n"
26+
+ "3. Parameters have same schema\n";
27+
28+
@Test
29+
@DisplayName(
30+
"Same Path, different PathParams, Params in the `Parameters`: match pathParam, Different Schema")
31+
public void pathSamePathParamsDiffParamSameAsInPathButSchemaDiff() {
32+
final Logger logger = LoggerFactory.getLogger(PathParameterSchemaDiffTest.class);
33+
logger.info(TEST_MSG_1);
34+
String OPENAPI_DOC1 = "path_parameter_diff_param_schema_diff_old.yaml";
35+
String OPENAPI_DOC2 = "path_parameter_diff_param_schema_diff_new.yaml";
36+
ChangedOpenApi diff = OpenApiCompare.fromLocations(OPENAPI_DOC1, OPENAPI_DOC2);
37+
Assertions.assertTrue(diff.isDifferent());
38+
Assertions.assertFalse(diff.isCompatible());
39+
}
40+
41+
@Test
42+
@DisplayName(
43+
"Same Path, different PathParams, Params in the `Parameters`: match pathParam, same Schema")
44+
public void pathSamePathParamsDiffParamNameDiffSchemaSame() {
45+
final Logger logger = LoggerFactory.getLogger(PathParameterSchemaDiffTest.class);
46+
logger.info(TEST_MSG_2);
47+
String OPENAPI_DOC1 = "path_parameter_diff_param_name_diff_old.yaml";
48+
String OPENAPI_DOC2 = "path_parameter_diff_param_name_diff_new.yaml";
49+
ChangedOpenApi diff = OpenApiCompare.fromLocations(OPENAPI_DOC1, OPENAPI_DOC2);
50+
Assertions.assertFalse(diff.isDifferent());
51+
Assertions.assertTrue(diff.isCompatible());
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Sample API
4+
description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
5+
version: 0.1.9
6+
servers:
7+
- url: http://api.example.com/v1
8+
description: Example api server
9+
10+
paths:
11+
12+
/student/{username}:
13+
get:
14+
parameters:
15+
- in: path
16+
name: username # Note the name is the same as in the path
17+
required: true
18+
schema:
19+
type: string
20+
minimum: 1
21+
22+
23+
summary: Returns data about a student
24+
responses:
25+
'200': # status code
26+
description: A JSON Object student data
27+
content:
28+
application/json:
29+
schema:
30+
type: object
31+
items:
32+
$ref: '#/components/schemas/Student'
33+
34+
components:
35+
schemas:
36+
Student:
37+
type: object
38+
properties:
39+
id:
40+
type: integer
41+
example: "1"
42+
username:
43+
type: string
44+
example: "alloy_horizon"
45+
pattern: A..Z a..z 0..9 . _ -
46+
name:
47+
type: string
48+
example: "Alloy Horizon"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Sample API
4+
description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
5+
version: 0.1.9
6+
servers:
7+
- url: http://api.example.com/v1
8+
description: Example api server
9+
10+
paths:
11+
12+
/student/{id}:
13+
get:
14+
parameters:
15+
- in: path
16+
name: id # Note the name is the same as in the path
17+
required: true
18+
schema:
19+
type: string
20+
minimum: 1
21+
22+
23+
summary: Returns data about a student
24+
responses:
25+
'200': # status code
26+
description: A JSON Object student data
27+
content:
28+
application/json:
29+
schema:
30+
type: object
31+
items:
32+
$ref: '#/components/schemas/Student'
33+
34+
components:
35+
schemas:
36+
Student:
37+
type: object
38+
properties:
39+
id:
40+
type: integer
41+
example: "1"
42+
username:
43+
type: string
44+
example: "alloy_horizon"
45+
pattern: A..Z a..z 0..9 . _ -
46+
name:
47+
type: string
48+
example: "Alloy Horizon"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Sample API
4+
description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
5+
version: 0.1.9
6+
servers:
7+
- url: http://api.example.com/v1
8+
description: Example api server
9+
10+
paths:
11+
12+
/student/{username}:
13+
get:
14+
parameters:
15+
- in: path
16+
name: username # Note the name is the same as in the path
17+
required: true
18+
schema:
19+
type: string
20+
minimum: 1
21+
22+
23+
summary: Returns data about a student
24+
responses:
25+
'200': # status code
26+
description: A JSON Object student data
27+
content:
28+
application/json:
29+
schema:
30+
type: object
31+
items:
32+
$ref: '#/components/schemas/Student'
33+
34+
components:
35+
schemas:
36+
Student:
37+
type: object
38+
properties:
39+
id:
40+
type: integer
41+
example: "1"
42+
username:
43+
type: string
44+
example: "alloy_horizon"
45+
pattern: A..Z a..z 0..9 . _ -
46+
name:
47+
type: string
48+
example: "Alloy Horizon"

0 commit comments

Comments
 (0)