Skip to content

Commit 5baf47d

Browse files
committed
Add custom mediatypes
1 parent 9046615 commit 5baf47d

File tree

54 files changed

+1097
-341
lines changed

Some content is hidden

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

54 files changed

+1097
-341
lines changed

src/main/asciidoc/index.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,12 @@ The validation can fail for formal reasons, such as the wrong format for a param
151151
However, validation can of course also fail because the content does not match the defined schema.
152152
In this case a {@link io.vertx.openapi.validation.SchemaValidationException} is thrown.
153153
It is a subclass of _ValidatorException_ and provides access to the related {@link io.vertx.json.schema.OutputUnit} to allow further analysis of the error.
154+
155+
=== Add custom media types
156+
157+
By default a commonly used set of media types is supported. In case you need support for additional media types you have to register via the OpenAPIContractBuilder and a MediaTypeRegistry.
158+
159+
[source,$lang]
160+
----
161+
{@link examples.ContractExamples#createContractWithCustomMediaTypes}
162+
----

src/main/java/examples/ContractExamples.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
import io.vertx.openapi.contract.Operation;
1919
import io.vertx.openapi.contract.Parameter;
2020
import io.vertx.openapi.contract.Path;
21-
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;
2225
import java.util.HashMap;
2326
import java.util.Map;
2427

@@ -61,4 +64,23 @@ public void pathParameterOperationExample() {
6164
private OpenAPIContract getContract() {
6265
return null;
6366
}
67+
68+
public void createContractWithCustomMediaTypes(Vertx vertx) {
69+
String pathToContract = ".../.../myContract.json"; // json or yaml
70+
String pathToComponents = ".../.../myComponents.json"; // json or yaml
71+
72+
Future<OpenAPIContract> contract =
73+
OpenAPIContract.builder(vertx)
74+
.setContractPath(pathToContract)
75+
.setAdditionalContractPartPaths(Map.of(
76+
"https://example.com/pet-components", pathToComponents))
77+
.mediaTypeRegistry(
78+
MediaTypeRegistry.createDefault()
79+
.register(
80+
MediaTypeRegistration.create(
81+
MediaTypePredicate.ofExactTypes("text/my-custom-type+json"),
82+
ContentAnalyserFactory.json()))
83+
)
84+
.build();
85+
}
6486
}

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/OpenAPIContract.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212

1313
package io.vertx.openapi.contract;
1414

15+
import io.vertx.codegen.annotations.GenIgnore;
1516
import io.vertx.codegen.annotations.Nullable;
1617
import io.vertx.codegen.annotations.VertxGen;
1718
import io.vertx.core.Future;
1819
import io.vertx.core.Vertx;
1920
import io.vertx.core.http.HttpMethod;
2021
import io.vertx.core.json.JsonObject;
2122
import io.vertx.json.schema.SchemaRepository;
23+
import io.vertx.openapi.mediatype.MediaTypeRegistry;
2224
import java.util.List;
2325
import java.util.Map;
2426

@@ -175,4 +177,12 @@ static Future<OpenAPIContract> from(Vertx vertx, JsonObject contract,
175177
*/
176178
@Nullable
177179
SecurityScheme securityScheme(String name);
180+
181+
/**
182+
* Gets the mediatype registry.
183+
*
184+
* @return The registry.
185+
*/
186+
@GenIgnore
187+
MediaTypeRegistry getMediaTypeRegistry();
178188
}

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/OpenAPIContractImpl.java

Lines changed: 12 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 mediaTypes;
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 mediaTypes) {
7276
this.rawContract = resolvedSpec;
7377
this.version = version;
7478
this.schemaRepository = schemaRepository;
79+
this.mediaTypes = mediaTypes;
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, mediaTypes))
99104
.collect(toList());
100105

101106
List<PathImpl> sortedPaths = applyMountOrder(unsortedPaths);
@@ -232,4 +237,9 @@ public List<SecurityRequirement> getSecurityRequirements() {
232237
public SecurityScheme securityScheme(String name) {
233238
return securitySchemes.get(name);
234239
}
240+
241+
@Override
242+
public MediaTypeRegistry getMediaTypeRegistry() {
243+
return mediaTypes;
244+
}
235245
}

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

