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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/main/java/io/vertx/openapi/contract/MediaType.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

package io.vertx.openapi.contract;

import io.netty.handler.codec.http.HttpHeaderValues;
import io.vertx.codegen.annotations.VertxGen;
import io.vertx.json.schema.JsonSchema;

Expand All @@ -29,18 +28,24 @@
@VertxGen
public interface MediaType extends OpenAPIObject {

String APPLICATION_HAL_JSON = "application/hal+json";
String APPLICATION_JSON = HttpHeaderValues.APPLICATION_JSON.toString();
String APPLICATION_JSON = "application/json";
String APPLICATION_JSON_UTF8 = APPLICATION_JSON + "; charset=utf-8";
String MULTIPART_FORM_DATA = HttpHeaderValues.MULTIPART_FORM_DATA.toString();
List<String> SUPPORTED_MEDIA_TYPES = Arrays.asList(APPLICATION_JSON, APPLICATION_JSON_UTF8, MULTIPART_FORM_DATA, APPLICATION_HAL_JSON);
String MULTIPART_FORM_DATA = "multipart/form-data";
String APPLICATION_HAL_JSON = "application/hal+json";
String APPLICATION_OCTET_STREAM = "application/octet-stream";
List<String> SUPPORTED_MEDIA_TYPES = Arrays.asList(APPLICATION_JSON, APPLICATION_JSON_UTF8, MULTIPART_FORM_DATA,
APPLICATION_HAL_JSON, APPLICATION_OCTET_STREAM);

static boolean isMediaTypeSupported(String type) {
return SUPPORTED_MEDIA_TYPES.contains(type.toLowerCase());
}

/**
* @return the schema defining the content of the request.
* This method returns the schema defined in the media type.
* <p></p>
* In OpenAPI 3.1 it is allowed to define an empty media type model. In this case the method returns null.
*
* @return the schema defined in the media type model, or null in case no media type model was defined.
*/
JsonSchema getSchema();

Expand Down
17 changes: 14 additions & 3 deletions src/main/java/io/vertx/openapi/contract/impl/MediaTypeImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,22 @@ public class MediaTypeImpl implements MediaType {
public MediaTypeImpl(String identifier, JsonObject mediaTypeModel) {
this.identifier = identifier;
this.mediaTypeModel = mediaTypeModel;
JsonObject schemaJson = mediaTypeModel.getJsonObject(KEY_SCHEMA);
if (schemaJson == null || schemaJson.isEmpty()) {

if (mediaTypeModel == null) {
throw createUnsupportedFeature("Media Type without a schema");
}
schema = JsonSchema.of(schemaJson);

if (mediaTypeModel.isEmpty()) {
// OpenAPI 3.1 allows defining MediaTypes without a schema.
schema = null;
} else {
JsonObject schemaJson = mediaTypeModel.getJsonObject(KEY_SCHEMA);
if (schemaJson == null || schemaJson.isEmpty()) {
throw createUnsupportedFeature("Media Type without a schema");
}
schema = JsonSchema.of(schemaJson);

}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public MediaType determineContentType(String contentType) {
if (contentType == null) {
return null;
}

String condensedIdentifier = removeWhiteSpaces(contentType);
if (content.containsKey(condensedIdentifier)) {
return content.get(condensedIdentifier);
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/vertx/openapi/impl/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ public static Future<JsonObject> readYamlOrJson(Vertx vertx, String path) {
});
}

/**
/**
* Reads YAML string and transforms it into a JsonObject.
*
* @param path The yamlString proper YAML formatted STring
* @param yamlString The yamlString proper YAML formatted STring
* @return A succeeded Future holding the JsonObject, or a failed Future if the file could not be parsed.
*/
public static Future<JsonObject> yamlStringToJson(String yamlString) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,23 @@ public static SchemaValidationException createInvalidValueParameter(Parameter pa
return new SchemaValidationException(msg, INVALID_VALUE, outputUnit, cause);
}

public static SchemaValidationException createInvalidValueRequestBody(OutputUnit outputUnit,
JsonSchemaValidationException cause) {
String msg = String.format("The value of the request body is invalid. Reason: %s", extractReason(outputUnit));
return new SchemaValidationException(msg, INVALID_VALUE, outputUnit, cause);
}

public static SchemaValidationException createInvalidValueResponseBody(OutputUnit outputUnit,
JsonSchemaValidationException cause) {
String msg = String.format("The value of the response body is invalid. Reason: %s", extractReason(outputUnit));
public static SchemaValidationException createInvalidValueBody(OutputUnit outputUnit,
ValidationContext requestOrResponse,
JsonSchemaValidationException cause) {
String msg = String.format("The value of the " + requestOrResponse + " body is invalid. Reason: %s",
extractReason(outputUnit));
return new SchemaValidationException(msg, INVALID_VALUE, outputUnit, cause);
}

public static SchemaValidationException createMissingValueRequestBody(OutputUnit outputUnit,
JsonSchemaValidationException cause) {
JsonSchemaValidationException cause) {
String msg = String.format("The value of the request body is missing. Reason: %s", extractReason(outputUnit));
return new SchemaValidationException(msg, MISSING_REQUIRED_PARAMETER, outputUnit, cause);
}

public static SchemaValidationException createErrorFromOutputUnitType(Parameter parameter, OutputUnit outputUnit,
JsonSchemaValidationException cause) {
switch(outputUnit.getErrorType()) {
switch (outputUnit.getErrorType()) {
case MISSING_VALUE:
return createMissingValueRequestBody(outputUnit, cause);
case INVALID_VALUE:
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/io/vertx/openapi/validation/ValidationContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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;

public enum ValidationContext {
REQUEST, RESPONSE;

@Override
public String toString() {
return name().toLowerCase();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 io.vertx.core.buffer.Buffer;
import io.vertx.openapi.validation.ValidationContext;

public class ApplicationJsonAnalyser extends ContentAnalyser {
private Object decodedValue;

public ApplicationJsonAnalyser(String contentType, Buffer content, ValidationContext context) {
super(contentType, content, context);
}

@Override
public void checkSyntacticalCorrectness() {
decodedValue = decodeJsonContent(content, requestOrResponse);
}

@Override
public Object transform() {
return decodedValue;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* 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 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;

import static io.vertx.openapi.validation.ValidatorErrorType.ILLEGAL_VALUE;

/**
* The content analyser is responsible for checking if the content is syntactically correct, and transforming the
* content.
* <p>
* 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.
* <p>
* Therefore, it is very important to ensure that the {@link #checkSyntacticalCorrectness()} method is always called
* before.
*/
public abstract class ContentAnalyser {
private static class OctetStreamAnalyser extends ContentAnalyser {
public OctetStreamAnalyser(String contentType, Buffer content, ValidationContext context) {
super(contentType, content, context);
}

@Override
public void checkSyntacticalCorrectness() {
// no syntax check for octet-stream
}

@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:
return new OctetStreamAnalyser(contentType, content, context);
default:
return null;
}
}

protected String contentType;
protected Buffer content;
protected 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.
* <p>
* 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}.
* <p>
* 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");
}
}
}
Loading