diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 5657930b..bc1cfd0a 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -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 diff --git a/src/main/java/examples/ContractExamples.java b/src/main/java/examples/ContractExamples.java index 92f1ca70..adfdbc3b 100644 --- a/src/main/java/examples/ContractExamples.java +++ b/src/main/java/examples/ContractExamples.java @@ -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; @@ -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 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(); + } } diff --git a/src/main/java/io/vertx/openapi/contract/MediaType.java b/src/main/java/io/vertx/openapi/contract/MediaType.java index 3726afdb..d581c6c2 100644 --- a/src/main/java/io/vertx/openapi/contract/MediaType.java +++ b/src/main/java/io/vertx/openapi/contract/MediaType.java @@ -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 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); } diff --git a/src/main/java/io/vertx/openapi/contract/OpenAPIContractBuilder.java b/src/main/java/io/vertx/openapi/contract/OpenAPIContractBuilder.java index 0fc05dd7..8bcf8f5b 100644 --- a/src/main/java/io/vertx/openapi/contract/OpenAPIContractBuilder.java +++ b/src/main/java/io/vertx/openapi/contract/OpenAPIContractBuilder.java @@ -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; @@ -57,6 +58,7 @@ public OpenAPIContractBuilderException(String message) { private JsonObject contract; private final Map additionalContractPartPaths = new HashMap<>(); private final Map additionalContractParts = new HashMap<>(); + private MediaTypeRegistry registry; public OpenAPIContractBuilder(Vertx vertx) { this.vertx = vertx; @@ -153,12 +155,19 @@ public OpenAPIContractBuilder setAdditionalContractParts(Map return this; } + public OpenAPIContractBuilder mediaTypeRegistry(MediaTypeRegistry registry) { + this.registry = registry; + return this; + } + /** * Builds the contract. * * @return The contract. */ public Future build() { + if (this.registry == null) + this.registry = MediaTypeRegistry.createDefault(); if (contractPath == null && contract == null) { return Future.failedFuture(new OpenAPIContractBuilderException( @@ -192,7 +201,7 @@ private Future 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) { diff --git a/src/main/java/io/vertx/openapi/contract/impl/MediaTypeImpl.java b/src/main/java/io/vertx/openapi/contract/impl/MediaTypeImpl.java index cbf82003..1ea7ec82 100644 --- a/src/main/java/io/vertx/openapi/contract/impl/MediaTypeImpl.java +++ b/src/main/java/io/vertx/openapi/contract/impl/MediaTypeImpl.java @@ -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"); @@ -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; + } } diff --git a/src/main/java/io/vertx/openapi/contract/impl/OpenAPIContractImpl.java b/src/main/java/io/vertx/openapi/contract/impl/OpenAPIContractImpl.java index e696eb7d..52864be7 100644 --- a/src/main/java/io/vertx/openapi/contract/impl/OpenAPIContractImpl.java +++ b/src/main/java/io/vertx/openapi/contract/impl/OpenAPIContractImpl.java @@ -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; @@ -65,13 +66,17 @@ public class OpenAPIContractImpl implements OpenAPIContract { private final Map 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) @@ -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 sortedPaths = applyMountOrder(unsortedPaths); diff --git a/src/main/java/io/vertx/openapi/contract/impl/OperationImpl.java b/src/main/java/io/vertx/openapi/contract/impl/OperationImpl.java index 1c86a05b..36e0f815 100644 --- a/src/main/java/io/vertx/openapi/contract/impl/OperationImpl.java +++ b/src/main/java/io/vertx/openapi/contract/impl/OperationImpl.java @@ -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; @@ -67,7 +68,7 @@ public class OperationImpl implements Operation { public OperationImpl(String absolutePath, String path, HttpMethod method, JsonObject operationModel, List pathParameters, Map pathExtensions, - List globalSecReq) { + List globalSecReq, MediaTypeRegistry registry) { this.absolutePath = absolutePath; this.operationId = operationModel.getString(KEY_OPERATION_ID); this.method = method; @@ -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); @@ -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 @@ -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 diff --git a/src/main/java/io/vertx/openapi/contract/impl/PathImpl.java b/src/main/java/io/vertx/openapi/contract/impl/PathImpl.java index 802ce572..4bdbfe7c 100644 --- a/src/main/java/io/vertx/openapi/contract/impl/PathImpl.java +++ b/src/main/java/io/vertx/openapi/contract/impl/PathImpl.java @@ -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; @@ -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 globalSecReq) { + public PathImpl(String basePath, String name, JsonObject pathModel, List globalSecReq, + MediaTypeRegistry registry) { this.absolutePath = (basePath.endsWith("/") ? basePath.substring(0, basePath.length() - 1) : basePath) + name; this.pathModel = pathModel; if (name.contains("*")) { @@ -82,7 +84,7 @@ public PathImpl(String basePath, String name, JsonObject pathModel, List 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); } diff --git a/src/main/java/io/vertx/openapi/contract/impl/RequestBodyImpl.java b/src/main/java/io/vertx/openapi/contract/impl/RequestBodyImpl.java index d125ea77..12d1e0c3 100644 --- a/src/main/java/io/vertx/openapi/contract/impl/RequestBodyImpl.java +++ b/src/main/java/io/vertx/openapi/contract/impl/RequestBodyImpl.java @@ -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; @@ -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 { @@ -37,7 +37,7 @@ public class RequestBodyImpl implements RequestBody { private final Map 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); @@ -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 = diff --git a/src/main/java/io/vertx/openapi/contract/impl/ResponseImpl.java b/src/main/java/io/vertx/openapi/contract/impl/ResponseImpl.java index 3ed58fb5..26d9f42a 100644 --- a/src/main/java/io/vertx/openapi/contract/impl/ResponseImpl.java +++ b/src/main/java/io/vertx/openapi/contract/impl/ResponseImpl.java @@ -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; @@ -29,6 +27,8 @@ import io.vertx.openapi.contract.MediaType; import io.vertx.openapi.contract.Parameter; import io.vertx.openapi.contract.Response; +import io.vertx.openapi.mediatype.MediaTypeRegistration; +import io.vertx.openapi.mediatype.MediaTypeRegistry; import java.util.List; import java.util.Map; import java.util.function.Predicate; @@ -45,7 +45,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); @@ -66,12 +66,19 @@ public ResponseImpl(JsonObject responseModel, String operationId) { .fieldNames() .stream() .filter(JsonSchema.EXCLUDE_ANNOTATIONS) - .collect(toMap(identity(), key -> new MediaTypeImpl(key, contentObject.getJsonObject(key))))); - - if (content.keySet().stream().anyMatch(type -> !isMediaTypeSupported(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))); - } + .filter(mediaTypeIdentifier -> { + if (registry.isSupported(mediaTypeIdentifier)) { + return true; + } + String msgTemplate = "Operation %s defines a response with an unsupported media type. Supported: %s"; + throw createUnsupportedFeature( + String.format(msgTemplate, operationId, join(", ", registry.supportedTypes()))); + }) + .collect(toMap(identity(), key -> { + // Can't be null, otherwise isSupported would have returned false + MediaTypeRegistration registration = registry.get(key); + return new MediaTypeImpl(key, contentObject.getJsonObject(key), registration); + }))); } @Override diff --git a/src/main/java/io/vertx/openapi/contract/impl/VendorSpecificJson.java b/src/main/java/io/vertx/openapi/contract/impl/VendorSpecificJson.java index d3f19443..17f9f17e 100644 --- a/src/main/java/io/vertx/openapi/contract/impl/VendorSpecificJson.java +++ b/src/main/java/io/vertx/openapi/contract/impl/VendorSpecificJson.java @@ -17,9 +17,10 @@ /** * Class to expose specific check about a media type being a vendor specific JSON */ +@Deprecated public class VendorSpecificJson { - private static final Pattern VENDOR_SPECIFIC_JSON = Pattern.compile("^[^/]+/vnd\\.[\\w.-]+\\+json$"); + public static final Pattern VENDOR_SPECIFIC_JSON = Pattern.compile("^[^/]+/vnd\\.[\\w.-]+\\+json$"); public static boolean matches(String type) { return type != null && VENDOR_SPECIFIC_JSON.matcher(type).matches(); diff --git a/src/main/java/io/vertx/openapi/mediatype/ContentAnalyser.java b/src/main/java/io/vertx/openapi/mediatype/ContentAnalyser.java new file mode 100644 index 00000000..84ee4f6a --- /dev/null +++ b/src/main/java/io/vertx/openapi/mediatype/ContentAnalyser.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025, SAP SE + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.openapi.mediatype; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.openapi.validation.ValidatorException; + +/** + * The content analyser is responsible to check if a request or response has the correct format i.e. is syntactically + * correct, and to transform the content-buffer into a representation that can be validated against a schema. + *

+ * These two methods are intentionally bundled in {@link ContentAnalyser} to prevent some operations from having to + * be performed twice. This is particularly helpful if a library is used that cannot distinguish between these steps. + * In this case, an intermediate result that was generated in {@link #checkSyntacticalCorrectness()}, for example, + * can be reused. + *

+ * Therefore, it is very important to ensure that the {@link #checkSyntacticalCorrectness()} method is always called + * before. + */ +@VertxGen +public interface ContentAnalyser { + /** + * Checks if the content has the expected format i.e. is syntactically correct. + *

+ * Throws a {@link ValidatorException} if the content is syntactically incorrect. + */ + void checkSyntacticalCorrectness() throws ValidatorException; + + /** + * Transforms the content into a format that can be validated by the + * {@link io.vertx.openapi.validation.RequestValidator}, or {@link io.vertx.openapi.validation.ResponseValidator}. + *

+ * Throws a {@link ValidatorException} if the content can't be transformed. + * + * @return the transformed content. + */ + Object transform(); +} diff --git a/src/main/java/io/vertx/openapi/mediatype/ContentAnalyserFactory.java b/src/main/java/io/vertx/openapi/mediatype/ContentAnalyserFactory.java new file mode 100644 index 00000000..230b5edf --- /dev/null +++ b/src/main/java/io/vertx/openapi/mediatype/ContentAnalyserFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025, Lukas Jelonek + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.openapi.mediatype; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.buffer.Buffer; +import io.vertx.openapi.mediatype.impl.ApplicationJsonAnalyser; +import io.vertx.openapi.mediatype.impl.MultipartFormAnalyser; +import io.vertx.openapi.mediatype.impl.NoOpAnalyser; +import io.vertx.openapi.validation.ValidationContext; + +@VertxGen +public interface ContentAnalyserFactory { + + /** + * Creates a new ContentAnalyser for the provided request. + * + * @param contentType The raw content type from the http headers. + * @param content The content of the request or response. + * @param context Whether the analyser is for a request or response. + * @return A fresh content analyser instance. + */ + ContentAnalyser create(String contentType, Buffer content, ValidationContext context); + + static ContentAnalyserFactory json() { + return ApplicationJsonAnalyser::new; + } + + static ContentAnalyserFactory noop() { + return NoOpAnalyser::new; + } + + static ContentAnalyserFactory multipart() { + return MultipartFormAnalyser::new; + } +} diff --git a/src/main/java/io/vertx/openapi/mediatype/MediaTypeInfo.java b/src/main/java/io/vertx/openapi/mediatype/MediaTypeInfo.java new file mode 100644 index 00000000..6addef54 --- /dev/null +++ b/src/main/java/io/vertx/openapi/mediatype/MediaTypeInfo.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2025, Lukas Jelonek + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.openapi.mediatype; + +import io.vertx.codegen.annotations.Nullable; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents a media type string and provides simple access to the type, the suffix and the parameters. + */ +public class MediaTypeInfo { + + private final String type; + private final String subtype; + private final String suffix; + private final Map parameters; + + public MediaTypeInfo(String type, String subtype, @Nullable String suffix, Map parameters) { + this.type = type; + this.subtype = subtype; + this.suffix = suffix; + this.parameters = parameters; + } + + public static MediaTypeInfo of(String mediaType) { + var type = new StringBuilder(); + var subtype = new StringBuilder(); + var suffix = new StringBuilder(); + + var parameters = new LinkedHashMap(); + var paramKey = new StringBuilder(); + var paramValue = new StringBuilder(); + + var mode = 0; // 0: type, 1: subtype, 2: suffix, 3: parameter key, 4: parameter value + for (int i = 0; i < mediaType.length(); i++) { + var c = mediaType.charAt(i); + if (c == '/') + mode = 1; + else if (c == '+') + mode = 2; + else if (c == ';') { + mode = 3; + if (paramKey.length() > 0) { + parameters.put(paramKey.toString().trim(), paramValue.toString().trim()); + paramKey = new StringBuilder(); + paramValue = new StringBuilder(); + } + } else if (c == '=') + mode = 4; + else + switch (mode) { + case 0: + type.append(c); + break; + case 1: + subtype.append(c); + break; + case 2: + suffix.append(c); + break; + case 3: + paramKey.append(c); + break; + case 4: + paramValue.append(c); + break; + default: + throw new IllegalStateException("Should not happen"); + } + } + if (paramKey.length() > 0) { + parameters.put(paramKey.toString().trim(), paramValue.toString().trim()); + } + return new MediaTypeInfo( + type.toString(), + subtype.toString(), + suffix.length() == 0 ? null : suffix.toString().trim(), + parameters); + } + + public String type() { + return type; + } + + public String subtype() { + return subtype; + } + + /** + * Get the type including suffix if available. + * + * @return type including suffix if available + */ + public String fullType() { + var sb = new StringBuilder(type).append("/").append(subtype); + suffix().ifPresent(s -> sb.append("+").append(s)); + return sb.toString(); + } + + public Map parameters() { + return parameters; + } + + public Optional suffix() { + return Optional.ofNullable(suffix); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) + return false; + MediaTypeInfo mediaType = (MediaTypeInfo) o; + return Objects.equals(type, mediaType.type) && Objects.equals(suffix, mediaType.suffix) + && Objects.equals(parameters, mediaType.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(type, suffix, parameters); + } + + @Override + public String toString() { + var sb = new StringBuilder(fullType()); + if (!parameters.isEmpty()) { + for (var e : parameters.entrySet()) { + sb.append("; ").append(e.getKey()).append("=").append(e.getValue()); + } + } + return sb.toString(); + } + + /** + * Checks if the other mediatype is compatible to this mediatype. The other mediatype is compatible when it is equal + * or more specific than this mediatype, e.g. application/vnd.example+json is more specific than application/vnd.example + * and thus compatible to it, whereas the other way around they are not compatible. + * + * @param other The other mediatype + * @return true if it is compatible, false otherwise + */ + public boolean doesInclude(MediaTypeInfo other) { + if (this.type.equals(other.type)) { + if (this.subtype.equals("*") || this.subtype.equals(other.subtype)) { + if (this.suffix == null || Objects.equals(this.suffix, other.suffix)) { + for (var e : this.parameters.entrySet()) { + if (!(other.parameters.containsKey(e.getKey()) + && Objects.equals(e.getValue(), other.parameters.get(e.getKey())))) { + // does not include the same parameter, so we fail + return false; + } + } + // every check was okay so far. They are compatible + return true; + } + } + } + return false; + } +} diff --git a/src/main/java/io/vertx/openapi/mediatype/MediaTypePredicate.java b/src/main/java/io/vertx/openapi/mediatype/MediaTypePredicate.java new file mode 100644 index 00000000..e02adfdb --- /dev/null +++ b/src/main/java/io/vertx/openapi/mediatype/MediaTypePredicate.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025, Lukas Jelonek + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.openapi.mediatype; + +import java.util.List; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public interface MediaTypePredicate extends Predicate { + + /** + * This method is intended for reporting of supported media types in the system. + * + * @return The list of supported types. + */ + List supportedTypes(); + + /** + * Factory for a predicate that accepts a list of types. Checks if the mediatype is equal to one of the types + * provided. Only checks the type/subtype+suffix. Ignores the attributes. + * + * @param types The types to accept + * @return The predicate that checks if the mediatype is part of the provided list. + */ + static MediaTypePredicate ofExactTypes(String... types) { + var list = List.of(types); + return new MediaTypePredicate() { + @Override + public List supportedTypes() { + return list; + } + + @Override + public boolean test(MediaTypeInfo s) { + return list.stream().anyMatch(x -> x.equals(s.fullType())); + } + }; + } + + /** + * Factory for a predicate that accepts types based on a regular expression. Only checks the type/subtype+suffix. + * Ignores the attributes. + * + * @param regexp The regular expression + * @return A predicate that checks if the mediatype matches the regular expression. + */ + static MediaTypePredicate ofRegexp(String regexp) { + var pattern = Pattern.compile(regexp); + + return new MediaTypePredicate() { + @Override + public List supportedTypes() { + return List.of(regexp); + } + + @Override + public boolean test(MediaTypeInfo s) { + return pattern.matcher(s.fullType()).matches(); + } + }; + } +} diff --git a/src/main/java/io/vertx/openapi/mediatype/MediaTypeRegistration.java b/src/main/java/io/vertx/openapi/mediatype/MediaTypeRegistration.java new file mode 100644 index 00000000..8ffbc5e8 --- /dev/null +++ b/src/main/java/io/vertx/openapi/mediatype/MediaTypeRegistration.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025, Lukas Jelonek + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.openapi.mediatype; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.buffer.Buffer; +import io.vertx.openapi.mediatype.impl.DefaultMediaTypeRegistration; +import io.vertx.openapi.validation.ValidationContext; +import java.util.List; +import java.util.regex.Pattern; + +/** + * A MediaTypeRegistration is used to register mediatypes to the openapi mediatype registry. It consists of a predicate + * that checks if a media type can be handled by the created content analysers. + */ +@VertxGen +public interface MediaTypeRegistration { + + MediaTypeRegistration APPLICATION_JSON = new DefaultMediaTypeRegistration( + MediaTypePredicate.ofExactTypes( + DefaultMediaTypeRegistration.APPLICATION_JSON, + DefaultMediaTypeRegistration.APPLICATION_JSON_UTF8, + DefaultMediaTypeRegistration.APPLICATION_HAL_JSON), + ContentAnalyserFactory.json()); + + MediaTypeRegistration MULTIPART_FORM_DATA = new DefaultMediaTypeRegistration( + MediaTypePredicate.ofExactTypes(DefaultMediaTypeRegistration.MULTIPART_FORM_DATA), + ContentAnalyserFactory.multipart()); + + MediaTypeRegistration TEXT_PLAIN = new DefaultMediaTypeRegistration( + MediaTypePredicate.ofExactTypes( + DefaultMediaTypeRegistration.TEXT_PLAIN, + DefaultMediaTypeRegistration.TEXT_PLAIN_UTF8), + ContentAnalyserFactory.noop()); + + MediaTypeRegistration APPLICATION_OCTET_STREAM = new DefaultMediaTypeRegistration( + MediaTypePredicate.ofExactTypes(DefaultMediaTypeRegistration.APPLICATION_OCTET_STREAM), + ContentAnalyserFactory.noop()); + + MediaTypeRegistration VENDOR_SPECIFIC_JSON = new DefaultMediaTypeRegistration( + MediaTypePredicate.ofRegexp(Pattern.compile("^[^/]+/vnd\\.[\\w.-]+\\+json$").pattern()), + ContentAnalyserFactory.json()); + + /** + * Creates a new {@link ContentAnalyser}. This is required, because {@link ContentAnalyser} could be stateful. + * + * @param contentType The raw content type from the http headers. + * @param content The content of the request or response. + * @param context Whether the analyser is for a request or response. + * @return A fresh content analyser instance. + */ + ContentAnalyser createContentAnalyser(String contentType, Buffer content, ValidationContext context); + + /** + * Checks if this registration can handle the given media type. This method is intended to be used by the + * MediaTypeRegistry. + * + * @param mediaType The media type to check + * @return true if the mediatype can be handled, false otherwise + */ + boolean canHandle(String mediaType); + + /** + * This method is intended for reporting of supported media types in the system. + * + * @return The list of supported types. + */ + List supportedTypes(); +} diff --git a/src/main/java/io/vertx/openapi/mediatype/MediaTypeRegistry.java b/src/main/java/io/vertx/openapi/mediatype/MediaTypeRegistry.java new file mode 100644 index 00000000..cc837c6b --- /dev/null +++ b/src/main/java/io/vertx/openapi/mediatype/MediaTypeRegistry.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, Lukas Jelonek + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.openapi.mediatype; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.openapi.mediatype.impl.DefaultMediaTypeRegistry; +import java.util.List; + +/** + * The MediaTypeRegistry contains all supported MediaTypes and Validators for the mediatypes. New MediaTypes can be registered + * by providing new MediaTypeRegistrations. + */ +@VertxGen +public interface MediaTypeRegistry { + /** + * Creates a default registry with application/json, application/multipart and text/plain mediatypes registered. + * + * @return A registry with default options. + */ + static MediaTypeRegistry createDefault() { + return new DefaultMediaTypeRegistry() + .register(MediaTypeRegistration.APPLICATION_JSON) + .register(MediaTypeRegistration.MULTIPART_FORM_DATA) + .register(MediaTypeRegistration.APPLICATION_OCTET_STREAM) + .register(MediaTypeRegistration.TEXT_PLAIN) + .register(MediaTypeRegistration.VENDOR_SPECIFIC_JSON); + } + + /** + * Creates an empty registry. + * + * @return An empty registry. + */ + static MediaTypeRegistry createEmpty() { + return new DefaultMediaTypeRegistry(); + } + + /** + * Registers a new MediaTypeHandler + * + * @param registration The mediatype registration. + * @return This registry for a fluent interface. + */ + MediaTypeRegistry register(MediaTypeRegistration registration); + + /** + * Checks if the provided media type is supported by the registration + * + * @param type The raw mediatype string + * @return true if it supported, false otherwise + */ + default boolean isSupported(String type) { + return get(type) != null; + } + + /** + * @return A list of all supported types. + */ + List supportedTypes(); + + /** + * Finds the registration for the provided media type. + * @param mediaType The media type to find the registration for. + * @return The registration if found, null otherwise. + */ + MediaTypeRegistration get(String mediaType); +} diff --git a/src/main/java/io/vertx/openapi/mediatype/impl/AbstractContentAnalyser.java b/src/main/java/io/vertx/openapi/mediatype/impl/AbstractContentAnalyser.java new file mode 100644 index 00000000..406aa030 --- /dev/null +++ b/src/main/java/io/vertx/openapi/mediatype/impl/AbstractContentAnalyser.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024, SAP SE + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.openapi.mediatype.impl; + +import static io.vertx.openapi.validation.ValidatorErrorType.ILLEGAL_VALUE; + +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; +import io.vertx.openapi.mediatype.ContentAnalyser; +import io.vertx.openapi.validation.ValidationContext; +import io.vertx.openapi.validation.ValidatorException; + +/** + * Base class for your own analysers. Provides access to some useful helper methods. + */ +public abstract class AbstractContentAnalyser implements ContentAnalyser { + + protected final String contentType; + protected final Buffer content; + protected final ValidationContext requestOrResponse; + + /** + * Creates a new content analyser. + * + * @param contentType the content type. + * @param content the content to be analysed. + * @param context the context in which the content is used. + */ + public AbstractContentAnalyser(String contentType, Buffer content, ValidationContext context) { + this.contentType = contentType; + this.content = content; + this.requestOrResponse = context; + } + + /** + * Builds a {@link ValidatorException} for the case that the content is syntactically incorrect. + * + * @param message the error message. + * @return the {@link ValidatorException}. + */ + protected static ValidatorException buildSyntaxException(String message) { + return new ValidatorException(message, ILLEGAL_VALUE); + } + + /** + * Decodes the passed content as JSON. + * + * @return an object representing the passed JSON content. + * @throws ValidatorException if the content can't be decoded. + */ + protected static Object decodeJsonContent(Buffer content, ValidationContext requestOrResponse) { + try { + return Json.decodeValue(content); + } catch (DecodeException e) { + throw buildSyntaxException("The " + requestOrResponse + " body can't be decoded"); + } + } +} diff --git a/src/main/java/io/vertx/openapi/validation/analyser/ApplicationJsonAnalyser.java b/src/main/java/io/vertx/openapi/mediatype/impl/ApplicationJsonAnalyser.java similarity index 88% rename from src/main/java/io/vertx/openapi/validation/analyser/ApplicationJsonAnalyser.java rename to src/main/java/io/vertx/openapi/mediatype/impl/ApplicationJsonAnalyser.java index d2f47a3f..1603bb3b 100644 --- a/src/main/java/io/vertx/openapi/validation/analyser/ApplicationJsonAnalyser.java +++ b/src/main/java/io/vertx/openapi/mediatype/impl/ApplicationJsonAnalyser.java @@ -10,12 +10,12 @@ * */ -package io.vertx.openapi.validation.analyser; +package io.vertx.openapi.mediatype.impl; import io.vertx.core.buffer.Buffer; import io.vertx.openapi.validation.ValidationContext; -public class ApplicationJsonAnalyser extends ContentAnalyser { +public class ApplicationJsonAnalyser extends AbstractContentAnalyser { private Object decodedValue; public ApplicationJsonAnalyser(String contentType, Buffer content, ValidationContext context) { diff --git a/src/main/java/io/vertx/openapi/mediatype/impl/DefaultMediaTypeRegistration.java b/src/main/java/io/vertx/openapi/mediatype/impl/DefaultMediaTypeRegistration.java new file mode 100644 index 00000000..0469323f --- /dev/null +++ b/src/main/java/io/vertx/openapi/mediatype/impl/DefaultMediaTypeRegistration.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025, SAP SE + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.openapi.mediatype.impl; + +import io.vertx.core.buffer.Buffer; +import io.vertx.openapi.mediatype.ContentAnalyser; +import io.vertx.openapi.mediatype.ContentAnalyserFactory; +import io.vertx.openapi.mediatype.MediaTypeInfo; +import io.vertx.openapi.mediatype.MediaTypePredicate; +import io.vertx.openapi.mediatype.MediaTypeRegistration; +import io.vertx.openapi.validation.ValidationContext; +import java.util.List; + +public class DefaultMediaTypeRegistration implements MediaTypeRegistration { + public static final String APPLICATION_JSON = "application/json"; + public static final String APPLICATION_JSON_UTF8 = APPLICATION_JSON + "; charset=utf-8"; + public static final String MULTIPART_FORM_DATA = "multipart/form-data"; + public static final String APPLICATION_HAL_JSON = "application/hal+json"; + public static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; + public static final String TEXT_PLAIN = "text/plain"; + public static final String TEXT_PLAIN_UTF8 = TEXT_PLAIN + "; charset=utf-8"; + + private final MediaTypePredicate canHandleMediaType; + private final ContentAnalyserFactory contentAnalyserFactory; + + /** + * Creates a new registration from the provided predicate and ContentAnalyserFactory. + * + * @param canHandleMediaType A predicate to check if the mediatype can be handled + * @return The registration object + */ + public DefaultMediaTypeRegistration(MediaTypePredicate canHandleMediaType, + ContentAnalyserFactory contentAnalyserFactory) { + this.canHandleMediaType = canHandleMediaType; + this.contentAnalyserFactory = contentAnalyserFactory; + } + + @Override + public ContentAnalyser createContentAnalyser(String contentType, Buffer content, ValidationContext context) { + return contentAnalyserFactory.create(contentType, content, context); + } + + @Override + public boolean canHandle(String mediaType) { + return canHandleMediaType.test(MediaTypeInfo.of(mediaType)); + } + + @Override + public List supportedTypes() { + return canHandleMediaType.supportedTypes(); + } +} diff --git a/src/main/java/io/vertx/openapi/mediatype/impl/DefaultMediaTypeRegistry.java b/src/main/java/io/vertx/openapi/mediatype/impl/DefaultMediaTypeRegistry.java new file mode 100644 index 00000000..c1c87efa --- /dev/null +++ b/src/main/java/io/vertx/openapi/mediatype/impl/DefaultMediaTypeRegistry.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025, Lukas Jelonek + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.openapi.mediatype.impl; + +import io.vertx.openapi.mediatype.MediaTypeRegistration; +import io.vertx.openapi.mediatype.MediaTypeRegistry; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Default implementation for MediaTypeRegistry + */ +public class DefaultMediaTypeRegistry implements MediaTypeRegistry { + private final List registrations = new ArrayList<>(); + + @Override + public DefaultMediaTypeRegistry register(MediaTypeRegistration registration) { + this.registrations.add(registration); + return this; + } + + @Override + public List supportedTypes() { + return registrations.stream().flatMap(x -> x.supportedTypes().stream()).collect(Collectors.toList()); + } + + @Override + public MediaTypeRegistration get(String mediaType) { + return registrations.stream().filter(x -> x.canHandle(mediaType)).findFirst().orElse(null); + } +} diff --git a/src/main/java/io/vertx/openapi/validation/analyser/MultipartFormAnalyser.java b/src/main/java/io/vertx/openapi/mediatype/impl/MultipartFormAnalyser.java similarity index 97% rename from src/main/java/io/vertx/openapi/validation/analyser/MultipartFormAnalyser.java rename to src/main/java/io/vertx/openapi/mediatype/impl/MultipartFormAnalyser.java index b2874908..b3f2731c 100644 --- a/src/main/java/io/vertx/openapi/validation/analyser/MultipartFormAnalyser.java +++ b/src/main/java/io/vertx/openapi/mediatype/impl/MultipartFormAnalyser.java @@ -10,7 +10,7 @@ * */ -package io.vertx.openapi.validation.analyser; +package io.vertx.openapi.mediatype.impl; import static io.netty.handler.codec.http.HttpHeaderValues.MULTIPART_FORM_DATA; import static io.vertx.openapi.validation.ValidatorErrorType.MISSING_REQUIRED_PARAMETER; @@ -24,7 +24,7 @@ import io.vertx.openapi.validation.ValidatorException; import java.util.List; -public class MultipartFormAnalyser extends ContentAnalyser { +public class MultipartFormAnalyser extends AbstractContentAnalyser { private static final String BOUNDARY = "boundary="; private List parts; diff --git a/src/main/java/io/vertx/openapi/validation/analyser/MultipartPart.java b/src/main/java/io/vertx/openapi/mediatype/impl/MultipartPart.java similarity index 98% rename from src/main/java/io/vertx/openapi/validation/analyser/MultipartPart.java rename to src/main/java/io/vertx/openapi/mediatype/impl/MultipartPart.java index 32c96fde..73322eee 100644 --- a/src/main/java/io/vertx/openapi/validation/analyser/MultipartPart.java +++ b/src/main/java/io/vertx/openapi/mediatype/impl/MultipartPart.java @@ -10,7 +10,7 @@ * */ -package io.vertx.openapi.validation.analyser; +package io.vertx.openapi.mediatype.impl; import static io.vertx.openapi.validation.ValidatorErrorType.INVALID_VALUE; import static java.util.regex.Pattern.CASE_INSENSITIVE; diff --git a/src/main/java/io/vertx/openapi/mediatype/impl/NoOpAnalyser.java b/src/main/java/io/vertx/openapi/mediatype/impl/NoOpAnalyser.java new file mode 100644 index 00000000..ae53ae48 --- /dev/null +++ b/src/main/java/io/vertx/openapi/mediatype/impl/NoOpAnalyser.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025, Lukas Jelonek + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.openapi.mediatype.impl; + +import io.vertx.core.buffer.Buffer; +import io.vertx.openapi.validation.ValidationContext; + +public class NoOpAnalyser extends AbstractContentAnalyser { + public NoOpAnalyser(String contentType, Buffer content, ValidationContext context) { + super(contentType, content, context); + } + + @Override + public void checkSyntacticalCorrectness() { + // no syntax check + } + + @Override + public Object transform() { + return content; + } +} diff --git a/src/main/java/io/vertx/openapi/validation/analyser/ContentAnalyser.java b/src/main/java/io/vertx/openapi/validation/analyser/ContentAnalyser.java deleted file mode 100644 index 60781d2d..00000000 --- a/src/main/java/io/vertx/openapi/validation/analyser/ContentAnalyser.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (c) 2024, SAP SE - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 - * which is available at https://www.apache.org/licenses/LICENSE-2.0. - * - * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 - * - */ - -package io.vertx.openapi.validation.analyser; - -import static io.vertx.openapi.validation.ValidatorErrorType.ILLEGAL_VALUE; - -import io.vertx.core.buffer.Buffer; -import io.vertx.core.json.DecodeException; -import io.vertx.core.json.Json; -import io.vertx.openapi.contract.MediaType; -import io.vertx.openapi.validation.ValidationContext; -import io.vertx.openapi.validation.ValidatorException; - -/** - * The content analyser is responsible for checking if the content is syntactically correct, and transforming the - * content. - *

- * These two methods are intentionally bundled in {@link ContentAnalyser} to prevent some operations from having to - * be performed twice. This is particularly helpful if a library is used that cannot distinguish between these steps. - * In this case, an intermediate result that was generated in {@link #checkSyntacticalCorrectness()}, for example, - * can be reused. - *

- * Therefore, it is very important to ensure that the {@link #checkSyntacticalCorrectness()} method is always called - * before. - */ -public abstract class ContentAnalyser { - private static class NoOpAnalyser extends ContentAnalyser { - public NoOpAnalyser(String contentType, Buffer content, ValidationContext context) { - super(contentType, content, context); - } - - @Override - public void checkSyntacticalCorrectness() { - // no syntax check - } - - @Override - public Object transform() { - return content; - } - } - - /** - * Returns the content analyser for the given content type. - * - * @param mediaType the media type to determine the content analyser. - * @param contentType the raw content type value from the HTTP header field. - * @param content the content to be analysed. - * @return the content analyser for the given content type. - */ - public static ContentAnalyser getContentAnalyser(MediaType mediaType, String contentType, Buffer content, - ValidationContext context) { - switch (mediaType.getIdentifier()) { - case MediaType.APPLICATION_JSON: - case MediaType.APPLICATION_JSON_UTF8: - case MediaType.APPLICATION_HAL_JSON: - return new ApplicationJsonAnalyser(contentType, content, context); - case MediaType.MULTIPART_FORM_DATA: - return new MultipartFormAnalyser(contentType, content, context); - case MediaType.APPLICATION_OCTET_STREAM: - case MediaType.TEXT_PLAIN: - case MediaType.TEXT_PLAIN_UTF8: - return new NoOpAnalyser(contentType, content, context); - default: - if (MediaType.isVendorSpecificJson(contentType)) { - return new ApplicationJsonAnalyser(contentType, content, context); - } - return null; - } - } - - protected final String contentType; - protected final Buffer content; - protected final ValidationContext requestOrResponse; - - /** - * Creates a new content analyser. - * - * @param contentType the content type. - * @param content the content to be analysed. - * @param context the context in which the content is used. - */ - public ContentAnalyser(String contentType, Buffer content, ValidationContext context) { - this.contentType = contentType; - this.content = content; - this.requestOrResponse = context; - } - - /** - * Checks if the content is syntactically correct. - *

- * Throws a {@link ValidatorException} if the content is syntactically incorrect. - */ - public abstract void checkSyntacticalCorrectness(); - - /** - * Transforms the content into a format that can be validated by the - * {@link io.vertx.openapi.validation.RequestValidator}, or {@link io.vertx.openapi.validation.ResponseValidator}. - *

- * Throws a {@link ValidatorException} if the content can't be transformed. - * - * @return the transformed content. - */ - public abstract Object transform(); - - /** - * Builds a {@link ValidatorException} for the case that the content is syntactically incorrect. - * - * @param message the error message. - * @return the {@link ValidatorException}. - */ - protected static ValidatorException buildSyntaxException(String message) { - return new ValidatorException(message, ILLEGAL_VALUE); - } - - /** - * Decodes the passed content as JSON. - * - * @return an object representing the passed JSON content. - * @throws ValidatorException if the content can't be decoded. - */ - protected static Object decodeJsonContent(Buffer content, ValidationContext requestOrResponse) { - try { - return Json.decodeValue(content); - } catch (DecodeException e) { - throw buildSyntaxException("The " + requestOrResponse + " body can't be decoded"); - } - } -} diff --git a/src/main/java/io/vertx/openapi/validation/impl/BaseValidator.java b/src/main/java/io/vertx/openapi/validation/impl/BaseValidator.java index b9bdfa1c..32b51f4c 100644 --- a/src/main/java/io/vertx/openapi/validation/impl/BaseValidator.java +++ b/src/main/java/io/vertx/openapi/validation/impl/BaseValidator.java @@ -26,9 +26,11 @@ import io.vertx.openapi.contract.MediaType; import io.vertx.openapi.contract.OpenAPIContract; import io.vertx.openapi.contract.Operation; +import io.vertx.openapi.contract.impl.MediaTypeImpl; +import io.vertx.openapi.mediatype.ContentAnalyser; import io.vertx.openapi.validation.ValidationContext; import io.vertx.openapi.validation.ValidatorException; -import io.vertx.openapi.validation.analyser.ContentAnalyser; +import java.util.Optional; public class BaseValidator { protected final Vertx vertx; @@ -67,8 +69,11 @@ protected boolean isSchemaValidationRequired(MediaType mediaType) { protected RequestParameterImpl validate(MediaType mediaType, String contentType, Buffer rawContent, ValidationContext requestOrResponse) { - ContentAnalyser contentAnalyser = mediaType == null ? null - : ContentAnalyser.getContentAnalyser(mediaType, contentType, rawContent, requestOrResponse); + + MediaTypeImpl mediaTypeImpl = (MediaTypeImpl) mediaType; + ContentAnalyser contentAnalyser = Optional.ofNullable(mediaTypeImpl).map(MediaTypeImpl::getRegistration) + .map(r -> r.createContentAnalyser(contentType, rawContent, requestOrResponse)) + .orElse(null); if (contentAnalyser == null) { throw new ValidatorException("The format of the " + requestOrResponse + " body is not supported", diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index e35fe18e..52c1143d 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -13,11 +13,13 @@ exports io.vertx.openapi.contract; exports io.vertx.openapi.validation; exports io.vertx.openapi.validation.transformer; + exports io.vertx.openapi.mediatype; exports io.vertx.openapi.impl to io.vertx.tests; exports io.vertx.openapi.validation.impl to io.vertx.tests; - exports io.vertx.openapi.validation.analyser to io.vertx.tests; exports io.vertx.openapi.contract.impl to io.vertx.tests; opens io.vertx.openapi.validation.impl to io.vertx.tests; + exports io.vertx.openapi.mediatype.impl; + } diff --git a/src/test/java/io/vertx/tests/contract/OpenAPIContractBuilderTest.java b/src/test/java/io/vertx/tests/contract/OpenAPIContractBuilderTest.java index 8d0122ed..efb260b7 100644 --- a/src/test/java/io/vertx/tests/contract/OpenAPIContractBuilderTest.java +++ b/src/test/java/io/vertx/tests/contract/OpenAPIContractBuilderTest.java @@ -22,6 +22,10 @@ import io.vertx.openapi.contract.OpenAPIContractBuilder; import io.vertx.openapi.contract.OpenAPIContractException; import io.vertx.openapi.impl.Utils; +import io.vertx.openapi.mediatype.ContentAnalyserFactory; +import io.vertx.openapi.mediatype.MediaTypePredicate; +import io.vertx.openapi.mediatype.MediaTypeRegistry; +import io.vertx.openapi.mediatype.impl.DefaultMediaTypeRegistration; import io.vertx.tests.ResourceHelper; import java.nio.file.Path; import java.nio.file.Paths; @@ -235,4 +239,24 @@ void set_additional_contract_paths_should_replace_existing_contract_paths(Vertx } } + @Test + void registering_custom_mediatypes_should_succeed(Vertx vertx, VertxTestContext ctx) { + var path = getRelatedTestResourcePath(TestSetupOfAdditionalContractParts.class).resolve("builder") + .resolve("contract-with-unsupported-mediatype.yaml").toString(); + OpenAPIContract.builder(vertx) + .setContractPath(path) + .mediaTypeRegistry(MediaTypeRegistry.createDefault() + .register( + new DefaultMediaTypeRegistration( + MediaTypePredicate.ofExactTypes("text/tab-separated-values"), + ContentAnalyserFactory.json())) + .register( + new DefaultMediaTypeRegistration( + MediaTypePredicate + .ofExactTypes("application/vnd.openxmlformats-officedocument.wordprocessingml.document"), + ContentAnalyserFactory.json()))) + .build() + .onComplete(ctx.succeedingThenComplete()); + } + } diff --git a/src/test/java/io/vertx/tests/contract/impl/MediaTypeImplTest.java b/src/test/java/io/vertx/tests/contract/impl/MediaTypeImplTest.java index 3fe7cc4c..3103a098 100644 --- a/src/test/java/io/vertx/tests/contract/impl/MediaTypeImplTest.java +++ b/src/test/java/io/vertx/tests/contract/impl/MediaTypeImplTest.java @@ -20,9 +20,9 @@ import io.vertx.core.json.JsonObject; import io.vertx.openapi.contract.ContractErrorType; -import io.vertx.openapi.contract.MediaType; import io.vertx.openapi.contract.OpenAPIContractException; import io.vertx.openapi.contract.impl.MediaTypeImpl; +import io.vertx.openapi.mediatype.MediaTypeRegistration; import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -37,6 +37,7 @@ class MediaTypeImplTest { private static final String ABS_RECURSIVE_REF = "__absolute_recursive_ref__"; private static final String ABS_REF = "__absolute_ref__"; private static final String DUMMY_REF_VALUE = "dummy-ref-value"; + private static final MediaTypeRegistration MEDIA_TYPE_REGISTRATION = MediaTypeRegistration.APPLICATION_JSON; private static Stream testGetters() { var partialSchemaJson = JsonObject.of("schema", stringSchema().toJson()); @@ -71,7 +72,7 @@ private static Stream testGetters() { @ParameterizedTest(name = "{index} test getters for scenario: {0}") @MethodSource void testGetters(String scenario, JsonObject mediaTypeModel, List fieldNames) { - MediaType mediaType = new MediaTypeImpl(DUMMY_IDENTIFIER, mediaTypeModel); + MediaTypeImpl mediaType = new MediaTypeImpl(DUMMY_IDENTIFIER, mediaTypeModel, MEDIA_TYPE_REGISTRATION); assertThat(mediaType.getOpenAPIModel()).isEqualTo(mediaTypeModel); if (fieldNames.isEmpty()) { assertThat(mediaType.getSchema()).isNull(); @@ -79,6 +80,7 @@ void testGetters(String scenario, JsonObject mediaTypeModel, List fieldN assertThat(mediaType.getSchema().fieldNames()).containsExactlyElementsIn(fieldNames); } assertThat(mediaType.getIdentifier()).isEqualTo(DUMMY_IDENTIFIER); + assertThat(mediaType.getRegistration()).isNotNull(); } @Test @@ -86,19 +88,21 @@ void testExceptions() { String msg = "The passed OpenAPI contract contains a feature that is not supported: Media Type without a schema"; OpenAPIContractException exceptionNoModel = - assertThrows(OpenAPIContractException.class, () -> new MediaTypeImpl(DUMMY_IDENTIFIER, null)); + assertThrows(OpenAPIContractException.class, + () -> new MediaTypeImpl(DUMMY_IDENTIFIER, null, MEDIA_TYPE_REGISTRATION)); assertThat(exceptionNoModel.type()).isEqualTo(ContractErrorType.UNSUPPORTED_FEATURE); assertThat(exceptionNoModel).hasMessageThat().isEqualTo(msg); OpenAPIContractException exceptionSchemaNull = assertThrows(OpenAPIContractException.class, () -> new MediaTypeImpl(DUMMY_IDENTIFIER, - new JsonObject().putNull("schema"))); + new JsonObject().putNull("schema"), MEDIA_TYPE_REGISTRATION)); assertThat(exceptionSchemaNull.type()).isEqualTo(ContractErrorType.UNSUPPORTED_FEATURE); assertThat(exceptionSchemaNull).hasMessageThat().isEqualTo(msg); OpenAPIContractException exceptionSchemaEmpty = assertThrows(OpenAPIContractException.class, - () -> new MediaTypeImpl(DUMMY_IDENTIFIER, new JsonObject().put("schema", EMPTY_JSON_OBJECT))); + () -> new MediaTypeImpl(DUMMY_IDENTIFIER, new JsonObject().put("schema", EMPTY_JSON_OBJECT), + MEDIA_TYPE_REGISTRATION)); assertThat(exceptionSchemaEmpty.type()).isEqualTo(ContractErrorType.UNSUPPORTED_FEATURE); assertThat(exceptionSchemaEmpty).hasMessageThat().isEqualTo(msg); } diff --git a/src/test/java/io/vertx/tests/contract/impl/OpenAPIContractImplTest.java b/src/test/java/io/vertx/tests/contract/impl/OpenAPIContractImplTest.java index abf795b2..56f45655 100644 --- a/src/test/java/io/vertx/tests/contract/impl/OpenAPIContractImplTest.java +++ b/src/test/java/io/vertx/tests/contract/impl/OpenAPIContractImplTest.java @@ -29,6 +29,7 @@ import io.vertx.openapi.contract.Operation; import io.vertx.openapi.contract.impl.OpenAPIContractImpl; import io.vertx.openapi.contract.impl.PathImpl; +import io.vertx.openapi.mediatype.MediaTypeRegistry; import io.vertx.tests.ResourceHelper; import java.io.IOException; import java.nio.file.Files; @@ -53,32 +54,33 @@ class OpenAPIContractImplTest { private static final Path VALID_CONTRACTS_JSON = RESOURCE_PATH.resolve("contract_valid.json"); private static final List PATHS_UNSORTED = Arrays.asList( - new PathImpl(BASE_PATH, "/v2", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/{abc}/pets/{petId}", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/{abc}/{foo}/bar", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/pets/{petId}", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/v1/docs/docId", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/pets/petId", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/v1/docs/{docId}", EMPTY_JSON_OBJECT, emptyList())); + new PathImpl(BASE_PATH, "/v2", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/{abc}/pets/{petId}", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/{abc}/{foo}/bar", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/pets/{petId}", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/v1/docs/docId", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/pets/petId", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/v1/docs/{docId}", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault())); private static final List PATHS_SORTED = Arrays.asList( - new PathImpl(BASE_PATH, "/pets/petId", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/v1/docs/docId", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/v2", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/pets/{petId}", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/v1/docs/{docId}", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/{abc}/pets/{petId}", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/{abc}/{foo}/bar", EMPTY_JSON_OBJECT, emptyList())); + new PathImpl(BASE_PATH, "/pets/petId", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/v1/docs/docId", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/v2", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/pets/{petId}", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/v1/docs/{docId}", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/{abc}/pets/{petId}", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/{abc}/{foo}/bar", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault())); private static Stream testApplyMountOrderThrows() { return Stream.of( Arguments.of("a duplicate has been found", Arrays.asList( - new PathImpl(BASE_PATH, "/pets/{petId}", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/pets/{petId}", EMPTY_JSON_OBJECT, emptyList())), + new PathImpl(BASE_PATH, "/pets/{petId}", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/pets/{petId}", EMPTY_JSON_OBJECT, emptyList(), + MediaTypeRegistry.createDefault())), "Found Path duplicate: /pets/{petId}"), Arguments.of("paths with same hierarchy but different templated names has been found", Arrays.asList( - new PathImpl(BASE_PATH, "/pets/{petId}", EMPTY_JSON_OBJECT, emptyList()), - new PathImpl(BASE_PATH, "/pets/{foo}", EMPTY_JSON_OBJECT, emptyList())), + new PathImpl(BASE_PATH, "/pets/{petId}", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()), + new PathImpl(BASE_PATH, "/pets/{foo}", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault())), "Found Paths with same hierarchy but different templated names: /pets/{}")); } @@ -108,7 +110,7 @@ void testDifferentBasePaths() { JsonObject contract = new JsonObject().put("servers", new JsonArray().add(server1).add(server2)); OpenAPIContractException exception = - assertThrows(OpenAPIContractException.class, () -> new OpenAPIContractImpl(contract, null, null)); + assertThrows(OpenAPIContractException.class, () -> new OpenAPIContractImpl(contract, null, null, null)); String expectedMsg = "The passed OpenAPI contract contains a feature that is not supported: Different base paths in server urls"; assertThat(exception).hasMessageThat().isEqualTo(expectedMsg); @@ -120,10 +122,10 @@ void testBasePath() { JsonObject server1 = new JsonObject().put("url", "http://foo.bar/foo"); JsonObject contractJson = new JsonObject().put("servers", new JsonArray().add(server1)); - OpenAPIContractImpl contract = new OpenAPIContractImpl(contractJson, null, null); + OpenAPIContractImpl contract = new OpenAPIContractImpl(contractJson, null, null, null); assertThat(contract.basePath()).isEqualTo("/foo"); - OpenAPIContractImpl contractEmpty = new OpenAPIContractImpl(new JsonObject(), null, null); + OpenAPIContractImpl contractEmpty = new OpenAPIContractImpl(new JsonObject(), null, null, null); assertThat(contractEmpty.basePath()).isEqualTo(""); } @@ -133,7 +135,8 @@ void testGetters() throws IOException { Buffer.buffer(Files.readAllBytes(VALID_CONTRACTS_JSON)).toJsonObject().getJsonObject("0000_Test_Getters"); JsonObject resolvedSpec = testDataObject.getJsonObject("contractModel"); SchemaRepository schemaRepository = Mockito.mock(SchemaRepository.class); - OpenAPIContractImpl contract = new OpenAPIContractImpl(resolvedSpec, V3_1, schemaRepository); + OpenAPIContractImpl contract = + new OpenAPIContractImpl(resolvedSpec, V3_1, schemaRepository, MediaTypeRegistry.createDefault()); assertThat(contract.getServers()).hasSize(1); assertThat(contract.getServers().get(0).getURL()).isEqualTo("https://petstore.swagger.io/v1"); @@ -173,6 +176,6 @@ private static OpenAPIContractImpl fromTestData(String testId) throws IOExceptio Buffer.buffer(Files.readAllBytes(VALID_CONTRACTS_JSON)).toJsonObject().getJsonObject(testId); JsonObject resolvedSpec = testDataObject.getJsonObject("contractModel"); SchemaRepository schemaRepository = Mockito.mock(SchemaRepository.class); - return new OpenAPIContractImpl(resolvedSpec, V3_1, schemaRepository); + return new OpenAPIContractImpl(resolvedSpec, V3_1, schemaRepository, MediaTypeRegistry.createDefault()); } } diff --git a/src/test/java/io/vertx/tests/contract/impl/OperationImplTest.java b/src/test/java/io/vertx/tests/contract/impl/OperationImplTest.java index 2f59c330..570e14e6 100644 --- a/src/test/java/io/vertx/tests/contract/impl/OperationImplTest.java +++ b/src/test/java/io/vertx/tests/contract/impl/OperationImplTest.java @@ -37,6 +37,7 @@ import io.vertx.openapi.contract.RequestBody; import io.vertx.openapi.contract.impl.OperationImpl; import io.vertx.openapi.contract.impl.SecurityRequirementImpl; +import io.vertx.openapi.mediatype.MediaTypeRegistry; import io.vertx.tests.ResourceHelper; import java.nio.file.Path; import java.util.Arrays; @@ -85,7 +86,7 @@ private static OperationImpl fromTestData(String id, JsonObject testData, Securi JsonObject operationModel = testDataObject.getJsonObject("operationModel"); List pathParams = parseParameters(path, testDataObject.getJsonArray("pathParams", EMPTY_JSON_ARRAY)); return new OperationImpl("/absolute" + path, path, method, operationModel, pathParams, emptyMap(), - Arrays.asList(secReqs)); + Arrays.asList(secReqs), MediaTypeRegistry.createDefault()); } @ParameterizedTest(name = "{index} should throw an exception for scenario: {0}") @@ -175,7 +176,8 @@ private static Stream providePathExtensions() { void testMergeExtensions(String scenario, Map pathExtensions, Map expected) { JsonObject testDataObject = validTestData.getJsonObject("0005_Test_Merge_Extensions"); JsonObject operationModel = testDataObject.getJsonObject("operationModel"); - Operation op = new OperationImpl("/", "path", GET, operationModel, emptyList(), pathExtensions, emptyList()); + Operation op = new OperationImpl("/", "path", GET, operationModel, emptyList(), pathExtensions, emptyList(), + MediaTypeRegistry.createDefault()); assertThat(op.getExtensions()).containsExactlyEntriesIn(expected); } diff --git a/src/test/java/io/vertx/tests/contract/impl/PathImplTest.java b/src/test/java/io/vertx/tests/contract/impl/PathImplTest.java index a32fe8ce..6472d7df 100644 --- a/src/test/java/io/vertx/tests/contract/impl/PathImplTest.java +++ b/src/test/java/io/vertx/tests/contract/impl/PathImplTest.java @@ -24,6 +24,7 @@ import io.vertx.openapi.contract.OpenAPIContractException; import io.vertx.openapi.contract.Operation; import io.vertx.openapi.contract.impl.PathImpl; +import io.vertx.openapi.mediatype.MediaTypeRegistry; import io.vertx.tests.ResourceHelper; import java.nio.file.Path; import org.junit.jupiter.api.BeforeAll; @@ -49,7 +50,7 @@ private static PathImpl fromTestData(String id, JsonObject testData) { JsonObject testDataObject = testData.getJsonObject(id); String name = testDataObject.getString("name"); JsonObject pathModel = testDataObject.getJsonObject("pathModel"); - return new PathImpl(BASE_PATH, name, pathModel, emptyList()); + return new PathImpl(BASE_PATH, name, pathModel, emptyList(), MediaTypeRegistry.createDefault()); } @Test @@ -74,7 +75,7 @@ void testGetters() { void testWildcardInPath() { OpenAPIContractException exception = assertThrows(OpenAPIContractException.class, () -> new PathImpl(BASE_PATH, "/pets/*", EMPTY_JSON_OBJECT, - emptyList())); + emptyList(), MediaTypeRegistry.createDefault())); String expectedMsg = "The passed OpenAPI contract is invalid: Paths must not have a wildcard (asterisk): /pets/*"; assertThat(exception).hasMessageThat().isEqualTo(expectedMsg); } @@ -85,7 +86,7 @@ void testWildcardInPath() { void testWrongCurlyBracesInPath(String path) { OpenAPIContractException exception = assertThrows(OpenAPIContractException.class, - () -> new PathImpl(BASE_PATH, path, EMPTY_JSON_OBJECT, emptyList())); + () -> new PathImpl(BASE_PATH, path, EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault())); String expectedMsg = "The passed OpenAPI contract is invalid: Curly brace MUST be the first/last character in a path segment " + "(/{parameterName}/): " + path; @@ -101,19 +102,25 @@ void testValidCurlyBracesInPath(String path) { @Test void testCutTrailingSlash() { String expected = "/pets"; - assertThat(new PathImpl(BASE_PATH, expected + "/", EMPTY_JSON_OBJECT, emptyList()).getName()).isEqualTo(expected); + assertThat( + new PathImpl(BASE_PATH, expected + "/", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()) + .getName()).isEqualTo(expected); } @Test void testRootPath() { String expected = "/"; - assertThat(new PathImpl(BASE_PATH, expected, EMPTY_JSON_OBJECT, emptyList()).getName()).isEqualTo(expected); + assertThat( + new PathImpl(BASE_PATH, expected, EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()).getName()) + .isEqualTo(expected); } @Test void testGetAbsolutePath() { String expected = "/base/foo"; - assertThat(new PathImpl("/base", "/foo", EMPTY_JSON_OBJECT, emptyList()).getAbsolutePath()).isEqualTo(expected); - assertThat(new PathImpl("/base/", "/foo", EMPTY_JSON_OBJECT, emptyList()).getAbsolutePath()).isEqualTo(expected); + assertThat(new PathImpl("/base", "/foo", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()) + .getAbsolutePath()).isEqualTo(expected); + assertThat(new PathImpl("/base/", "/foo", EMPTY_JSON_OBJECT, emptyList(), MediaTypeRegistry.createDefault()) + .getAbsolutePath()).isEqualTo(expected); } } diff --git a/src/test/java/io/vertx/tests/contract/impl/RequestBodyImplTest.java b/src/test/java/io/vertx/tests/contract/impl/RequestBodyImplTest.java index 413eeeb6..5dcd29a6 100644 --- a/src/test/java/io/vertx/tests/contract/impl/RequestBodyImplTest.java +++ b/src/test/java/io/vertx/tests/contract/impl/RequestBodyImplTest.java @@ -15,8 +15,8 @@ import static com.google.common.truth.Truth.assertThat; import static io.vertx.openapi.contract.ContractErrorType.INVALID_SPEC; import static io.vertx.openapi.contract.ContractErrorType.UNSUPPORTED_FEATURE; -import static io.vertx.openapi.contract.MediaType.APPLICATION_JSON; -import static io.vertx.openapi.contract.MediaType.APPLICATION_JSON_UTF8; +import static io.vertx.openapi.mediatype.impl.DefaultMediaTypeRegistration.APPLICATION_JSON; +import static io.vertx.openapi.mediatype.impl.DefaultMediaTypeRegistration.APPLICATION_JSON_UTF8; import static io.vertx.tests.ResourceHelper.getRelatedTestResourcePath; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -27,6 +27,7 @@ import io.vertx.openapi.contract.OpenAPIContractException; import io.vertx.openapi.contract.RequestBody; import io.vertx.openapi.contract.impl.RequestBodyImpl; +import io.vertx.openapi.mediatype.MediaTypeRegistry; import java.nio.file.Path; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; @@ -75,14 +76,15 @@ private static Stream testExceptions() { Arguments.of("0002_RequestBody_With_Content_Type_Application_Png", UNSUPPORTED_FEATURE, "The passed OpenAPI contract contains a feature that is not supported: Operation dummyOperation defines a " + "request body with an unsupported media type. Supported: application/json, application/json; charset=utf-8," - + " multipart/form-data, application/hal+json, application/octet-stream, text/plain, text/plain; charset=utf-8")); + + " application/hal+json, multipart/form-data, application/octet-stream, text/plain, text/plain; charset=utf-8, ^[^/]+/vnd\\.[\\w.-]+\\+json$")); } @ParameterizedTest(name = "{index} test getters for scenario: {0}") @MethodSource void testGetters(String testId, boolean required) { JsonObject requestBodyModel = validTestData.getJsonObject(testId); - RequestBodyImpl requestBody = new RequestBodyImpl(requestBodyModel, DUMMY_OPERATION_ID); + RequestBodyImpl requestBody = + new RequestBodyImpl(requestBodyModel, DUMMY_OPERATION_ID, MediaTypeRegistry.createDefault()); assertThat(requestBody.isRequired()).isEqualTo(required); assertThat(requestBody.getOpenAPIModel()).isEqualTo(requestBodyModel); @@ -95,7 +97,8 @@ void testGetters(String testId, boolean required) { void testExceptions(String testId, ContractErrorType type, String msg) { JsonObject requestBody = invalidTestData.getJsonObject(testId); OpenAPIContractException exception = - assertThrows(OpenAPIContractException.class, () -> new RequestBodyImpl(requestBody, DUMMY_OPERATION_ID)); + assertThrows(OpenAPIContractException.class, + () -> new RequestBodyImpl(requestBody, DUMMY_OPERATION_ID, MediaTypeRegistry.createDefault())); assertThat(exception.type()).isEqualTo(type); assertThat(exception).hasMessageThat().isEqualTo(msg); } @@ -106,7 +109,8 @@ private RequestBodyImpl buildWithContent(String... contentTypes) { for (String type : contentTypes) { content.put(type, dummySchema); } - return new RequestBodyImpl(new JsonObject().put("content", content), DUMMY_OPERATION_ID); + return new RequestBodyImpl(new JsonObject().put("content", content), DUMMY_OPERATION_ID, + MediaTypeRegistry.createDefault()); } @Test diff --git a/src/test/java/io/vertx/tests/contract/impl/ResponseImplTest.java b/src/test/java/io/vertx/tests/contract/impl/ResponseImplTest.java index 801331c0..37b9ab61 100644 --- a/src/test/java/io/vertx/tests/contract/impl/ResponseImplTest.java +++ b/src/test/java/io/vertx/tests/contract/impl/ResponseImplTest.java @@ -27,6 +27,7 @@ import io.vertx.openapi.contract.ContractErrorType; import io.vertx.openapi.contract.OpenAPIContractException; import io.vertx.openapi.contract.impl.ResponseImpl; +import io.vertx.openapi.mediatype.MediaTypeRegistry; import java.nio.file.Path; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; @@ -67,14 +68,14 @@ private static Stream testExceptions() { Arguments.of("0000_Response_With_Content_Type_Application_Png", UNSUPPORTED_FEATURE, "The passed OpenAPI contract contains a feature that is not supported: Operation dummyOperation defines a " + "response with an unsupported media type. Supported: application/json, application/json; charset=utf-8, " - + "multipart/form-data, application/hal+json, application/octet-stream, text/plain, text/plain; charset=utf-8")); + + "application/hal+json, multipart/form-data, application/octet-stream, text/plain, text/plain; charset=utf-8, ^[^/]+/vnd\\.[\\w.-]+\\+json$")); } @ParameterizedTest(name = "{index} test getters for scenario: {0}") @MethodSource void testGetters(String testId, int contentSize, String contentKey, int headerSize, SchemaType type) { JsonObject responseModel = validTestData.getJsonObject(testId); - ResponseImpl response = new ResponseImpl(responseModel, DUMMY_OPERATION_ID); + ResponseImpl response = new ResponseImpl(responseModel, DUMMY_OPERATION_ID, MediaTypeRegistry.createDefault()); assertThat(response.getHeaders()).hasSize(headerSize); if (headerSize > 0) { @@ -94,7 +95,8 @@ void testGetters(String testId, int contentSize, String contentKey, int headerSi void testExceptions(String testId, ContractErrorType type, String msg) { JsonObject response = invalidTestData.getJsonObject(testId); OpenAPIContractException exception = - assertThrows(OpenAPIContractException.class, () -> new ResponseImpl(response, DUMMY_OPERATION_ID)); + assertThrows(OpenAPIContractException.class, + () -> new ResponseImpl(response, DUMMY_OPERATION_ID, MediaTypeRegistry.createDefault())); assertThat(exception.type()).isEqualTo(type); assertThat(exception).hasMessageThat().isEqualTo(msg); } diff --git a/src/test/java/io/vertx/tests/mediatype/ContentAnalyserFactoryTest.java b/src/test/java/io/vertx/tests/mediatype/ContentAnalyserFactoryTest.java new file mode 100644 index 00000000..853ef8a9 --- /dev/null +++ b/src/test/java/io/vertx/tests/mediatype/ContentAnalyserFactoryTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025, SAP SE + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.tests.mediatype; + +import static com.google.common.truth.Truth.assertThat; + +import io.vertx.openapi.mediatype.ContentAnalyserFactory; +import io.vertx.openapi.mediatype.impl.ApplicationJsonAnalyser; +import io.vertx.openapi.mediatype.impl.MultipartFormAnalyser; +import io.vertx.openapi.mediatype.impl.NoOpAnalyser; +import org.junit.jupiter.api.Test; + +public class ContentAnalyserFactoryTest { + + @Test + void testJson() { + assertThat(ContentAnalyserFactory.json().create(null, null, null)).isInstanceOf(ApplicationJsonAnalyser.class); + assertThat(ContentAnalyserFactory.noop().create(null, null, null)).isInstanceOf(NoOpAnalyser.class); + assertThat(ContentAnalyserFactory.multipart().create(null, null, null)).isInstanceOf(MultipartFormAnalyser.class); + } +} diff --git a/src/test/java/io/vertx/tests/mediatype/MediaTypeInfoTest.java b/src/test/java/io/vertx/tests/mediatype/MediaTypeInfoTest.java new file mode 100644 index 00000000..c7fca9f1 --- /dev/null +++ b/src/test/java/io/vertx/tests/mediatype/MediaTypeInfoTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2025, Lukas Jelonek + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.tests.mediatype; + +import static com.google.common.truth.Truth.assertThat; + +import io.vertx.openapi.mediatype.MediaTypeInfo; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class MediaTypeInfoTest { + + static Stream mediatypes() { + return Stream.of( + Arguments.of("text/html", "text", "html", null, Map.of()), + Arguments.of("application/json", "application", "json", null, Map.of()), + Arguments.of("image/png", "image", "png", null, Map.of()), + Arguments.of("application/vnd.api+json", "application", "vnd.api", "json", Map.of()), + Arguments.of("text/plain; charset=UTF-8", "text", "plain", null, Map.of("charset", "UTF-8")), + Arguments.of("audio/*", "audio", "*", null, Map.of()), + Arguments.of("application/xhtml+xml", "application", "xhtml", "xml", Map.of()), + Arguments.of("application/xml; charset=ISO-8859-1", "application", "xml", null, + Map.of("charset", "ISO-8859-1")), + Arguments.of("video/*; codec=H.264", "video", "*", null, Map.of("codec", "H.264")), + Arguments.of("application/octet-stream", "application", "octet-stream", null, Map.of()), + Arguments.of("application/json; charset=UTF-8; version=1.0", "application", "json", null, + orderedMap("charset", "UTF-8", "version", "1.0"))); + } + + private static Map orderedMap(String k1, String v1, String k2, String v2) { + var m = new LinkedHashMap(); + m.put(k1, v1); + m.put(k2, v2); + return m; + } + + @ParameterizedTest + @MethodSource("mediatypes") + void should_parse_mediatype_correctly(String mediatypestring, String type, String subtype, String suffix, + Map parameters) { + var info = MediaTypeInfo.of(mediatypestring); + assertThat(info.type()).isEqualTo(type); + assertThat(info.subtype()).isEqualTo(subtype); + assertThat(info.suffix()).isEqualTo(Optional.ofNullable(suffix)); + assertThat(info.parameters()).isEqualTo(parameters); + } + + @ParameterizedTest + @MethodSource("mediatypes") + void should_serialize_mediatype_info_correctly(String mediatypestring, String type, String subtype, String suffix, + Map parameters) { + var info = new MediaTypeInfo(type, subtype, suffix, parameters); + assertThat(info.toString()).isEqualTo(mediatypestring); + } + + @ParameterizedTest + @MethodSource("mediatypes") + void fullType_should_include_type_subtype_suffix(String mediatypestring) { + var info = MediaTypeInfo.of(mediatypestring); + assertThat(info.fullType()).isEqualTo(mediatypestring.split(";")[0].trim()); + } + + static Stream compatibility_data() { + return Stream.of( + Arguments.of("application/json", "application/json", true), + Arguments.of("application/json", "application/json; charset=utf-8", true), + Arguments.of("application/json; charset=iso-8851-1", "application/json; charset=utf-8", false), + Arguments.of("application/json; charset=utf-8", "application/json", false), + Arguments.of("application/*", "application/json", true), + Arguments.of("application/json", "application/*", false), + Arguments.of("application/*", "text/plain", false), + Arguments.of("application/*", "text/*", false), + Arguments.of("application/vnd.example", "application/vnd.example+xml", true), + Arguments.of("application/vnd.example", "application/vnd.example+json", true), + Arguments.of("application/vnd.example+json", "application/vnd.example", false), + Arguments.of("application/vnd.example+json", "application/vnd.example+xml", false), + Arguments.of("application/vnd.example+json", "application/vnd.example+json", true)); + } + + @ParameterizedTest + @MethodSource("compatibility_data") + void doesInclude_should_work_correctly(String src, String other, boolean expected) { + assertThat(MediaTypeInfo.of(src).doesInclude(MediaTypeInfo.of(other))).isEqualTo(expected); + } +} diff --git a/src/test/java/io/vertx/tests/mediatype/MediaTypePredicateTest.java b/src/test/java/io/vertx/tests/mediatype/MediaTypePredicateTest.java new file mode 100644 index 00000000..949c58b0 --- /dev/null +++ b/src/test/java/io/vertx/tests/mediatype/MediaTypePredicateTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025, SAP SE + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.tests.mediatype; + +import static com.google.common.truth.Truth.assertThat; + +import io.vertx.openapi.mediatype.MediaTypeInfo; +import io.vertx.openapi.mediatype.MediaTypePredicate; +import org.junit.jupiter.api.Test; + +class MediaTypePredicateTest { + + @Test + void testOfExactTypesMatches() { + MediaTypePredicate predicate = MediaTypePredicate.ofExactTypes("application/json", "text/plain"); + assertThat(predicate.test(MediaTypeInfo.of("application/json"))).isTrue(); + assertThat(predicate.test(MediaTypeInfo.of("text/plain"))).isTrue(); + assertThat(predicate.test(MediaTypeInfo.of("image/png"))).isFalse(); + assertThat(predicate.supportedTypes()).containsExactly("application/json", "text/plain").inOrder(); + } + + @Test + void testOfRegexpMatches() { + MediaTypePredicate predicate = MediaTypePredicate.ofRegexp("application/.*"); + assertThat(predicate.test(MediaTypeInfo.of("application/json"))).isTrue(); + assertThat(predicate.test(MediaTypeInfo.of("application/xml"))).isTrue(); + assertThat(predicate.test(MediaTypeInfo.of("text/plain"))).isFalse(); + assertThat(predicate.supportedTypes()).containsExactly("application/.*"); + } +} diff --git a/src/test/java/io/vertx/tests/mediatype/MediaTypeRegistryTest.java b/src/test/java/io/vertx/tests/mediatype/MediaTypeRegistryTest.java new file mode 100644 index 00000000..b843e23d --- /dev/null +++ b/src/test/java/io/vertx/tests/mediatype/MediaTypeRegistryTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025, Lukas Jelonek + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.tests.mediatype; + +import static com.google.common.truth.Truth.assertThat; +import static io.vertx.openapi.mediatype.MediaTypePredicate.ofExactTypes; + +import io.vertx.openapi.mediatype.ContentAnalyserFactory; +import io.vertx.openapi.mediatype.MediaTypeRegistration; +import io.vertx.openapi.mediatype.MediaTypeRegistry; +import io.vertx.openapi.mediatype.impl.DefaultMediaTypeRegistration; +import org.junit.jupiter.api.Test; + +public class MediaTypeRegistryTest { + + @Test + void testCreateEmpty() { + MediaTypeRegistry r = MediaTypeRegistry.createEmpty(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.TEXT_PLAIN)).isFalse(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.TEXT_PLAIN_UTF8)).isFalse(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.APPLICATION_JSON)).isFalse(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.APPLICATION_JSON_UTF8)).isFalse(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.APPLICATION_HAL_JSON)).isFalse(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.APPLICATION_OCTET_STREAM)).isFalse(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.MULTIPART_FORM_DATA)).isFalse(); + } + + @Test + void testCreateDefault() { + MediaTypeRegistry r = MediaTypeRegistry.createDefault(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.TEXT_PLAIN)).isTrue(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.TEXT_PLAIN_UTF8)).isTrue(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.APPLICATION_JSON)).isTrue(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.APPLICATION_JSON_UTF8)).isTrue(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.APPLICATION_HAL_JSON)).isTrue(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.APPLICATION_OCTET_STREAM)).isTrue(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.MULTIPART_FORM_DATA)).isTrue(); + } + + @Test + void testIsSupported() { + MediaTypeRegistry r = MediaTypeRegistry.createEmpty(); + assertThat(r.isSupported(DefaultMediaTypeRegistration.TEXT_PLAIN)).isFalse(); + r.register(MediaTypeRegistration.TEXT_PLAIN); + assertThat(r.isSupported(DefaultMediaTypeRegistration.TEXT_PLAIN)).isTrue(); + } + + @Test + void addCustomTypeShouldMakeItSupported() { + MediaTypeRegistry r = MediaTypeRegistry.createEmpty(); + String t = "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml"; + r.register(new DefaultMediaTypeRegistration(ofExactTypes(t), ContentAnalyserFactory.noop())); + assertThat(r.isSupported(t)).isTrue(); + } +} diff --git a/src/test/java/io/vertx/tests/validation/analyser/ApplicationJsonAnalyserTest.java b/src/test/java/io/vertx/tests/mediatype/impl/ApplicationJsonAnalyserTest.java similarity index 94% rename from src/test/java/io/vertx/tests/validation/analyser/ApplicationJsonAnalyserTest.java rename to src/test/java/io/vertx/tests/mediatype/impl/ApplicationJsonAnalyserTest.java index c7dbc51b..ea1d53ab 100644 --- a/src/test/java/io/vertx/tests/validation/analyser/ApplicationJsonAnalyserTest.java +++ b/src/test/java/io/vertx/tests/mediatype/impl/ApplicationJsonAnalyserTest.java @@ -10,7 +10,7 @@ * */ -package io.vertx.tests.validation.analyser; +package io.vertx.tests.mediatype.impl; import static com.google.common.truth.Truth.assertThat; import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_JSON; @@ -20,8 +20,8 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonObject; +import io.vertx.openapi.mediatype.impl.ApplicationJsonAnalyser; import io.vertx.openapi.validation.ValidatorException; -import io.vertx.openapi.validation.analyser.ApplicationJsonAnalyser; import org.junit.jupiter.api.Test; class ApplicationJsonAnalyserTest { diff --git a/src/test/java/io/vertx/tests/mediatype/impl/DefaultMediaTypeRegistrationTest.java b/src/test/java/io/vertx/tests/mediatype/impl/DefaultMediaTypeRegistrationTest.java new file mode 100644 index 00000000..bab95dad --- /dev/null +++ b/src/test/java/io/vertx/tests/mediatype/impl/DefaultMediaTypeRegistrationTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025, SAP SE + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.tests.mediatype.impl; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.*; + +import io.vertx.core.buffer.Buffer; +import io.vertx.openapi.mediatype.ContentAnalyser; +import io.vertx.openapi.mediatype.ContentAnalyserFactory; +import io.vertx.openapi.mediatype.MediaTypeInfo; +import io.vertx.openapi.mediatype.MediaTypePredicate; +import io.vertx.openapi.mediatype.impl.DefaultMediaTypeRegistration; +import io.vertx.openapi.validation.ValidationContext; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DefaultMediaTypeRegistrationTest { + + private MediaTypePredicate predicate; + private ContentAnalyserFactory factory; + private DefaultMediaTypeRegistration registration; + + @BeforeEach + void setUp() { + predicate = mock(MediaTypePredicate.class); + factory = mock(ContentAnalyserFactory.class); + registration = new DefaultMediaTypeRegistration(predicate, factory); + } + + @Test + void testCreateContentAnalyser() { + String contentType = "application/json"; + Buffer buffer = mock(Buffer.class); + ValidationContext context = mock(ValidationContext.class); + ContentAnalyser analyser = mock(ContentAnalyser.class); + when(factory.create(contentType, buffer, context)).thenReturn(analyser); + + ContentAnalyser result = registration.createContentAnalyser(contentType, buffer, context); + assertThat(result).isSameInstanceAs(analyser); + verify(factory).create(contentType, buffer, context); + } + + @Test + void testCanHandle() { + String mediaType = "application/xml"; + when(predicate.test(any(MediaTypeInfo.class))).thenReturn(true); + + assertThat(registration.canHandle(mediaType)).isTrue(); + verify(predicate).test(any(MediaTypeInfo.class)); + } + + @Test + void testSupportedTypes() { + List types = Arrays.asList("application/json", "application/xml"); + when(predicate.supportedTypes()).thenReturn(types); + + List result = registration.supportedTypes(); + assertThat(result).isEqualTo(types); + verify(predicate).supportedTypes(); + } +} diff --git a/src/test/java/io/vertx/tests/mediatype/impl/DefaultMediaTypeRegistryTest.java b/src/test/java/io/vertx/tests/mediatype/impl/DefaultMediaTypeRegistryTest.java new file mode 100644 index 00000000..8d036d21 --- /dev/null +++ b/src/test/java/io/vertx/tests/mediatype/impl/DefaultMediaTypeRegistryTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025, SAP SE + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ + +package io.vertx.tests.mediatype.impl; + +import static com.google.common.truth.Truth.assertThat; + +import io.vertx.openapi.mediatype.MediaTypeRegistration; +import io.vertx.openapi.mediatype.impl.DefaultMediaTypeRegistration; +import io.vertx.openapi.mediatype.impl.DefaultMediaTypeRegistry; +import org.junit.jupiter.api.Test; + +class DefaultMediaTypeRegistryTest { + + @Test + void testGet() { + DefaultMediaTypeRegistry registry = new DefaultMediaTypeRegistry(); + MediaTypeRegistration jsonReg = MediaTypeRegistration.APPLICATION_JSON; + MediaTypeRegistration textReg = MediaTypeRegistration.TEXT_PLAIN; + registry.register(jsonReg).register(textReg); + + assertThat(registry.get("application/json")).isSameInstanceAs(jsonReg); + assertThat(registry.get("text/plain")).isSameInstanceAs(textReg); + assertThat(registry.get("foo/bar")).isNull(); + } + + @Test + void testSupportedTypes() { + DefaultMediaTypeRegistry registry = new DefaultMediaTypeRegistry(); + MediaTypeRegistration jsonReg = MediaTypeRegistration.APPLICATION_JSON; + MediaTypeRegistration textReg = MediaTypeRegistration.TEXT_PLAIN; + registry.register(jsonReg).register(textReg); + assertThat(registry.supportedTypes()).containsExactly( + DefaultMediaTypeRegistration.APPLICATION_JSON, + DefaultMediaTypeRegistration.APPLICATION_JSON_UTF8, + DefaultMediaTypeRegistration.APPLICATION_HAL_JSON, + DefaultMediaTypeRegistration.TEXT_PLAIN, + DefaultMediaTypeRegistration.TEXT_PLAIN_UTF8); + } +} diff --git a/src/test/java/io/vertx/tests/validation/analyser/MultipartFormAnalyserTest.java b/src/test/java/io/vertx/tests/mediatype/impl/MultipartFormAnalyserTest.java similarity index 96% rename from src/test/java/io/vertx/tests/validation/analyser/MultipartFormAnalyserTest.java rename to src/test/java/io/vertx/tests/mediatype/impl/MultipartFormAnalyserTest.java index fdea5234..a9360d8e 100644 --- a/src/test/java/io/vertx/tests/validation/analyser/MultipartFormAnalyserTest.java +++ b/src/test/java/io/vertx/tests/mediatype/impl/MultipartFormAnalyserTest.java @@ -10,19 +10,19 @@ * */ -package io.vertx.tests.validation.analyser; +package io.vertx.tests.mediatype.impl; import static com.google.common.truth.Truth.assertThat; +import static io.vertx.openapi.mediatype.impl.MultipartFormAnalyser.extractBoundary; import static io.vertx.openapi.validation.ValidationContext.REQUEST; -import static io.vertx.openapi.validation.analyser.MultipartFormAnalyser.extractBoundary; import static io.vertx.tests.ResourceHelper.getRelatedTestResourcePath; import static org.junit.jupiter.api.Assertions.assertThrows; import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonObject; +import io.vertx.openapi.mediatype.impl.MultipartFormAnalyser; import io.vertx.openapi.validation.ValidatorErrorType; import io.vertx.openapi.validation.ValidatorException; -import io.vertx.openapi.validation.analyser.MultipartFormAnalyser; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; diff --git a/src/test/java/io/vertx/tests/validation/analyser/MultipartPartTest.java b/src/test/java/io/vertx/tests/mediatype/impl/MultipartPartTest.java similarity index 97% rename from src/test/java/io/vertx/tests/validation/analyser/MultipartPartTest.java rename to src/test/java/io/vertx/tests/mediatype/impl/MultipartPartTest.java index 79f2d7e6..d6bca459 100644 --- a/src/test/java/io/vertx/tests/validation/analyser/MultipartPartTest.java +++ b/src/test/java/io/vertx/tests/mediatype/impl/MultipartPartTest.java @@ -10,7 +10,7 @@ * */ -package io.vertx.tests.validation.analyser; +package io.vertx.tests.mediatype.impl; import static com.google.common.truth.Truth.assertThat; import static io.vertx.tests.ResourceHelper.getRelatedTestResourcePath; @@ -19,9 +19,9 @@ import com.google.common.truth.Truth; import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonObject; +import io.vertx.openapi.mediatype.impl.MultipartPart; import io.vertx.openapi.validation.ValidatorErrorType; import io.vertx.openapi.validation.ValidatorException; -import io.vertx.openapi.validation.analyser.MultipartPart; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; diff --git a/src/test/java/io/vertx/tests/validation/analyser/ContentAnalyserTest.java b/src/test/java/io/vertx/tests/validation/analyser/ContentAnalyserTest.java deleted file mode 100644 index 606ea741..00000000 --- a/src/test/java/io/vertx/tests/validation/analyser/ContentAnalyserTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2024, SAP SE - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 - * which is available at https://www.apache.org/licenses/LICENSE-2.0. - * - * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 - * - */ - -package io.vertx.tests.validation.analyser; - -import static com.google.common.truth.Truth.assertThat; -import static io.vertx.openapi.contract.MediaType.APPLICATION_HAL_JSON; -import static io.vertx.openapi.contract.MediaType.APPLICATION_JSON; -import static io.vertx.openapi.contract.MediaType.APPLICATION_JSON_UTF8; -import static io.vertx.openapi.contract.MediaType.MULTIPART_FORM_DATA; -import static io.vertx.openapi.validation.analyser.ContentAnalyser.getContentAnalyser; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import io.vertx.openapi.contract.MediaType; -import io.vertx.openapi.validation.analyser.ApplicationJsonAnalyser; -import io.vertx.openapi.validation.analyser.MultipartFormAnalyser; -import org.junit.jupiter.api.Test; - -public class ContentAnalyserTest { - - @Test - void testGetContentAnalyser() { - assertThat(getContentAnalyser(mockMediaType(APPLICATION_JSON), null, null, null)) - .isInstanceOf(ApplicationJsonAnalyser.class); - assertThat(getContentAnalyser(mockMediaType(APPLICATION_JSON_UTF8), null, null, null)) - .isInstanceOf(ApplicationJsonAnalyser.class); - assertThat(getContentAnalyser(mockMediaType(APPLICATION_HAL_JSON), null, null, null)) - .isInstanceOf(ApplicationJsonAnalyser.class); - assertThat(getContentAnalyser(mockMediaType(MULTIPART_FORM_DATA), null, null, null)) - .isInstanceOf(MultipartFormAnalyser.class); - - assertThat(getContentAnalyser(mockMediaType("application/xml"), null, null, null)).isNull(); - } - - MediaType mockMediaType(String identifier) { - MediaType mockedMediaType = mock(MediaType.class); - when(mockedMediaType.getIdentifier()).thenReturn(identifier); - return mockedMediaType; - } -} diff --git a/src/test/java/io/vertx/tests/validation/impl/BaseValidatorTest.java b/src/test/java/io/vertx/tests/validation/impl/BaseValidatorTest.java index 8207283c..8394d60b 100644 --- a/src/test/java/io/vertx/tests/validation/impl/BaseValidatorTest.java +++ b/src/test/java/io/vertx/tests/validation/impl/BaseValidatorTest.java @@ -83,14 +83,14 @@ static Stream testIsSchemaValidationRequired() { JsonObject binaryStringSchema = stringSchema.copy().put("format", "binary"); Function buildMediaModel = schema -> new JsonObject().put("schema", schema); - MediaType noMediaModel = new MediaTypeImpl("", EMPTY_JSON_OBJECT); - MediaType typeNumber = new MediaTypeImpl("", buildMediaModel.apply(new JsonObject().put("type", "number"))); - MediaType typeStringNoFormat = new MediaTypeImpl("", buildMediaModel.apply(stringSchema)); - MediaType typeStringFormatBinary = new MediaTypeImpl("", buildMediaModel.apply(binaryStringSchema)); + MediaType noMediaModel = new MediaTypeImpl("", EMPTY_JSON_OBJECT, null); + MediaType typeNumber = new MediaTypeImpl("", buildMediaModel.apply(new JsonObject().put("type", "number")), null); + MediaType typeStringNoFormat = new MediaTypeImpl("", buildMediaModel.apply(stringSchema), null); + MediaType typeStringFormatBinary = new MediaTypeImpl("", buildMediaModel.apply(binaryStringSchema), null); MediaType typeStringFormatTime = new MediaTypeImpl("", buildMediaModel.apply(stringSchema.copy().put("format", - "time"))); + "time")), null); MediaType typeStringFormatBinaryMinLength = new MediaTypeImpl("", - buildMediaModel.apply(binaryStringSchema.copy().put("minLength", 1))); + buildMediaModel.apply(binaryStringSchema.copy().put("minLength", 1)), null); return Stream.of( Arguments.of("No media model is defined", noMediaModel, false), diff --git a/src/test/java/io/vertx/tests/validation/impl/RequestValidatorImplTest.java b/src/test/java/io/vertx/tests/validation/impl/RequestValidatorImplTest.java index a3931e5e..ae36f00a 100644 --- a/src/test/java/io/vertx/tests/validation/impl/RequestValidatorImplTest.java +++ b/src/test/java/io/vertx/tests/validation/impl/RequestValidatorImplTest.java @@ -59,6 +59,8 @@ import io.vertx.openapi.contract.Parameter; import io.vertx.openapi.contract.RequestBody; import io.vertx.openapi.contract.Style; +import io.vertx.openapi.contract.impl.MediaTypeImpl; +import io.vertx.openapi.mediatype.MediaTypeRegistration; import io.vertx.openapi.validation.RequestParameter; import io.vertx.openapi.validation.RequestValidator; import io.vertx.openapi.validation.ValidatableRequest; @@ -111,12 +113,14 @@ private static Stream testValidateWithValidatableRequestAndOperationI parameters.add(buildParam("PathParamVersion", PATH, SIMPLE, numberSchema().toJson(), true)); parameters.add(buildParam("QueryParamTrace", QUERY, FORM, booleanSchema().toJson(), true)); - MediaType mockedMediaType = mock(MediaType.class); + MediaTypeImpl mockedMediaType = mock(MediaTypeImpl.class); when(mockedMediaType.getIdentifier()).thenReturn(MediaType.APPLICATION_JSON); + when(mockedMediaType.getRegistration()).thenReturn(MediaTypeRegistration.APPLICATION_JSON); when(mockedMediaType.getSchema()).thenReturn(JsonSchema.of(objectSchema().toJson())); - MediaType mockedMediaTypeBinary = mock(MediaType.class); + MediaTypeImpl mockedMediaTypeBinary = mock(MediaTypeImpl.class); when(mockedMediaTypeBinary.getIdentifier()).thenReturn("application/octet-stream"); + when(mockedMediaTypeBinary.getRegistration()).thenReturn(MediaTypeRegistration.APPLICATION_OCTET_STREAM); when(mockedMediaTypeBinary.getSchema()).thenReturn(null); RequestBody mockedRequestBody = mock(RequestBody.class); @@ -383,9 +387,10 @@ void testValidateParameterThrowUnsupportedValueFormat(Style style) { } private RequestBody mockRequestBody(boolean isRequired) { - MediaType mockedMediaType = mock(MediaType.class); + MediaTypeImpl mockedMediaType = mock(MediaTypeImpl.class); when(mockedMediaType.getSchema()).thenReturn(JsonSchema.of(objectSchema().toJson())); when(mockedMediaType.getIdentifier()).thenReturn(MediaType.APPLICATION_JSON); + when(mockedMediaType.getRegistration()).thenReturn(MediaTypeRegistration.APPLICATION_JSON); return mockRequestBody(isRequired, mockedMediaType); } @@ -423,7 +428,7 @@ void testValidateBodyNotRequiredAndBodyIsNullOrEmpty(String scenario, RequestPar @ParameterizedTest(name = "validateBody should throw an error if MediaType or Transformer is null") @ValueSource(strings = { "application/png", "foo/bar" }) void testValidateBodyMediaTypeOrAnalyserNull(String contentType) { - MediaType mockedMediaType = mock(MediaType.class); + MediaTypeImpl mockedMediaType = mock(MediaTypeImpl.class); when(mockedMediaType.getIdentifier()).thenReturn(contentType); RequestBody mockedRequestBody = mockRequestBody(false, mockedMediaType); diff --git a/src/test/java/io/vertx/tests/validation/impl/ResponseValidatorImplTest.java b/src/test/java/io/vertx/tests/validation/impl/ResponseValidatorImplTest.java index c573abad..61eb999f 100644 --- a/src/test/java/io/vertx/tests/validation/impl/ResponseValidatorImplTest.java +++ b/src/test/java/io/vertx/tests/validation/impl/ResponseValidatorImplTest.java @@ -42,11 +42,12 @@ import io.vertx.junit5.Timeout; import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; -import io.vertx.openapi.contract.MediaType; import io.vertx.openapi.contract.OpenAPIContract; import io.vertx.openapi.contract.Operation; import io.vertx.openapi.contract.Parameter; import io.vertx.openapi.contract.Response; +import io.vertx.openapi.contract.impl.MediaTypeImpl; +import io.vertx.openapi.mediatype.MediaTypeRegistration; import io.vertx.openapi.validation.ResponseParameter; import io.vertx.openapi.validation.ValidatableResponse; import io.vertx.openapi.validation.ValidatorException; @@ -81,9 +82,10 @@ private static Stream provideNullRequestParameters() { } private static Response mockResponse() { - MediaType mockedMediaType = mock(MediaType.class); + MediaTypeImpl mockedMediaType = mock(MediaTypeImpl.class); when(mockedMediaType.getSchema()).thenReturn(JsonSchema.of(objectSchema().toJson())); when(mockedMediaType.getIdentifier()).thenReturn(APPLICATION_JSON.toString()); + when(mockedMediaType.getRegistration()).thenReturn(MediaTypeRegistration.APPLICATION_JSON); Response mockedResponse = mock(Response.class); when(mockedResponse.getContent()).thenReturn(ImmutableMap.of(APPLICATION_JSON.toString(), mockedMediaType)); @@ -237,8 +239,9 @@ void testValidateBodyRequiredButNullOrEmpty(String scenario, ResponseParameter p @ParameterizedTest(name = "validateBody should throw an error if MediaType or Transformer is null") @ValueSource(strings = { "application/png", "foo/bar" }) void testValidateBodyMediaTypeOrAnalyserNull(String contentType) { - MediaType mockedMediaType = mock(MediaType.class); + MediaTypeImpl mockedMediaType = mock(MediaTypeImpl.class); when(mockedMediaType.getIdentifier()).thenReturn(contentType); + when(mockedMediaType.getRegistration()).thenReturn(null); Response mockedResponse = mock(Response.class); when(mockedResponse.getContent()).thenReturn(ImmutableMap.of("application/png", mockedMediaType)); diff --git a/src/test/resources/io/vertx/tests/contract/builder/contract-with-unsupported-mediatype.yaml b/src/test/resources/io/vertx/tests/contract/builder/contract-with-unsupported-mediatype.yaml new file mode 100644 index 00000000..b084c61e --- /dev/null +++ b/src/test/resources/io/vertx/tests/contract/builder/contract-with-unsupported-mediatype.yaml @@ -0,0 +1,28 @@ +openapi: 3.1.1 +info: + title: Example with non-default supported mediatype + version: 2.0.0 + description: "" +paths: + /: + get: + responses: + "200": + description: "" + content: + text/tab-separated-values: + schema: + type: string + operationId: customResponse + post: + requestBody: + required: true + content: + application/vnd.openxmlformats-officedocument.wordprocessingml.document: + schema: + type: string + format: binary + responses: + "204": + description: "" + operationId: customRequest diff --git a/src/test/resources/io/vertx/tests/validation/analyser/.gitattributes b/src/test/resources/io/vertx/tests/mediatype/impl/.gitattributes similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/.gitattributes rename to src/test/resources/io/vertx/tests/mediatype/impl/.gitattributes diff --git a/src/test/resources/io/vertx/tests/validation/analyser/multipart.txt b/src/test/resources/io/vertx/tests/mediatype/impl/multipart.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/multipart.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/multipart.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/multipart_extended_content_type.txt b/src/test/resources/io/vertx/tests/mediatype/impl/multipart_extended_content_type.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/multipart_extended_content_type.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/multipart_extended_content_type.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/multipart_id_no_body.txt b/src/test/resources/io/vertx/tests/mediatype/impl/multipart_id_no_body.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/multipart_id_no_body.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/multipart_id_no_body.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/multipart_invalid_structure.txt b/src/test/resources/io/vertx/tests/mediatype/impl/multipart_invalid_structure.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/multipart_invalid_structure.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/multipart_invalid_structure.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/multipart_invalid_structure_2.txt b/src/test/resources/io/vertx/tests/mediatype/impl/multipart_invalid_structure_2.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/multipart_invalid_structure_2.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/multipart_invalid_structure_2.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/multipart_octet_stream.txt b/src/test/resources/io/vertx/tests/mediatype/impl/multipart_octet_stream.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/multipart_octet_stream.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/multipart_octet_stream.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/multipart_part_invalid_contenttype.txt b/src/test/resources/io/vertx/tests/mediatype/impl/multipart_part_invalid_contenttype.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/multipart_part_invalid_contenttype.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/multipart_part_invalid_contenttype.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/part1.txt b/src/test/resources/io/vertx/tests/mediatype/impl/part1.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/part1.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/part1.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/part2.txt b/src/test/resources/io/vertx/tests/mediatype/impl/part2.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/part2.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/part2.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/part3.txt b/src/test/resources/io/vertx/tests/mediatype/impl/part3.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/part3.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/part3.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/part_without_body.txt b/src/test/resources/io/vertx/tests/mediatype/impl/part_without_body.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/part_without_body.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/part_without_body.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/part_without_contenttype.txt b/src/test/resources/io/vertx/tests/mediatype/impl/part_without_contenttype.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/part_without_contenttype.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/part_without_contenttype.txt diff --git a/src/test/resources/io/vertx/tests/validation/analyser/part_without_name.txt b/src/test/resources/io/vertx/tests/mediatype/impl/part_without_name.txt similarity index 100% rename from src/test/resources/io/vertx/tests/validation/analyser/part_without_name.txt rename to src/test/resources/io/vertx/tests/mediatype/impl/part_without_name.txt