Skip to content

Commit 2b0a876

Browse files
authored
Support multiple spec files (#2)
1 parent 739f901 commit 2b0a876

File tree

10 files changed

+399
-18
lines changed

10 files changed

+399
-18
lines changed

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,44 @@ public class SampleRateTrafficSelector implements TrafficSelector {
163163
}
164164
```
165165

166+
### Custom log levels
167+
One can customize log levels as per the following example. The default log level is `info`.
168+
169+
The key to be used here is also printed in the violation log message.
170+
171+
```java
172+
@Configuration
173+
public class ValidatorConfiguration {
174+
@Bean
175+
public ValidatorConfiguration buildValidatorConfiguration() {
176+
return new ValidatorConfigurationBuilder()
177+
.levelResolverLevel("validation.request.body.schema.additionalProperties", LogLevel.ERROR)
178+
.levelResolverDefaultLevel(LogLevel.INFO)
179+
.build();
180+
}
181+
}
182+
```
183+
184+
### Multiple spec files
185+
It is possible to use multiple spec files for different paths. This can be achieved as demonstrated in the following
186+
code snipped.
187+
188+
It is best practice to use a catch-all spec file. If a request is not matching any of the paths defined here it will
189+
result in a violation error with log level `warn`.
190+
191+
```java
192+
@Configuration
193+
public class ValidatorConfiguration {
194+
@Bean
195+
public ValidatorConfiguration buildValidatorConfiguration() {
196+
return new ValidatorConfigurationBuilder()
197+
.specificationPath(Pattern.compile("/v1/.*"), "openapi-v1.yaml")
198+
.specificationPath(Pattern.compile("/.*"), "openapi.yaml")
199+
.build();
200+
}
201+
}
202+
```
203+
166204
## Examples
167205
Run examples with `./gradlew :examples:example-spring-boot-starter-web:bootRun` or `./gradlew :examples:example-spring-boot-starter-webflux:bootRun`.
168206

Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
package com.getyourguide.openapi.validation.api.model;
22

33
import com.getyourguide.openapi.validation.api.log.LogLevel;
4+
import java.util.List;
45
import java.util.Map;
5-
import lombok.Builder;
6+
import java.util.regex.Pattern;
7+
import lombok.AllArgsConstructor;
68
import lombok.Getter;
79

8-
@Builder
10+
@AllArgsConstructor
911
@Getter
1012
public class ValidatorConfiguration {
1113
private final LogLevel levelResolverDefaultLevel;
1214
private final Map<String, LogLevel> levelResolverLevels;
15+
16+
private final List<PathPatternSpec> specificationPaths;
17+
18+
public record PathPatternSpec(Pattern pathPattern, String specificationFilePath) {
19+
}
1320
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.getyourguide.openapi.validation.api.model;
2+
3+
import com.getyourguide.openapi.validation.api.log.LogLevel;
4+
import java.util.ArrayList;
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.regex.Pattern;
9+
10+
public class ValidatorConfigurationBuilder {
11+
private LogLevel levelResolverDefaultLevel;
12+
private Map<String, LogLevel> levelResolverLevels;
13+
private List<ValidatorConfiguration.PathPatternSpec> specificationPaths;
14+
15+
public ValidatorConfigurationBuilder levelResolverDefaultLevel(LogLevel levelResolverDefaultLevel) {
16+
this.levelResolverDefaultLevel = levelResolverDefaultLevel;
17+
return this;
18+
}
19+
20+
public ValidatorConfigurationBuilder levelResolverLevel(String messageKey, LogLevel level) {
21+
if (this.levelResolverLevels == null) {
22+
this.levelResolverLevels = new HashMap<>();
23+
}
24+
this.levelResolverLevels.put(messageKey, level);
25+
return this;
26+
}
27+
28+
public ValidatorConfigurationBuilder specificationPath(Pattern pathPattern, String specPath) {
29+
if (this.specificationPaths == null) {
30+
this.specificationPaths = new ArrayList<>();
31+
}
32+
this.specificationPaths.add(new ValidatorConfiguration.PathPatternSpec(pathPattern, specPath));
33+
return this;
34+
}
35+
36+
public ValidatorConfiguration build() {
37+
return new ValidatorConfiguration(
38+
levelResolverDefaultLevel,
39+
levelResolverLevels,
40+
specificationPaths
41+
);
42+
}
43+
44+
public String toString() {
45+
return "ValidatorConfigurationBuilder("
46+
+ "levelResolverDefaultLevel=" + this.levelResolverDefaultLevel + ", "
47+
+ "levelResolverLevels=" + this.levelResolverLevels
48+
+ ")";
49+
}
50+
}

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

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,77 @@
55
import com.atlassian.oai.validator.report.ValidationReport;
66
import com.getyourguide.openapi.validation.api.log.LogLevel;
77
import com.getyourguide.openapi.validation.api.model.ValidatorConfiguration;
8+
import com.getyourguide.openapi.validation.core.validator.MultipleSpecOpenApiInteractionValidatorWrapper;
9+
import com.getyourguide.openapi.validation.core.validator.OpenApiInteractionValidatorWrapper;
10+
import com.getyourguide.openapi.validation.core.validator.SingleSpecOpenApiInteractionValidatorWrapper;
811
import java.io.BufferedReader;
912
import java.io.File;
1013
import java.io.IOException;
1114
import java.io.InputStreamReader;
1215
import java.nio.charset.StandardCharsets;
1316
import java.util.Map;
17+
import java.util.Objects;
1418
import java.util.Optional;
1519
import java.util.stream.Collectors;
1620
import javax.annotation.Nullable;
1721
import lombok.NonNull;
1822
import lombok.extern.slf4j.Slf4j;
1923
import org.apache.commons.io.FileUtils;
24+
import org.apache.commons.lang3.tuple.Pair;
2025

2126
@Slf4j
2227
public class OpenApiInteractionValidatorFactory {
2328
@Nullable
24-
public OpenApiInteractionValidator build(String specificationFilePath, ValidatorConfiguration configuration) {
29+
public OpenApiInteractionValidatorWrapper build(
30+
String specificationFilePath,
31+
ValidatorConfiguration configuration
32+
) {
33+
if (configuration.getSpecificationPaths() != null && !configuration.getSpecificationPaths().isEmpty()) {
34+
return buildMultipleSpecOpenApiInteractionValidatorWrapper(configuration);
35+
}
36+
2537
var specOptional = loadOpenAPISpec(specificationFilePath);
2638
if (specOptional.isEmpty()) {
2739
log.info("OpenAPI spec file could not be found [validation disabled]");
2840
return null;
2941
}
3042

31-
var spec = specOptional.get();
43+
return buildSingleSpecOpenApiInteractionValidatorWrapper(specOptional.get(),
44+
configuration.getLevelResolverLevels(), configuration.getLevelResolverDefaultLevel());
45+
}
46+
47+
private MultipleSpecOpenApiInteractionValidatorWrapper buildMultipleSpecOpenApiInteractionValidatorWrapper(
48+
ValidatorConfiguration configuration) {
49+
var validators = configuration.getSpecificationPaths().stream()
50+
.map(entry -> {
51+
var path = entry.specificationFilePath();
52+
var specOptional = loadSpecFromPath(path).or(() -> loadSpecFromResources(path));
53+
if (specOptional.isEmpty()) {
54+
log.error("OpenAPI spec file {} could not be found", path);
55+
return null;
56+
}
57+
var validator = buildSingleSpecOpenApiInteractionValidatorWrapper(specOptional.get(),
58+
configuration.getLevelResolverLevels(), configuration.getLevelResolverDefaultLevel());
59+
return Pair.of(entry.pathPattern(), (OpenApiInteractionValidatorWrapper) validator);
60+
})
61+
.filter(Objects::nonNull)
62+
.collect(Collectors.toList());
63+
return new MultipleSpecOpenApiInteractionValidatorWrapper(validators);
64+
}
65+
66+
private SingleSpecOpenApiInteractionValidatorWrapper buildSingleSpecOpenApiInteractionValidatorWrapper(
67+
String spec,
68+
Map<String, LogLevel> levelResolverLevels,
69+
LogLevel levelResolverDefaultLevel
70+
) {
3271
try {
33-
return OpenApiInteractionValidator
72+
var validator = OpenApiInteractionValidator
3473
.createForInlineApiSpecification(spec)
3574
.withResolveRefs(true)
3675
.withResolveCombinators(true) // Inline to avoid problems with allOf
37-
.withLevelResolver(buildLevelResolver(configuration))
76+
.withLevelResolver(buildLevelResolver(levelResolverLevels, levelResolverDefaultLevel))
3877
.build();
78+
return new SingleSpecOpenApiInteractionValidatorWrapper(validator);
3979
} catch (Throwable e) {
4080
log.error("Could not initialize OpenApiInteractionValidator [validation disabled]", e);
4181
return null;
@@ -95,17 +135,22 @@ private Optional<String> loadSpecFromResources(String resourceFileLocation) {
95135
}
96136
}
97137

98-
private LevelResolver buildLevelResolver(ValidatorConfiguration configuration) {
138+
private LevelResolver buildLevelResolver(
139+
Map<String, LogLevel> levelResolverLevels,
140+
LogLevel levelResolverDefaultLevel
141+
) {
99142
var builder = LevelResolver.create();
100-
if (configuration.getLevelResolverLevels() != null && !configuration.getLevelResolverLevels().isEmpty()) {
143+
if (levelResolverLevels != null && !levelResolverLevels.isEmpty()) {
101144
builder.withLevels(
102-
configuration.getLevelResolverLevels().entrySet().stream()
103-
.collect(Collectors.toMap(Map.Entry::getKey, entry -> mapLevel(entry.getValue()).orElse(ValidationReport.Level.INFO)))
145+
levelResolverLevels.entrySet().stream()
146+
.collect(Collectors.toMap(Map.Entry::getKey, entry ->
147+
mapLevel(entry.getValue()).orElse(ValidationReport.Level.INFO))
148+
)
104149
);
105150
}
106151
return builder
107152
// this will cause all messages to be warn by default
108-
.withDefaultLevel(mapLevel(configuration.getLevelResolverDefaultLevel()).orElse(ValidationReport.Level.INFO))
153+
.withDefaultLevel(mapLevel(levelResolverDefaultLevel).orElse(ValidationReport.Level.INFO))
109154
.build();
110155
}
111156

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.getyourguide.openapi.validation.core;
22

3-
import com.atlassian.oai.validator.OpenApiInteractionValidator;
43
import com.atlassian.oai.validator.model.Request;
54
import com.atlassian.oai.validator.model.SimpleRequest;
65
import com.atlassian.oai.validator.model.SimpleResponse;
76
import com.getyourguide.openapi.validation.api.model.Direction;
87
import com.getyourguide.openapi.validation.api.model.RequestMetaData;
98
import com.getyourguide.openapi.validation.api.model.ResponseMetaData;
109
import com.getyourguide.openapi.validation.api.model.ValidatorConfiguration;
10+
import com.getyourguide.openapi.validation.core.validator.OpenApiInteractionValidatorWrapper;
1111
import java.nio.charset.StandardCharsets;
1212
import java.util.concurrent.LinkedBlockingQueue;
1313
import java.util.concurrent.ThreadPoolExecutor;
@@ -19,7 +19,7 @@
1919
public class OpenApiRequestValidator {
2020
private final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 2, 1000L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10));
2121

22-
private final OpenApiInteractionValidator validator;
22+
private final OpenApiInteractionValidatorWrapper validator;
2323
private final ValidationReportHandler validationReportHandler;
2424

2525
public OpenApiRequestValidator(ValidationReportHandler validationReportHandler, String specificationFilePath, ValidatorConfiguration configuration) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package com.getyourguide.openapi.validation.core.validator;
2+
3+
import com.atlassian.oai.validator.model.Request;
4+
import com.atlassian.oai.validator.model.SimpleRequest;
5+
import com.atlassian.oai.validator.model.SimpleResponse;
6+
import com.atlassian.oai.validator.report.ValidationReport;
7+
import java.util.List;
8+
import java.util.Optional;
9+
import java.util.regex.Pattern;
10+
import javax.annotation.Nonnull;
11+
import lombok.AllArgsConstructor;
12+
import org.apache.commons.lang3.tuple.Pair;
13+
14+
public class MultipleSpecOpenApiInteractionValidatorWrapper implements OpenApiInteractionValidatorWrapper {
15+
public static final String MESSAGE_KEY_VALIDATOR_FOUND = "zopenapi-validator-java.noValidatorFound";
16+
private final List<Pair<Pattern, OpenApiInteractionValidatorWrapper>> validators;
17+
18+
public MultipleSpecOpenApiInteractionValidatorWrapper(
19+
List<Pair<Pattern, OpenApiInteractionValidatorWrapper>> validators
20+
) {
21+
assert validators != null && validators.size() > 0;
22+
23+
this.validators = validators;
24+
}
25+
26+
@Override
27+
public ValidationReport validateRequest(SimpleRequest request) {
28+
return getValidatorForPath(request.getPath())
29+
.map(validator -> validator.validateRequest(request))
30+
.orElse(new SimpleValidationReport(List.of(buildNoValidatorFoundMessage(request.getPath()))));
31+
}
32+
33+
@Override
34+
public ValidationReport validateResponse(String path, Request.Method method, SimpleResponse response) {
35+
return getValidatorForPath(path)
36+
.map(validator -> validator.validateResponse(path, method, response))
37+
.orElse(new SimpleValidationReport(List.of(buildNoValidatorFoundMessage(path))));
38+
}
39+
40+
private Optional<OpenApiInteractionValidatorWrapper> getValidatorForPath(String path) {
41+
for (var validator : validators) {
42+
if (validator.getLeft().matcher(path).matches()) {
43+
return Optional.of(validator.getRight());
44+
}
45+
}
46+
47+
return Optional.empty();
48+
}
49+
50+
private static SimpleMessage buildNoValidatorFoundMessage(String path) {
51+
return new SimpleMessage(
52+
MESSAGE_KEY_VALIDATOR_FOUND,
53+
"No validator found for path: " + path,
54+
ValidationReport.Level.WARN
55+
);
56+
}
57+
58+
@AllArgsConstructor
59+
private static class SimpleValidationReport implements ValidationReport {
60+
private final List<Message> messages;
61+
62+
@Nonnull
63+
@Override
64+
public List<Message> getMessages() {
65+
return messages;
66+
}
67+
68+
@Override
69+
public ValidationReport withAdditionalContext(MessageContext context) {
70+
return this;
71+
}
72+
}
73+
74+
@AllArgsConstructor
75+
private static class SimpleMessage implements ValidationReport.Message {
76+
private final String key;
77+
private final String message;
78+
private final ValidationReport.Level level;
79+
80+
@Override
81+
public String getKey() {
82+
return key;
83+
}
84+
85+
@Override
86+
public String getMessage() {
87+
return message;
88+
}
89+
90+
@Override
91+
public ValidationReport.Level getLevel() {
92+
return level;
93+
}
94+
95+
@Override
96+
public List<String> getAdditionalInfo() {
97+
return List.of();
98+
}
99+
100+
@Override
101+
public Optional<ValidationReport.MessageContext> getContext() {
102+
return Optional.empty();
103+
}
104+
105+
@Override
106+
public ValidationReport.Message withLevel(ValidationReport.Level level) {
107+
return this;
108+
}
109+
110+
@Override
111+
public ValidationReport.Message withAdditionalInfo(String info) {
112+
return this;
113+
}
114+
115+
@Override
116+
public ValidationReport.Message withAdditionalContext(ValidationReport.MessageContext context) {
117+
return this;
118+
}
119+
120+
@Override
121+
public String toString() {
122+
return message;
123+
}
124+
}
125+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.getyourguide.openapi.validation.core.validator;
2+
3+
import com.atlassian.oai.validator.model.Request;
4+
import com.atlassian.oai.validator.model.SimpleRequest;
5+
import com.atlassian.oai.validator.model.SimpleResponse;
6+
import com.atlassian.oai.validator.report.ValidationReport;
7+
8+
public interface OpenApiInteractionValidatorWrapper {
9+
ValidationReport validateRequest(SimpleRequest request);
10+
11+
ValidationReport validateResponse(String path, Request.Method method, SimpleResponse response);
12+
}

0 commit comments

Comments
 (0)