Skip to content
Closed
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
33 changes: 30 additions & 3 deletions src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,42 @@ 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 predicate 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}), regular expressions ({@link
io.vertx.openapi.mediatype.MediaTypePredicate#ofRegexp}) and mediatype compatibility ({@link
io.vertx.openapi.mediatype.MediaTypePredicate#ofCompatibleTypes}). 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: 23 additions & 1 deletion src/main/java/examples/ContractExamples.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
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 java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -61,4 +64,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(
MediaTypeRegistration.create(
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
10 changes: 10 additions & 0 deletions src/main/java/io/vertx/openapi/contract/OpenAPIContract.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@

package io.vertx.openapi.contract;

import io.vertx.codegen.annotations.GenIgnore;
import io.vertx.codegen.annotations.Nullable;
import io.vertx.codegen.annotations.VertxGen;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.JsonObject;
import io.vertx.json.schema.SchemaRepository;
import io.vertx.openapi.mediatype.MediaTypeRegistry;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -175,4 +177,12 @@ static Future<OpenAPIContract> from(Vertx vertx, JsonObject contract,
*/
@Nullable
SecurityScheme securityScheme(String name);

/**
* Gets the mediatype registry.
*
* @return The registry.
*/
@GenIgnore
MediaTypeRegistry getMediaTypeRegistry();
}
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
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 mediaTypes;

// VisibleForTesting
final String basePath;

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

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, mediaTypes))
.collect(toList());

List<PathImpl> sortedPaths = applyMountOrder(unsortedPaths);
Expand Down Expand Up @@ -232,4 +237,9 @@ public List<SecurityRequirement> getSecurityRequirements() {
public SecurityScheme securityScheme(String name) {
return securitySchemes.get(name);
}

@Override
public MediaTypeRegistry getMediaTypeRegistry() {
return mediaTypes;
}
}
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
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,7 @@
import io.vertx.json.schema.JsonSchema;
import io.vertx.openapi.contract.MediaType;
import io.vertx.openapi.contract.RequestBody;
import io.vertx.openapi.mediatype.MediaTypeRegistry;
import java.util.Map;

public class RequestBodyImpl implements RequestBody {
Expand All @@ -37,7 +36,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,12 +47,12 @@ 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)))));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@

import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.vertx.openapi.contract.Location.HEADER;
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.createUnsupportedFeature;
import static io.vertx.openapi.impl.Utils.EMPTY_JSON_OBJECT;
import static java.lang.String.join;
Expand All @@ -29,6 +27,7 @@
import io.vertx.openapi.contract.MediaType;
import io.vertx.openapi.contract.Parameter;
import io.vertx.openapi.contract.Response;
import io.vertx.openapi.mediatype.MediaTypeRegistry;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
Expand All @@ -45,7 +44,7 @@ public class ResponseImpl implements Response {

private final JsonObject responseModel;

public ResponseImpl(JsonObject responseModel, String operationId) {
public ResponseImpl(JsonObject responseModel, String operationId, MediaTypeRegistry registry) {
this.responseModel = responseModel;

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

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

Expand Down
Loading
Loading