Lines changed: 5 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,7 @@ 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, key -> new ResponseImpl(responsesJson.getJsonObject(key), operationId, registry))));
141142
}
142143

143144
@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: 4 additions & 5 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,7 @@
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.MediaTypeRegistry;
2827
import java.util.Map;
2928

3029
public class RequestBodyImpl implements RequestBody {
@@ -37,7 +36,7 @@ public class RequestBodyImpl implements RequestBody {
3736

3837
private final Map<String, MediaType> content;
3938

40-
public RequestBodyImpl(JsonObject requestBodyModel, String operationId) {
39+
public RequestBodyImpl(JsonObject requestBodyModel, String operationId, MediaTypeRegistry registry) {
4140
this.requestBodyModel = requestBodyModel;
4241
this.required = requestBodyModel.getBoolean(KEY_REQUIRED, false);
4342
JsonObject contentObject = requestBodyModel.getJsonObject(KEY_CONTENT, EMPTY_JSON_OBJECT);
@@ -48,12 +47,12 @@ public RequestBodyImpl(JsonObject requestBodyModel, String operationId) {
4847
.stream()
4948
.filter(JsonSchema.EXCLUDE_ANNOTATIONS)
5049
.filter(mediaTypeIdentifier -> {
51-
if (isMediaTypeSupported(mediaTypeIdentifier)) {
50+
if (registry.isSupported(mediaTypeIdentifier)) {
5251
return true;
5352
}
5453
String msgTemplate = "Operation %s defines a request body with an unsupported media type. Supported: %s";
5554
throw createUnsupportedFeature(
56-
String.format(msgTemplate, operationId, join(", ", SUPPORTED_MEDIA_TYPES)));
55+
String.format(msgTemplate, operationId, join(", ", registry.supportedTypes())));
5756
})
5857
.collect(toMap(this::removeWhiteSpaces, key -> new MediaTypeImpl(key, contentObject.getJsonObject(key)))));
5958

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static io.vertx.openapi.contract.MediaType.isMediaTypeSupported;
1919
import static io.vertx.openapi.contract.OpenAPIContractException.createUnsupportedFeature;
2020
import static io.vertx.openapi.impl.Utils.EMPTY_JSON_OBJECT;
21+
import io.vertx.openapi.mediatype.MediaTypeRegistry;
2122
import static java.lang.String.join;
2223
import static java.util.Collections.unmodifiableMap;
2324
import static java.util.function.Function.identity;
@@ -45,7 +46,7 @@ public class ResponseImpl implements Response {
4546

4647
private final JsonObject responseModel;
4748

48-
public ResponseImpl(JsonObject responseModel, String operationId) {
49+
public ResponseImpl(JsonObject responseModel, String operationId, MediaTypeRegistry registry) {
4950
this.responseModel = responseModel;
5051

5152
JsonObject headersObject = responseModel.getJsonObject(KEY_HEADERS, EMPTY_JSON_OBJECT);
@@ -68,9 +69,9 @@ public ResponseImpl(JsonObject responseModel, String operationId) {
6869
.filter(JsonSchema.EXCLUDE_ANNOTATIONS)
6970
.collect(toMap(identity(), key -> new MediaTypeImpl(key, contentObject.getJsonObject(key)))));
7071

71-
if (content.keySet().stream().anyMatch(type -> !isMediaTypeSupported(type))) {
72+
if (content.keySet().stream().anyMatch(type -> !registry.isSupported(type))) {
7273
String msgTemplate = "Operation %s defines a response with an unsupported media type. Supported: %s";
73-
throw createUnsupportedFeature(String.format(msgTemplate, operationId, join(", ", SUPPORTED_MEDIA_TYPES)));
74+
throw createUnsupportedFeature(String.format(msgTemplate, operationId, join(", ", registry.supportedTypes())));
7475
}
7576
}
7677

0 commit comments

Comments
 (0)