Skip to content

Commit 7e237cc

Browse files
lukasjelonekpk-work
andcommitted
feat: add support for custom media types
Co-authored-by: Pascal Krause <[email protected]> Signed-off-by: Pascal Krause <[email protected]>
1 parent 9046615 commit 7e237cc

File tree

62 files changed

+1358
-293
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+1358
-293
lines changed

src/main/asciidoc/index.adoc

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,41 @@ paths:
101101

102102
Vert.x OpenAPI checks both whether the content is syntactically correct and whether it corresponds to the schema.
103103
If no schema is defined, or the content is binary no schema validation is performed.
104-
Currently, only the following media types are supported:
104+
By Default, the following media types are supported:
105105

106106
* application/json
107107
* application/json+hal
108108
* application/octet-stream
109109
* multipart/form-data
110+
* Vendor specific json that matches the following regular expression [^/]+/vnd\.[\w.-]+\+json
110111

111-
NOTE: It is planned to support more media types in the future.
112-
It is also planned to support custom implementations of {@link io.vertx.openapi.validation.analyser.ContentAnalyser}, so that any media type can be validated.
112+
Unknown media types are rejected and the contract will load with an exception.
113+
114+
You can add additional media types when you construct the contract via the {@link
115+
io.vertx.openapi.contract.OpenAPIContractBuilder}. You need to provide a {@link
116+
io.vertx.openapi.mediatype.MediaTypeRegistry}, either by starting from the default one {@link
117+
io.vertx.openapi.mediatype.MediaTypeRegistry#createDefault} or an empty one {@link
118+
io.vertx.openapi.mediatype.MediaTypeRegistry#createEmpty}. On the registry you can register new media types via a
119+
`MediaTypeRegistration` that consist of a `MediaTypePredicate` that checks whether the registration can handle a
120+
given media type and a `ContentAnalyserFactory` that creates a new `ContentAnalyser` for each validation.
121+
122+
Vert.x OpenAPI contains ready to use predicates for static media type strings ({@link
123+
io.vertx.openapi.mediatype.MediaTypePredicate#ofExactTypes}) and regular expressions ({@link
124+
io.vertx.openapi.mediatype.MediaTypePredicate#ofRegexp}). If those do not match your need, you must provide
125+
your own predicate implementations.
126+
127+
Vert.x OpenAPI contains ready to use ContentAnalysers that perform schema validation for json bodies ({@link
128+
io.vertx.openapi.mediatype.ContentAnalyserFactory#json}), multipart bodies ({@link
129+
io.vertx.openapi.mediatype.ContentAnalyserFactory#multipart}). Additionally a noop ContentAnalyser is available that
130+
does not perform any validation ({@link io.vertx.openapi.mediatype.ContentAnalyserFactory#noop}). If those do not fit
131+
your needs, you need to provide your own implementation.
132+
133+
Example:
134+
135+
[source,$lang]
136+
----
137+
{@link examples.ContractExamples#createContractWithCustomMediaTypes}
138+
----
113139

114140
=== Validation of Requests
115141

src/main/java/examples/ContractExamples.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
import io.vertx.openapi.contract.Operation;
1919
import io.vertx.openapi.contract.Parameter;
2020
import io.vertx.openapi.contract.Path;
21+
import io.vertx.openapi.mediatype.ContentAnalyserFactory;
22+
import io.vertx.openapi.mediatype.MediaTypeRegistration;
23+
import io.vertx.openapi.mediatype.MediaTypePredicate;
24+
import io.vertx.openapi.mediatype.MediaTypeRegistry;
25+
import io.vertx.openapi.mediatype.impl.DefaultMediaTypeRegistration;
2126

2227
import java.util.HashMap;
2328
import java.util.Map;
@@ -61,4 +66,23 @@ public void pathParameterOperationExample() {
6166
private OpenAPIContract getContract() {
6267
return null;
6368
}
69+
70+
public void createContractWithCustomMediaTypes(Vertx vertx) {
71+
String pathToContract = ".../.../myContract.json"; // json or yaml
72+
String pathToComponents = ".../.../myComponents.json"; // json or yaml
73+
74+
Future<OpenAPIContract> contract =
75+
OpenAPIContract.builder(vertx)
76+
.setContractPath(pathToContract)
77+
.setAdditionalContractPartPaths(Map.of(
78+
"https://example.com/pet-components", pathToComponents))
79+
.mediaTypeRegistry(
80+
MediaTypeRegistry.createDefault()
81+
.register(
82+
new DefaultMediaTypeRegistration(
83+
MediaTypePredicate.ofExactTypes("text/my-custom-type+json"),
84+
ContentAnalyserFactory.json()))
85+
)
86+
.build();
87+
}
6488
}

src/main/java/io/vertx/openapi/contract/MediaType.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,36 @@
2727
@VertxGen
2828
public interface MediaType extends OpenAPIObject {
2929

30+
@Deprecated
3031
String APPLICATION_JSON = "application/json";
32+
@Deprecated
3133
String APPLICATION_JSON_UTF8 = APPLICATION_JSON + "; charset=utf-8";
34+
@Deprecated
3235
String MULTIPART_FORM_DATA = "multipart/form-data";
36+
@Deprecated
3337
String APPLICATION_HAL_JSON = "application/hal+json";
38+
@Deprecated
3439
String APPLICATION_OCTET_STREAM = "application/octet-stream";
40+
@Deprecated
3541
String TEXT_PLAIN = "text/plain";
42+
@Deprecated
3643
String TEXT_PLAIN_UTF8 = TEXT_PLAIN + "; charset=utf-8";
44+
@Deprecated
3745
List<String> SUPPORTED_MEDIA_TYPES = List.of(APPLICATION_JSON, APPLICATION_JSON_UTF8, MULTIPART_FORM_DATA,
3846
APPLICATION_HAL_JSON, APPLICATION_OCTET_STREAM, TEXT_PLAIN, TEXT_PLAIN_UTF8);
3947

48+
/**
49+
* @deprecated The {@link io.vertx.openapi.mediatype.MediaTypeRegistry} replaced the usage of this static method.
50+
*/
51+
@Deprecated
4052
static boolean isMediaTypeSupported(String type) {
4153
return SUPPORTED_MEDIA_TYPES.contains(type.toLowerCase()) || isVendorSpecificJson(type);
4254
}
4355

56+
/**
57+
* @deprecated The {@link io.vertx.openapi.mediatype.MediaTypeRegistry} replaced the usage of this static method.
58+
*/
59+
@Deprecated
4460
static boolean isVendorSpecificJson(String type) {
4561
return VendorSpecificJson.matches(type);
4662
}

src/main/java/io/vertx/openapi/contract/OpenAPIContractBuilder.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.vertx.json.schema.JsonSchemaValidationException;
2626
import io.vertx.openapi.contract.impl.OpenAPIContractImpl;
2727
import io.vertx.openapi.impl.Utils;
28+
import io.vertx.openapi.mediatype.MediaTypeRegistry;
2829
import java.util.HashMap;
2930
import java.util.Map;
3031
import java.util.stream.Collectors;
@@ -57,6 +58,7 @@ public OpenAPIContractBuilderException(String message) {
5758
private JsonObject contract;
5859
private final Map<String, String> additionalContractPartPaths = new HashMap<>();
5960
private final Map<String, JsonObject> additionalContractParts = new HashMap<>();
61+
private MediaTypeRegistry registry;
6062

6163
public OpenAPIContractBuilder(Vertx vertx) {
6264
this.vertx = vertx;
@@ -153,12 +155,19 @@ public OpenAPIContractBuilder setAdditionalContractParts(Map<String, JsonObject>
153155
return this;
154156
}
155157

158+
public OpenAPIContractBuilder mediaTypeRegistry(MediaTypeRegistry registry) {
159+
this.registry = registry;
160+
return this;
161+
}
162+
156163
/**
157164
* Builds the contract.
158165
*
159166
* @return The contract.
160167
*/
161168
public Future<OpenAPIContract> build() {
169+
if (this.registry == null)
170+
this.registry = MediaTypeRegistry.createDefault();
162171

163172
if (contractPath == null && contract == null) {
164173
return Future.failedFuture(new OpenAPIContractBuilderException(
@@ -192,7 +201,7 @@ private Future<OpenAPIContract> buildOpenAPIContract() {
192201
return failedFuture(createInvalidContract(null, e));
193202
}
194203
})
195-
.map(resolvedSpec -> new OpenAPIContractImpl(resolvedSpec, version, repository)))
204+
.map(resolvedSpec -> new OpenAPIContractImpl(resolvedSpec, version, repository, registry)))
196205
.recover(e -> {
197206
// Convert any non-openapi exceptions into an OpenAPIContractException
198207
if (e instanceof OpenAPIContractException) {

src/main/java/io/vertx/openapi/contract/impl/MediaTypeImpl.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@
1717
import io.vertx.core.json.JsonObject;
1818
import io.vertx.json.schema.JsonSchema;
1919
import io.vertx.openapi.contract.MediaType;
20+
import io.vertx.openapi.mediatype.MediaTypeRegistration;
2021

2122
public class MediaTypeImpl implements MediaType {
2223
private static final String KEY_SCHEMA = "schema";
2324
private final JsonObject mediaTypeModel;
2425
private final String identifier;
26+
private final MediaTypeRegistration registration;
2527

2628
private final JsonSchema schema;
2729

28-
public MediaTypeImpl(String identifier, JsonObject mediaTypeModel) {
30+
public MediaTypeImpl(String identifier, JsonObject mediaTypeModel, MediaTypeRegistration registration) {
2931
this.identifier = identifier;
3032
this.mediaTypeModel = mediaTypeModel;
33+
this.registration = registration;
3134

3235
if (mediaTypeModel == null) {
3336
throw createUnsupportedFeature("Media Type without a schema");
@@ -65,4 +68,12 @@ public String getIdentifier() {
6568
public JsonObject getOpenAPIModel() {
6669
return mediaTypeModel;
6770
}
71+
72+
/**
73+
* The MediaTypeRegistration which is associated to this MediaType.
74+
* @return the associated MediaTypeRegistration
75+
*/
76+
public MediaTypeRegistration getRegistration() {
77+
return registration;
78+
}
6879
}

src/main/java/io/vertx/openapi/contract/impl/OpenAPIContractImpl.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import io.vertx.openapi.contract.SecurityRequirement;
3636
import io.vertx.openapi.contract.SecurityScheme;
3737
import io.vertx.openapi.contract.Server;
38+
import io.vertx.openapi.mediatype.MediaTypeRegistry;
3839
import java.util.ArrayList;
3940
import java.util.List;
4041
import java.util.Map;
@@ -65,13 +66,17 @@ public class OpenAPIContractImpl implements OpenAPIContract {
6566

6667
private final Map<String, SecurityScheme> securitySchemes;
6768

69+
private final MediaTypeRegistry mediaTypeRegistry;
70+
6871
// VisibleForTesting
6972
final String basePath;
7073

71-
public OpenAPIContractImpl(JsonObject resolvedSpec, OpenAPIVersion version, SchemaRepository schemaRepository) {
74+
public OpenAPIContractImpl(JsonObject resolvedSpec, OpenAPIVersion version, SchemaRepository schemaRepository,
75+
MediaTypeRegistry mediaTypeRegistry) {
7276
this.rawContract = resolvedSpec;
7377
this.version = version;
7478
this.schemaRepository = schemaRepository;
79+
this.mediaTypeRegistry = mediaTypeRegistry;
7580

7681
servers = resolvedSpec
7782
.getJsonArray(KEY_SERVERS, EMPTY_JSON_ARRAY)
@@ -95,7 +100,7 @@ public OpenAPIContractImpl(JsonObject resolvedSpec, OpenAPIVersion version, Sche
95100
.stream()
96101
.filter(JsonSchema.EXCLUDE_ANNOTATION_ENTRIES)
97102
.map(pathEntry -> new PathImpl(basePath, pathEntry.getKey(), (JsonObject) pathEntry.getValue(),
98-
securityRequirements))
103+
securityRequirements, mediaTypeRegistry))
99104
.collect(toList());
100105

101106
List<PathImpl> sortedPaths = applyMountOrder(unsortedPaths);

src/main/java/io/vertx/openapi/contract/impl/OperationImpl.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.vertx.openapi.contract.RequestBody;
3535
import io.vertx.openapi.contract.Response;
3636
import io.vertx.openapi.contract.SecurityRequirement;
37+
import io.vertx.openapi.mediatype.MediaTypeRegistry;
3738
import java.util.HashMap;
3839
import java.util.List;
3940
import java.util.Map;
@@ -67,7 +68,7 @@ public class OperationImpl implements Operation {
6768

6869
public OperationImpl(String absolutePath, String path, HttpMethod method, JsonObject operationModel,
6970
List<Parameter> pathParameters, Map<String, Object> pathExtensions,
70-
List<SecurityRequirement> globalSecReq) {
71+
List<SecurityRequirement> globalSecReq, MediaTypeRegistry registry) {
7172
this.absolutePath = absolutePath;
7273
this.operationId = operationModel.getString(KEY_OPERATION_ID);
7374
this.method = method;
@@ -119,7 +120,7 @@ public OperationImpl(String absolutePath, String path, HttpMethod method, JsonOb
119120
if (requestBodyJson == null || requestBodyJson.isEmpty()) {
120121
this.requestBody = null;
121122
} else {
122-
this.requestBody = new RequestBodyImpl(requestBodyJson, operationId);
123+
this.requestBody = new RequestBodyImpl(requestBodyJson, operationId, registry);
123124
}
124125

125126
JsonObject responsesJson = operationModel.getJsonObject(KEY_RESPONSES, EMPTY_JSON_OBJECT);
@@ -128,7 +129,7 @@ public OperationImpl(String absolutePath, String path, HttpMethod method, JsonOb
128129
throw createInvalidContract(msg);
129130
}
130131
defaultResponse = responsesJson.stream().filter(entry -> "default".equalsIgnoreCase(entry.getKey())).findFirst()
131-
.map(entry -> new ResponseImpl((JsonObject) entry.getValue(), operationId)).orElse(null);
132+
.map(entry -> new ResponseImpl((JsonObject) entry.getValue(), operationId, registry)).orElse(null);
132133
responses =
133134
unmodifiableMap(
134135
responsesJson
@@ -137,7 +138,8 @@ public OperationImpl(String absolutePath, String path, HttpMethod method, JsonOb
137138
.filter(JsonSchema.EXCLUDE_ANNOTATIONS)
138139
.filter(RESPONSE_CODE_PATTERN.asPredicate())
139140
.collect(
140-
toMap(Integer::parseInt, key -> new ResponseImpl(responsesJson.getJsonObject(key), operationId))));
141+
toMap(Integer::parseInt,
142+
key -> new ResponseImpl(responsesJson.getJsonObject(key), operationId, registry))));
141143
}
142144

143145
@Override

src/main/java/io/vertx/openapi/contract/impl/PathImpl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import io.vertx.openapi.contract.Parameter;
3232
import io.vertx.openapi.contract.Path;
3333
import io.vertx.openapi.contract.SecurityRequirement;
34+
import io.vertx.openapi.mediatype.MediaTypeRegistry;
3435
import java.util.ArrayList;
3536
import java.util.HashMap;
3637
import java.util.List;
@@ -62,7 +63,8 @@ public class PathImpl implements Path {
6263
private final JsonObject pathModel;
6364
private final String absolutePath;
6465

65-
public PathImpl(String basePath, String name, JsonObject pathModel, List<SecurityRequirement> globalSecReq) {
66+
public PathImpl(String basePath, String name, JsonObject pathModel, List<SecurityRequirement> globalSecReq,
67+
MediaTypeRegistry registry) {
6668
this.absolutePath = (basePath.endsWith("/") ? basePath.substring(0, basePath.length() - 1) : basePath) + name;
6769
this.pathModel = pathModel;
6870
if (name.contains("*")) {
@@ -82,7 +84,7 @@ public PathImpl(String basePath, String name, JsonObject pathModel, List<Securit
8284
List<Operation> ops = new ArrayList<>();
8385
SUPPORTED_METHODS.forEach((methodName, method) -> Optional.ofNullable(pathModel.getJsonObject(methodName))
8486
.map(operationModel -> new OperationImpl(absolutePath, name, method, operationModel, parameters,
85-
getExtensions(), globalSecReq))
87+
getExtensions(), globalSecReq, registry))
8688
.ifPresent(ops::add));
8789
this.operations = unmodifiableList(ops);
8890
}

src/main/java/io/vertx/openapi/contract/impl/RequestBodyImpl.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212

1313
package io.vertx.openapi.contract.impl;
1414

15-
import static io.vertx.openapi.contract.MediaType.SUPPORTED_MEDIA_TYPES;
16-
import static io.vertx.openapi.contract.MediaType.isMediaTypeSupported;
1715
import static io.vertx.openapi.contract.OpenAPIContractException.createInvalidContract;
1816
import static io.vertx.openapi.contract.OpenAPIContractException.createUnsupportedFeature;
1917
import static io.vertx.openapi.impl.Utils.EMPTY_JSON_OBJECT;
@@ -25,6 +23,8 @@
2523
import io.vertx.json.schema.JsonSchema;
2624
import io.vertx.openapi.contract.MediaType;
2725
import io.vertx.openapi.contract.RequestBody;
26+
import io.vertx.openapi.mediatype.MediaTypeRegistration;
27+
import io.vertx.openapi.mediatype.MediaTypeRegistry;
2828
import java.util.Map;
2929

3030
public class RequestBodyImpl implements RequestBody {
@@ -37,7 +37,7 @@ public class RequestBodyImpl implements RequestBody {
3737

3838
private final Map<String, MediaType> content;
3939

40-
public RequestBodyImpl(JsonObject requestBodyModel, String operationId) {
40+
public RequestBodyImpl(JsonObject requestBodyModel, String operationId, MediaTypeRegistry registry) {
4141
this.requestBodyModel = requestBodyModel;
4242
this.required = requestBodyModel.getBoolean(KEY_REQUIRED, false);
4343
JsonObject contentObject = requestBodyModel.getJsonObject(KEY_CONTENT, EMPTY_JSON_OBJECT);
@@ -48,14 +48,18 @@ public RequestBodyImpl(JsonObject requestBodyModel, String operationId) {
4848
.stream()
4949
.filter(JsonSchema.EXCLUDE_ANNOTATIONS)
5050
.filter(mediaTypeIdentifier -> {
51-
if (isMediaTypeSupported(mediaTypeIdentifier)) {
51+
if (registry.isSupported(mediaTypeIdentifier)) {
5252
return true;
5353
}
5454
String msgTemplate = "Operation %s defines a request body with an unsupported media type. Supported: %s";
5555
throw createUnsupportedFeature(
56-
String.format(msgTemplate, operationId, join(", ", SUPPORTED_MEDIA_TYPES)));
56+
String.format(msgTemplate, operationId, join(", ", registry.supportedTypes())));
5757
})
58-
.collect(toMap(this::removeWhiteSpaces, key -> new MediaTypeImpl(key, contentObject.getJsonObject(key)))));
58+
.collect(toMap(this::removeWhiteSpaces, key -> {
59+
// Can't be null, otherwise isSupported would have returned false
60+
MediaTypeRegistration registration = registry.get(key);
61+
return new MediaTypeImpl(key, contentObject.getJsonObject(key), registration);
62+
})));
5963

6064
if (content.isEmpty()) {
6165
String msg =

0 commit comments

Comments
 (0)