Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,41 @@ paths:

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

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

NOTE: It is planned to support more media types in the future.
It is also planned to support custom implementations of {@link io.vertx.openapi.validation.analyser.ContentAnalyser}, so that any media type can be validated.
Unknown media types are rejected and the contract will load with an exception.

You can add additional media types when you construct the contract via the {@link
io.vertx.openapi.contract.OpenAPIContractBuilder}. You need to provide a {@link
io.vertx.openapi.mediatype.MediaTypeRegistry}, either by starting from the default one {@link
io.vertx.openapi.mediatype.MediaTypeRegistry#createDefault} or an empty one {@link
io.vertx.openapi.mediatype.MediaTypeRegistry#createEmpty}. On the registry you can register new media types via a
`MediaTypeRegistration` that consist of a `MediaTypePredicate` that checks whether the registration can handle a
given media type and a `ContentAnalyserFactory` that creates a new `ContentAnalyser` for each validation.

Vert.x OpenAPI contains ready to use predicates for static media type strings ({@link
io.vertx.openapi.mediatype.MediaTypePredicate#ofExactTypes}) and regular expressions ({@link
io.vertx.openapi.mediatype.MediaTypePredicate#ofRegexp}). If those do not match your need, you must provide
your own predicate implementations.

Vert.x OpenAPI contains ready to use ContentAnalysers that perform schema validation for json bodies ({@link
io.vertx.openapi.mediatype.ContentAnalyserFactory#json}), multipart bodies ({@link
io.vertx.openapi.mediatype.ContentAnalyserFactory#multipart}). Additionally a noop ContentAnalyser is available that
does not perform any validation ({@link io.vertx.openapi.mediatype.ContentAnalyserFactory#noop}). If those do not fit
your needs, you need to provide your own implementation.

Example:

[source,$lang]
----
{@link examples.ContractExamples#createContractWithCustomMediaTypes}
----

=== Validation of Requests

Expand Down
24 changes: 24 additions & 0 deletions src/main/java/examples/ContractExamples.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
import io.vertx.openapi.contract.Operation;
import io.vertx.openapi.contract.Parameter;
import io.vertx.openapi.contract.Path;
import io.vertx.openapi.mediatype.ContentAnalyserFactory;
import io.vertx.openapi.mediatype.MediaTypeRegistration;
import io.vertx.openapi.mediatype.MediaTypePredicate;
import io.vertx.openapi.mediatype.MediaTypeRegistry;
import io.vertx.openapi.mediatype.impl.DefaultMediaTypeRegistration;

import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -61,4 +66,23 @@ public void pathParameterOperationExample() {
private OpenAPIContract getContract() {
return null;
}

public void createContractWithCustomMediaTypes(Vertx vertx) {
String pathToContract = ".../.../myContract.json"; // json or yaml
String pathToComponents = ".../.../myComponents.json"; // json or yaml

Future<OpenAPIContract> contract =
OpenAPIContract.builder(vertx)
.setContractPath(pathToContract)
.setAdditionalContractPartPaths(Map.of(
"https://example.com/pet-components", pathToComponents))
.mediaTypeRegistry(
MediaTypeRegistry.createDefault()
.register(
new DefaultMediaTypeRegistration(
MediaTypePredicate.ofExactTypes("text/my-custom-type+json"),
ContentAnalyserFactory.json()))
)
.build();
}
}
16 changes: 16 additions & 0 deletions src/main/java/io/vertx/openapi/contract/MediaType.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,36 @@
@VertxGen
public interface MediaType extends OpenAPIObject {

@Deprecated
String APPLICATION_JSON = "application/json";
@Deprecated
String APPLICATION_JSON_UTF8 = APPLICATION_JSON + "; charset=utf-8";
@Deprecated
String MULTIPART_FORM_DATA = "multipart/form-data";
@Deprecated
String APPLICATION_HAL_JSON = "application/hal+json";
@Deprecated
String APPLICATION_OCTET_STREAM = "application/octet-stream";
@Deprecated
String TEXT_PLAIN = "text/plain";
@Deprecated
String TEXT_PLAIN_UTF8 = TEXT_PLAIN + "; charset=utf-8";
@Deprecated
List<String> SUPPORTED_MEDIA_TYPES = List.of(APPLICATION_JSON, APPLICATION_JSON_UTF8, MULTIPART_FORM_DATA,
APPLICATION_HAL_JSON, APPLICATION_OCTET_STREAM, TEXT_PLAIN, TEXT_PLAIN_UTF8);

/**
* @deprecated The {@link io.vertx.openapi.mediatype.MediaTypeRegistry} replaced the usage of this static method.
*/
@Deprecated
static boolean isMediaTypeSupported(String type) {
return SUPPORTED_MEDIA_TYPES.contains(type.toLowerCase()) || isVendorSpecificJson(type);
}

/**
* @deprecated The {@link io.vertx.openapi.mediatype.MediaTypeRegistry} replaced the usage of this static method.
*/
@Deprecated
static boolean isVendorSpecificJson(String type) {
return VendorSpecificJson.matches(type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.vertx.json.schema.JsonSchemaValidationException;
import io.vertx.openapi.contract.impl.OpenAPIContractImpl;
import io.vertx.openapi.impl.Utils;
import io.vertx.openapi.mediatype.MediaTypeRegistry;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -57,6 +58,7 @@ public OpenAPIContractBuilderException(String message) {
private JsonObject contract;
private final Map<String, String> additionalContractPartPaths = new HashMap<>();
private final Map<String, JsonObject> additionalContractParts = new HashMap<>();
private MediaTypeRegistry registry;

public OpenAPIContractBuilder(Vertx vertx) {
this.vertx = vertx;
Expand Down Expand Up @@ -153,12 +155,19 @@ public OpenAPIContractBuilder setAdditionalContractParts(Map<String, JsonObject>
return this;
}

public OpenAPIContractBuilder mediaTypeRegistry(MediaTypeRegistry registry) {
this.registry = registry;
return this;
}

/**
* Builds the contract.
*
* @return The contract.
*/
public Future<OpenAPIContract> build() {
if (this.registry == null)
this.registry = MediaTypeRegistry.createDefault();

if (contractPath == null && contract == null) {
return Future.failedFuture(new OpenAPIContractBuilderException(
Expand Down Expand Up @@ -192,7 +201,7 @@ private Future<OpenAPIContract> buildOpenAPIContract() {
return failedFuture(createInvalidContract(null, e));
}
})
.map(resolvedSpec -> new OpenAPIContractImpl(resolvedSpec, version, repository)))
.map(resolvedSpec -> new OpenAPIContractImpl(resolvedSpec, version, repository, registry)))
.recover(e -> {
// Convert any non-openapi exceptions into an OpenAPIContractException
if (e instanceof OpenAPIContractException) {
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/io/vertx/openapi/contract/impl/MediaTypeImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@
import io.vertx.core.json.JsonObject;
import io.vertx.json.schema.JsonSchema;
import io.vertx.openapi.contract.MediaType;
import io.vertx.openapi.mediatype.MediaTypeRegistration;

public class MediaTypeImpl implements MediaType {
private static final String KEY_SCHEMA = "schema";
private final JsonObject mediaTypeModel;
private final String identifier;
private final MediaTypeRegistration registration;

private final JsonSchema schema;

public MediaTypeImpl(String identifier, JsonObject mediaTypeModel) {
public MediaTypeImpl(String identifier, JsonObject mediaTypeModel, MediaTypeRegistration registration) {
this.identifier = identifier;
this.mediaTypeModel = mediaTypeModel;
this.registration = registration;

if (mediaTypeModel == null) {
throw createUnsupportedFeature("Media Type without a schema");
Expand Down Expand Up @@ -65,4 +68,12 @@ public String getIdentifier() {
public JsonObject getOpenAPIModel() {
return mediaTypeModel;
}

/**
* The MediaTypeRegistration which is associated to this MediaType.
* @return the associated MediaTypeRegistration
*/
public MediaTypeRegistration getRegistration() {
return registration;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import io.vertx.openapi.contract.SecurityRequirement;
import io.vertx.openapi.contract.SecurityScheme;
import io.vertx.openapi.contract.Server;
import io.vertx.openapi.mediatype.MediaTypeRegistry;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -65,13 +66,17 @@ public class OpenAPIContractImpl implements OpenAPIContract {

private final Map<String, SecurityScheme> securitySchemes;

private final MediaTypeRegistry mediaTypeRegistry;

// VisibleForTesting
final String basePath;

public OpenAPIContractImpl(JsonObject resolvedSpec, OpenAPIVersion version, SchemaRepository schemaRepository) {
public OpenAPIContractImpl(JsonObject resolvedSpec, OpenAPIVersion version, SchemaRepository schemaRepository,
MediaTypeRegistry mediaTypeRegistry) {
this.rawContract = resolvedSpec;
this.version = version;
this.schemaRepository = schemaRepository;
this.mediaTypeRegistry = mediaTypeRegistry;

servers = resolvedSpec
.getJsonArray(KEY_SERVERS, EMPTY_JSON_ARRAY)
Expand All @@ -95,7 +100,7 @@ public OpenAPIContractImpl(JsonObject resolvedSpec, OpenAPIVersion version, Sche
.stream()
.filter(JsonSchema.EXCLUDE_ANNOTATION_ENTRIES)
.map(pathEntry -> new PathImpl(basePath, pathEntry.getKey(), (JsonObject) pathEntry.getValue(),
securityRequirements))
securityRequirements, mediaTypeRegistry))
.collect(toList());

List<PathImpl> sortedPaths = applyMountOrder(unsortedPaths);
Expand Down
10 changes: 6 additions & 4 deletions src/main/java/io/vertx/openapi/contract/impl/OperationImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.vertx.openapi.contract.RequestBody;
import io.vertx.openapi.contract.Response;
import io.vertx.openapi.contract.SecurityRequirement;
import io.vertx.openapi.mediatype.MediaTypeRegistry;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -67,7 +68,7 @@ public class OperationImpl implements Operation {

public OperationImpl(String absolutePath, String path, HttpMethod method, JsonObject operationModel,
List<Parameter> pathParameters, Map<String, Object> pathExtensions,
List<SecurityRequirement> globalSecReq) {
List<SecurityRequirement> globalSecReq, MediaTypeRegistry registry) {
this.absolutePath = absolutePath;
this.operationId = operationModel.getString(KEY_OPERATION_ID);
this.method = method;
Expand Down Expand Up @@ -119,7 +120,7 @@ public OperationImpl(String absolutePath, String path, HttpMethod method, JsonOb
if (requestBodyJson == null || requestBodyJson.isEmpty()) {
this.requestBody = null;
} else {
this.requestBody = new RequestBodyImpl(requestBodyJson, operationId);
this.requestBody = new RequestBodyImpl(requestBodyJson, operationId, registry);
}

JsonObject responsesJson = operationModel.getJsonObject(KEY_RESPONSES, EMPTY_JSON_OBJECT);
Expand All @@ -128,7 +129,7 @@ public OperationImpl(String absolutePath, String path, HttpMethod method, JsonOb
throw createInvalidContract(msg);
}
defaultResponse = responsesJson.stream().filter(entry -> "default".equalsIgnoreCase(entry.getKey())).findFirst()
.map(entry -> new ResponseImpl((JsonObject) entry.getValue(), operationId)).orElse(null);
.map(entry -> new ResponseImpl((JsonObject) entry.getValue(), operationId, registry)).orElse(null);
responses =
unmodifiableMap(
responsesJson
Expand All @@ -137,7 +138,8 @@ public OperationImpl(String absolutePath, String path, HttpMethod method, JsonOb
.filter(JsonSchema.EXCLUDE_ANNOTATIONS)
.filter(RESPONSE_CODE_PATTERN.asPredicate())
.collect(
toMap(Integer::parseInt, key -> new ResponseImpl(responsesJson.getJsonObject(key), operationId))));
toMap(Integer::parseInt,
key -> new ResponseImpl(responsesJson.getJsonObject(key), operationId, registry))));
}

@Override
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/io/vertx/openapi/contract/impl/PathImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.vertx.openapi.contract.Parameter;
import io.vertx.openapi.contract.Path;
import io.vertx.openapi.contract.SecurityRequirement;
import io.vertx.openapi.mediatype.MediaTypeRegistry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -62,7 +63,8 @@ public class PathImpl implements Path {
private final JsonObject pathModel;
private final String absolutePath;

public PathImpl(String basePath, String name, JsonObject pathModel, List<SecurityRequirement> globalSecReq) {
public PathImpl(String basePath, String name, JsonObject pathModel, List<SecurityRequirement> globalSecReq,
MediaTypeRegistry registry) {
this.absolutePath = (basePath.endsWith("/") ? basePath.substring(0, basePath.length() - 1) : basePath) + name;
this.pathModel = pathModel;
if (name.contains("*")) {
Expand All @@ -82,7 +84,7 @@ public PathImpl(String basePath, String name, JsonObject pathModel, List<Securit
List<Operation> ops = new ArrayList<>();
SUPPORTED_METHODS.forEach((methodName, method) -> Optional.ofNullable(pathModel.getJsonObject(methodName))
.map(operationModel -> new OperationImpl(absolutePath, name, method, operationModel, parameters,
getExtensions(), globalSecReq))
getExtensions(), globalSecReq, registry))
.ifPresent(ops::add));
this.operations = unmodifiableList(ops);
}
Expand Down
16 changes: 10 additions & 6 deletions src/main/java/io/vertx/openapi/contract/impl/RequestBodyImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@

package io.vertx.openapi.contract.impl;

import static io.vertx.openapi.contract.MediaType.SUPPORTED_MEDIA_TYPES;
import static io.vertx.openapi.contract.MediaType.isMediaTypeSupported;
import static io.vertx.openapi.contract.OpenAPIContractException.createInvalidContract;
import static io.vertx.openapi.contract.OpenAPIContractException.createUnsupportedFeature;
import static io.vertx.openapi.impl.Utils.EMPTY_JSON_OBJECT;
Expand All @@ -25,6 +23,8 @@
import io.vertx.json.schema.JsonSchema;
import io.vertx.openapi.contract.MediaType;
import io.vertx.openapi.contract.RequestBody;
import io.vertx.openapi.mediatype.MediaTypeRegistration;
import io.vertx.openapi.mediatype.MediaTypeRegistry;
import java.util.Map;

public class RequestBodyImpl implements RequestBody {
Expand All @@ -37,7 +37,7 @@ public class RequestBodyImpl implements RequestBody {

private final Map<String, MediaType> content;

public RequestBodyImpl(JsonObject requestBodyModel, String operationId) {
public RequestBodyImpl(JsonObject requestBodyModel, String operationId, MediaTypeRegistry registry) {
this.requestBodyModel = requestBodyModel;
this.required = requestBodyModel.getBoolean(KEY_REQUIRED, false);
JsonObject contentObject = requestBodyModel.getJsonObject(KEY_CONTENT, EMPTY_JSON_OBJECT);
Expand All @@ -48,14 +48,18 @@ public RequestBodyImpl(JsonObject requestBodyModel, String operationId) {
.stream()
.filter(JsonSchema.EXCLUDE_ANNOTATIONS)
.filter(mediaTypeIdentifier -> {
if (isMediaTypeSupported(mediaTypeIdentifier)) {
if (registry.isSupported(mediaTypeIdentifier)) {
return true;
}
String msgTemplate = "Operation %s defines a request body with an unsupported media type. Supported: %s";
throw createUnsupportedFeature(
String.format(msgTemplate, operationId, join(", ", SUPPORTED_MEDIA_TYPES)));
String.format(msgTemplate, operationId, join(", ", registry.supportedTypes())));
})
.collect(toMap(this::removeWhiteSpaces, key -> new MediaTypeImpl(key, contentObject.getJsonObject(key)))));
.collect(toMap(this::removeWhiteSpaces, key -> {
// Can't be null, otherwise isSupported would have returned false
MediaTypeRegistration registration = registry.get(key);
return new MediaTypeImpl(key, contentObject.getJsonObject(key), registration);
})));

if (content.isEmpty()) {
String msg =
Expand Down
Loading
Loading