Skip to content

Commit 08afebc

Browse files
authored
Allow excluding certain violations from being reported (#4)
1 parent 2b0a876 commit 08afebc

File tree

11 files changed

+124
-100
lines changed

11 files changed

+124
-100
lines changed

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,23 @@ public class ValidatorConfiguration {
201201
}
202202
```
203203

204+
### Exclude certain violations
205+
Certain violations can get excluded from reporting. This can be achieved as demonstrated in the following snippet.
206+
207+
**Note:** Only use this where it is really needed. It is best practice to fix the actual violations by either adjusting
208+
the specification, the server implementation, or the client sending wrong requests.
209+
210+
```java
211+
@Component
212+
public class ViolationExclusionsExample implements ViolationExclusions {
213+
@Override
214+
public boolean isExcluded(OpenApiViolation violation) {
215+
return violation.getDirection().equals(Direction.REQUEST)
216+
&& violation.getMessage().equals("[Path '/name'] Instance type (integer) does not match any allowed primitive type (allowed: [\"string\"])");
217+
}
218+
}
219+
```
220+
204221
## Examples
205222
Run examples with `./gradlew :examples:example-spring-boot-starter-web:bootRun` or `./gradlew :examples:example-spring-boot-starter-webflux:bootRun`.
206223

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.getyourguide.openapi.validation.api.exclusions;
2+
3+
import com.getyourguide.openapi.validation.api.model.OpenApiViolation;
4+
5+
public class NoViolationExclusions implements ViolationExclusions {
6+
@Override
7+
public boolean isExcluded(OpenApiViolation violation) {
8+
return false;
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.getyourguide.openapi.validation.api.exclusions;
2+
3+
import com.getyourguide.openapi.validation.api.model.OpenApiViolation;
4+
5+
public interface ViolationExclusions {
6+
boolean isExcluded(OpenApiViolation violation);
7+
}

openapi-validation-api/src/main/java/com/getyourguide/openapi/validation/api/model/OpenApiViolation.java

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public class OpenApiViolation {
1616
private final Optional<String> operationId;
1717
private final Optional<String> normalizedPath;
1818
private final Optional<String> instance;
19+
private final Optional<String> schema;
1920
private final Optional<Integer> responseStatus;
2021
private final String message;
22+
private final String logMessage;
2123
}

openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/DefaultViolationLogger.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ public class DefaultViolationLogger implements ViolationLogger {
1919
public void log(OpenApiViolation violation) {
2020
try (var ignored = loggerExtension.addToLoggingContext(buildLoggingContext(violation))) {
2121
switch (violation.getLevel()) {
22-
case INFO -> log.info(violation.getMessage());
23-
case WARN -> log.warn(violation.getMessage());
24-
case ERROR -> log.error(violation.getMessage());
22+
case INFO -> log.info(violation.getLogMessage());
23+
case WARN -> log.warn(violation.getLogMessage());
24+
case ERROR -> log.error(violation.getLogMessage());
2525
// keeping ignored as debug for now
26-
case IGNORE -> log.debug(violation.getMessage());
26+
case IGNORE -> log.debug(violation.getLogMessage());
2727
default -> { /* do nothing */ }
2828
}
2929
} catch (IOException e) {

openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/ValidationReportHandler.java

+21-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.getyourguide.openapi.validation.core;
22

33
import com.atlassian.oai.validator.report.ValidationReport;
4+
import com.getyourguide.openapi.validation.api.exclusions.ViolationExclusions;
45
import com.getyourguide.openapi.validation.api.log.LogLevel;
56
import com.getyourguide.openapi.validation.api.log.ViolationLogger;
67
import com.getyourguide.openapi.validation.api.metrics.MetricTag;
@@ -15,14 +16,13 @@
1516
import lombok.AllArgsConstructor;
1617
import lombok.Builder;
1718
import lombok.Getter;
18-
import lombok.extern.slf4j.Slf4j;
1919

20-
@Slf4j
2120
@AllArgsConstructor
2221
public class ValidationReportHandler {
2322
private final ValidationReportThrottler throttleHelper;
2423
private final ViolationLogger logger;
2524
private final MetricsReporter metrics;
25+
private final ViolationExclusions violationExclusions;
2626
private final Configuration configuration;
2727

2828
public void handleValidationReport(
@@ -34,19 +34,14 @@ public void handleValidationReport(
3434
if (!result.getMessages().isEmpty()) {
3535
result
3636
.getMessages()
37-
.stream().filter(message -> !isExcludedMessage(message))
38-
.forEach(message -> throttleHelper.throttle(message, direction,
39-
() -> logValidationError(message, request, body, direction)));
37+
.stream()
38+
.map(message -> buildOpenApiViolation(message, request, body, direction))
39+
.filter(violation -> !isViolationExcluded(violation))
40+
.forEach(violation -> throttleHelper.throttle(violation, () -> logValidationError(violation)));
4041
}
4142
}
4243

43-
private void logValidationError(
44-
ValidationReport.Message message,
45-
RequestMetaData request,
46-
String body,
47-
Direction direction
48-
) {
49-
var openApiViolation = buildOpenApiViolation(message, request, body, direction);
44+
private void logValidationError(OpenApiViolation openApiViolation) {
5045
logger.log(openApiViolation);
5146
metrics.increment(configuration.getMetricName(), createTags(openApiViolation));
5247
}
@@ -78,17 +73,20 @@ private OpenApiViolation buildOpenApiViolation(
7873
.operationId(getOperationId(message))
7974
.normalizedPath(getNormalizedPath(message))
8075
.instance(getPointersInstance(message))
76+
.schema(getPointersSchema(message))
8177
.responseStatus(getResponseStatus(message))
82-
.message(logMessage)
78+
.logMessage(logMessage)
79+
.message(message.getMessage())
8380
.build();
8481
}
8582

86-
private boolean isExcludedMessage(ValidationReport.Message message) {
83+
private boolean isViolationExcluded(OpenApiViolation openApiViolation) {
8784
return
88-
// JSON parse errors should be ignored as it can't be compared to the schema then (this also prevents logging personal data!)
89-
message.getMessage().startsWith("Unable to parse JSON")
85+
violationExclusions.isExcluded(openApiViolation)
86+
// JSON parse errors should be ignored as it can't be compared to the schema then (this also prevents logging personal data!)
87+
|| openApiViolation.getMessage().startsWith("Unable to parse JSON")
9088
// If it matches more than 1, then we don't want to log a validation error
91-
|| message.getMessage().matches(
89+
|| openApiViolation.getMessage().matches(
9290
".*\\[Path '[^']+'] Instance failed to match exactly one schema \\(matched [1-9][0-9]* out of \\d\\).*");
9391
}
9492

@@ -114,6 +112,12 @@ private static Optional<String> getPointersInstance(ValidationReport.Message mes
114112
.map(ValidationReport.MessageContext.Pointers::getInstance);
115113
}
116114

115+
private static Optional<String> getPointersSchema(ValidationReport.Message message) {
116+
return message.getContext()
117+
.flatMap(ValidationReport.MessageContext::getPointers)
118+
.map(ValidationReport.MessageContext.Pointers::getSchema);
119+
}
120+
117121
private static Optional<String> getOperationId(ValidationReport.Message message) {
118122
return message.getContext()
119123
.flatMap(ValidationReport.MessageContext::getApiOperation)
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.getyourguide.openapi.validation.core.throttle;
22

3-
import com.atlassian.oai.validator.report.ValidationReport;
4-
import com.getyourguide.openapi.validation.api.model.Direction;
3+
import com.getyourguide.openapi.validation.api.model.OpenApiViolation;
54
import java.util.Map;
65
import java.util.concurrent.ConcurrentHashMap;
76
import lombok.AllArgsConstructor;
@@ -16,21 +15,21 @@ public class RequestBasedValidationReportThrottler implements ValidationReportTh
1615
private final Map<String, DateTime> loggedMessages = new ConcurrentHashMap<>();
1716

1817
@Override
19-
public void throttle(ValidationReport.Message message, Direction direction, Runnable runnable) {
20-
if (isThrottled(message, direction)) {
18+
public void throttle(OpenApiViolation openApiViolation, Runnable runnable) {
19+
if (isThrottled(openApiViolation)) {
2120
return;
2221
}
2322

2423
runnable.run();
25-
registerLoggedMessage(message, direction);
24+
registerLoggedMessage(openApiViolation);
2625
}
2726

28-
private void registerLoggedMessage(ValidationReport.Message message, Direction direction) {
29-
loggedMessages.put(buildKey(message, direction), DateTime.now());
27+
private void registerLoggedMessage(OpenApiViolation openApiViolation) {
28+
loggedMessages.put(buildKey(openApiViolation), DateTime.now());
3029
}
3130

32-
private boolean isThrottled(ValidationReport.Message message, Direction direction) {
33-
var key = buildKey(message, direction);
31+
private boolean isThrottled(OpenApiViolation openApiViolation) {
32+
var key = buildKey(openApiViolation);
3433
var lastLoggedTime = loggedMessages.get(key);
3534
if (lastLoggedTime == null) {
3635
return false;
@@ -39,18 +38,13 @@ private boolean isThrottled(ValidationReport.Message message, Direction directio
3938
}
4039

4140
@NonNull
42-
private String buildKey(ValidationReport.Message message, Direction direction) {
43-
var keyBuilder = new StringBuilder(direction.toString() + ":");
44-
message.getContext().ifPresentOrElse(
45-
messageContext -> {
46-
var method = messageContext.getRequestMethod().map(Enum::name).orElse("N/A");
47-
var apiOperationPathNormalized = messageContext.getApiOperation().map(apiOperation -> apiOperation.getApiPath().normalised()).orElse("N/A");
48-
var responseStatus = messageContext.getResponseStatus().map(Object::toString).orElse("N/A");
49-
var schema = messageContext.getPointers().map(ValidationReport.MessageContext.Pointers::getSchema).orElse("N/A");
50-
keyBuilder.append(String.format("%s:%s:%s:%s", method, apiOperationPathNormalized, responseStatus, schema));
51-
},
52-
() -> keyBuilder.append("N/A")
53-
);
41+
private String buildKey(OpenApiViolation openApiViolation) {
42+
var keyBuilder = new StringBuilder(openApiViolation.getDirection().toString() + ":");
43+
var method = openApiViolation.getRequestMetaData().getMethod();
44+
var apiOperationPathNormalized = openApiViolation.getNormalizedPath().orElse("N/A");
45+
var responseStatus = openApiViolation.getResponseStatus().map(Object::toString).orElse("N/A");
46+
var schema = openApiViolation.getSchema().orElse("N/A");
47+
keyBuilder.append(String.format("%s:%s:%s:%s", method, apiOperationPathNormalized, responseStatus, schema));
5448
return keyBuilder.toString();
5549
}
5650
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package com.getyourguide.openapi.validation.core.throttle;
22

3-
import com.atlassian.oai.validator.report.ValidationReport;
4-
import com.getyourguide.openapi.validation.api.model.Direction;
3+
import com.getyourguide.openapi.validation.api.model.OpenApiViolation;
54

65
public interface ValidationReportThrottler {
76

8-
void throttle(ValidationReport.Message message, Direction direction, Runnable runnable);
7+
void throttle(OpenApiViolation openApiViolation, Runnable runnable);
98
}
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package com.getyourguide.openapi.validation.core.throttle;
22

3-
import com.atlassian.oai.validator.report.ValidationReport;
4-
import com.getyourguide.openapi.validation.api.model.Direction;
3+
import com.getyourguide.openapi.validation.api.model.OpenApiViolation;
54

65
public class ValidationReportThrottlerNone implements ValidationReportThrottler {
76
@Override
8-
public void throttle(ValidationReport.Message message, Direction direction, Runnable runnable) {
7+
public void throttle(OpenApiViolation openApiViolation, Runnable runnable) {
98
runnable.run();
109
}
1110
}
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package com.getyourguide.openapi.validation.core.throttle;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4-
import static org.mockito.Mockito.mock;
5-
import static org.mockito.Mockito.when;
64

7-
import com.atlassian.oai.validator.model.ApiOperation;
8-
import com.atlassian.oai.validator.model.ApiPath;
95
import com.atlassian.oai.validator.model.Request;
10-
import com.atlassian.oai.validator.report.ValidationReport;
116
import com.getyourguide.openapi.validation.api.model.Direction;
7+
import com.getyourguide.openapi.validation.api.model.OpenApiViolation;
8+
import com.getyourguide.openapi.validation.api.model.RequestMetaData;
9+
import java.net.URI;
10+
import java.util.Collections;
1211
import java.util.Optional;
1312
import org.junit.jupiter.api.BeforeEach;
1413
import org.junit.jupiter.api.Test;
@@ -27,85 +26,73 @@ public void beforeEach() {
2726

2827
@Test
2928
public void testNotThrottledIfNoEntry() {
30-
var message = mockMessage(Request.Method.GET, "/path", 200);
29+
var violation = buildViolation(DIRECTION, Request.Method.GET, "/path", 200);
3130

32-
assertThrottled(false, message, DIRECTION);
31+
assertThrottled(false, violation);
3332
}
3433

3534
@Test
3635
public void testThrottledIfEntryExists() {
37-
var message = mockMessage(Request.Method.GET, "/path", 200);
38-
throttler.throttle(message, DIRECTION, NO_OP_RUNNABLE);
36+
var violation = buildViolation(DIRECTION, Request.Method.GET, "/path", 200);
37+
throttler.throttle(violation, NO_OP_RUNNABLE);
3938

40-
assertThrottled(true, message, DIRECTION);
39+
assertThrottled(true, violation);
4140
}
4241

4342
@Test
4443
public void testNotThrottledIfSmallDifference() {
45-
var message = mockMessage(Request.Method.GET, "/path", 200);
46-
throttler.throttle(message, DIRECTION, NO_OP_RUNNABLE);
44+
var violation = buildViolation(DIRECTION, Request.Method.GET, "/path", 200);
45+
throttler.throttle(violation, NO_OP_RUNNABLE);
4746

48-
assertThrottled(false, mockMessage(Request.Method.GET, "/path", 200), Direction.RESPONSE);
49-
assertThrottled(false, mockMessage(Request.Method.POST, "/path", 200), DIRECTION);
50-
assertThrottled(false, mockMessage(Request.Method.GET, "/other-path", 200), DIRECTION);
51-
assertThrottled(false, mockMessage(Request.Method.GET, "/path", 402), DIRECTION);
47+
assertThrottled(false, buildViolation(Direction.RESPONSE, Request.Method.GET, "/path", 200));
48+
assertThrottled(false, buildViolation(DIRECTION, Request.Method.POST, "/path", 200));
49+
assertThrottled(false, buildViolation(DIRECTION, Request.Method.GET, "/other-path", 200));
50+
assertThrottled(false, buildViolation(DIRECTION, Request.Method.GET, "/path", 402));
5251
}
5352

5453
@Test
5554
public void testThrottledIfInstanceContainsArrayIndex() {
56-
var message = mockMessage(Request.Method.GET, "/path", 200, "/items/1/name", "/properties/items/items/properties/name");
57-
throttler.throttle(message, DIRECTION, NO_OP_RUNNABLE);
55+
var violation = buildViolation(DIRECTION, Request.Method.GET, "/path", 200, "/items/1/name", "/properties/items/items/properties/name");
56+
throttler.throttle(violation, NO_OP_RUNNABLE);
5857

5958
assertThrottled(
6059
true,
61-
mockMessage(Request.Method.GET, "/path", 200, "/items/2/name", "/properties/items/items/properties/name"),
62-
DIRECTION
60+
buildViolation(DIRECTION, Request.Method.GET, "/path", 200, "/items/2/name", "/properties/items/items/properties/name")
6361
);
6462
assertThrottled(
6563
true,
66-
mockMessage(Request.Method.GET, "/path", 200, "/items/3/name", "/properties/items/items/properties/name"),
67-
DIRECTION
64+
buildViolation(DIRECTION, Request.Method.GET, "/path", 200, "/items/3/name", "/properties/items/items/properties/name")
6865
);
6966
assertThrottled(
7067
false,
71-
mockMessage(Request.Method.GET, "/path", 200, "/items/4/description", "/properties/items/items/properties/description"),
72-
DIRECTION
68+
buildViolation(DIRECTION, Request.Method.GET, "/path", 200, "/items/4/description", "/properties/items/items/properties/description")
7369
);
7470
}
7571

76-
private void assertThrottled(boolean expectThrottled, ValidationReport.Message message, Direction direction) {
72+
private void assertThrottled(boolean expectThrottled, OpenApiViolation openApiViolation) {
7773
var ref = new Object() {
7874
boolean wasThrottled = true;
7975
};
8076

81-
throttler.throttle(message, direction, () -> ref.wasThrottled = false);
77+
throttler.throttle(openApiViolation, () -> ref.wasThrottled = false);
8278

8379
assertEquals(expectThrottled, ref.wasThrottled);
8480
}
8581

86-
private ValidationReport.Message mockMessage(Request.Method method, String path, int status) {
87-
return mockMessage(method, path, status, "/items/1/name", "/properties/items/items/properties/name");
82+
private OpenApiViolation buildViolation(Direction direction, Request.Method method, String path, int status) {
83+
return buildViolation(direction, method, path, status, "/items/1/name", "/properties/items/items/properties/name");
8884
}
8985

90-
private ValidationReport.Message mockMessage(Request.Method method, String path, int status, String instance, String schema) {
91-
var message = mock(ValidationReport.Message.class);
92-
var context = mock(ValidationReport.MessageContext.class);
93-
94-
when(context.getRequestMethod()).thenReturn(Optional.of(method));
95-
96-
var apiOperation = mock(ApiOperation.class);
97-
var apiPath = mock(ApiPath.class);
98-
when(apiPath.normalised()).thenReturn(path);
99-
when(apiOperation.getApiPath()).thenReturn(apiPath);
100-
when(context.getApiOperation()).thenReturn(Optional.of(apiOperation));
101-
when(context.getResponseStatus()).thenReturn(Optional.of(status));
102-
103-
var pointers = mock(ValidationReport.MessageContext.Pointers.class);
104-
when(pointers.getInstance()).thenReturn(instance);
105-
when(pointers.getSchema()).thenReturn(schema);
106-
when(context.getPointers()).thenReturn(Optional.of(pointers));
107-
108-
when(message.getContext()).thenReturn(Optional.of(context));
109-
return message;
86+
private OpenApiViolation buildViolation(Direction direction, Request.Method method, String path, int status, String instance, String schema) {
87+
return OpenApiViolation.builder()
88+
.direction(direction)
89+
.requestMetaData(
90+
new RequestMetaData(method.toString(), URI.create("https://example.com" + path), Collections.emptyMap())
91+
)
92+
.responseStatus(Optional.of(status))
93+
.normalizedPath(Optional.of(path))
94+
.instance(Optional.of(instance))
95+
.schema(Optional.of(schema))
96+
.build();
11097
}
11198
}

0 commit comments

Comments
 (0)