io.vertx.ext.web.api.contract.openapi3.impl.OpenApi3Utils Maven / Gradle / Ivy
package io.vertx.ext.web.api.contract.openapi3.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.networknt.schema.*;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.parser.ObjectMapperFactory;
import io.swagger.v3.parser.core.models.ParseOptions;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.api.OperationRequest;
import io.vertx.ext.web.api.contract.RouterFactoryException;
import io.vertx.ext.web.api.validation.SpecFeatureNotSupportedException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @author Francesco Guardiani @slinkydeveloper
*/
public class OpenApi3Utils {
private static final JsonMetaSchema META_SCHEMA = JsonMetaSchema
.builder(JsonMetaSchema.getV4().getUri(), JsonMetaSchema.getV4())
.addKeyword(new NonValidationKeyword("example"))
.build();
private static final JsonSchemaFactory SCHEMA_FACTORY = new JsonSchemaFactory.Builder()
.defaultMetaSchemaURI(META_SCHEMA.getUri())
.addMetaSchema(META_SCHEMA)
.build();
private static final SchemaValidatorsConfig VALIDATOR_CONFIG = new SchemaValidatorsConfig();
static {
VALIDATOR_CONFIG.setTypeLoose(false);
VALIDATOR_CONFIG.setHandleNullableField(true);
}
public static JsonSchema parseJsonSchema(JsonNode schema) {
return SCHEMA_FACTORY.getSchema(schema, VALIDATOR_CONFIG);
}
public static ParseOptions getParseOptions() {
ParseOptions options = new ParseOptions();
options.setResolve(true);
options.setResolveCombinators(false);
options.setResolveFully(true);
return options;
}
public static boolean isParameterArrayType(Parameter parameter) {
return isSchemaArray(parameter.getSchema());
}
public static boolean isSchemaArray(Schema schema) {
if (schema != null && schema.getType() != null)
return schema.getType().equals("array");
else return false;
}
public static boolean isParameterObjectOrAllOfType(Parameter parameter) {
return isSchemaObjectOrAllOfType(parameter.getSchema());
}
public static boolean isSchemaObjectOrAllOfType(Schema schema) {
return isSchemaObject(schema) || isAllOfSchema(schema);
}
public static boolean isSchemaObject(Schema schema) {
return schema != null && ("object".equals(schema.getType()) || schema.getProperties() != null);
}
public static boolean isRequiredParam(Schema schema, String parameterName) {
return schema != null && schema.getRequired() != null && schema.getRequired().contains(parameterName);
}
public static boolean isRequiredParam(Parameter param) {
return Boolean.TRUE.equals(param.getRequired());
}
public static String resolveStyle(Parameter param) {
if (param.getStyle() != null) return param.getStyle().toString();
else switch (param.getIn()) {
case "query":
return "form";
case "path":
return "simple";
case "header":
return "simple";
case "cookie":
return "form";
default:
return null;
}
}
public static boolean isOneOfSchema(Schema schema) {
if (!(schema instanceof ComposedSchema)) return false;
ComposedSchema composedSchema = (ComposedSchema) schema;
return (composedSchema.getOneOf() != null && composedSchema.getOneOf().size() != 0);
}
public static boolean isAnyOfSchema(Schema schema) {
if (!(schema instanceof ComposedSchema)) return false;
ComposedSchema composedSchema = (ComposedSchema) schema;
return (composedSchema.getAnyOf() != null && composedSchema.getAnyOf().size() != 0);
}
public static boolean isAllOfSchema(Schema schema) {
if (!(schema instanceof ComposedSchema)) return false;
ComposedSchema composedSchema = (ComposedSchema) schema;
return (composedSchema.getAllOf() != null && composedSchema.getAllOf().size() != 0);
}
public static boolean resolveAllowEmptyValue(Parameter parameter) {
if (parameter.getAllowEmptyValue() != null) {
// As OAS says: This is valid only for query parameters and allows sending a parameter with an empty value. Default value is false. If style is used, and if behavior is n/a (cannot be serialized), the value of allowEmptyValue SHALL be ignored
if (!"form".equals(resolveStyle(parameter)))
return false;
else
return parameter.getAllowEmptyValue();
} else return false;
}
// Thank you StackOverflow :) https://stackoverflow
// .com/questions/28332924/case-insensitive-matching-of-a-string-to-a-java-enum :)
public static > T searchEnum(Class enumeration, String search) {
for (T each : enumeration.getEnumConstants()) {
if (each.name().compareToIgnoreCase(search) == 0) {
return each;
}
}
return null;
}
public static String resolveContentTypeRegex(String listContentTypes) {
// Check if it's list
if (listContentTypes.contains(",")) {
StringBuilder stringBuilder = new StringBuilder();
String[] contentTypes = listContentTypes.split(",");
for (String contentType : contentTypes)
stringBuilder.append(Pattern.quote(contentType.trim()) + "|");
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
return stringBuilder.toString();
} else if (listContentTypes.trim().endsWith("/*")) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(listContentTypes.trim(), 0, listContentTypes.indexOf("/*"));
stringBuilder.append("\\/.*");
return stringBuilder.toString();
} return Pattern.quote(listContentTypes);
}
public static List mergeParameters(List operationParameters, List parentParameters) {
if (parentParameters == null && operationParameters == null) {
return new ArrayList<>();
} else if (operationParameters == null) {
return new ArrayList<>(parentParameters);
} else if (parentParameters == null) {
return new ArrayList<>(operationParameters);
} else {
List result = new ArrayList<>(operationParameters);
List actualParams = new ArrayList<>(operationParameters);
for (Parameter parentParameter : parentParameters) {
for (Parameter actualParam1 : actualParams) {
Parameter parentParam = parentParameter;
Parameter actualParam = actualParam1;
if (!(parentParam.getIn().equalsIgnoreCase(actualParam.getIn()) && parentParam.getName().equals(actualParam
.getName())))
result.add(parentParam);
}
}
return result;
}
}
protected static class ObjectField {
Schema schema;
boolean required;
public ObjectField(Schema schema, String name, Schema superSchema) {
this.schema = schema;
this.required = superSchema.getRequired() != null && superSchema.getRequired().contains(name);
}
public Schema getSchema() {
return schema;
}
public boolean isRequired() {
return required;
}
}
/* This function resolve all properties inside an allOf array of schemas */
public static Map resolveAllOfArrays(List allOfSchemas) {
Map properties = new HashMap<>();
for (Schema schema : allOfSchemas) {
if (schema.getType() != null && !schema.getType().equals("object"))
throw new SpecFeatureNotSupportedException("allOf only allows inner object types");
for (Map.Entry entry : ((Map) schema.getProperties()).entrySet()) {
properties.put(entry.getKey(), new OpenApi3Utils.ObjectField(entry.getValue(), entry.getKey(), schema));
}
}
return properties;
}
/* This function check if schema is an allOf array or an object and returns a map of properties */
public static Map solveObjectParameters(Schema schema) {
if (OpenApi3Utils.isSchemaObjectOrAllOfType(schema)) {
if (OpenApi3Utils.isAllOfSchema(schema)) {
// allOf case
ComposedSchema composedSchema = (ComposedSchema) schema;
return resolveAllOfArrays(new ArrayList<>(composedSchema.getAllOf()));
} else {
// type object case
Map properties = new HashMap<>();
if (schema.getProperties() == null) return new HashMap<>();
for (Map.Entry entry : ((Map) schema.getProperties()).entrySet()) {
properties.put(entry.getKey(), new OpenApi3Utils.ObjectField(entry.getValue(), entry.getKey(), schema));
}
return properties;
}
} else return null;
}
private final static Pattern COMPONENTS_REFS_MATCHER = Pattern.compile("^\\#\\/components\\/schemas\\/(.+)$");
private final static String COMPONENTS_REFS_SUBSTITUTION = "\\#\\/definitions\\/$1";
public static JsonNode generateSanitizedJsonSchemaNode(Schema s, OpenAPI oas) {
ObjectNode node = ObjectMapperFactory.createJson().convertValue(s, ObjectNode.class);
walkAndSolve(node, node, oas);
return node;
}
private static void walkAndSolve(ObjectNode n, ObjectNode root, OpenAPI oas) {
if (n.has("$ref")) {
replaceRef(n, root, oas);
} else if (n.has("allOf")) {
for (JsonNode jsonNode : n.get("allOf")) {
// We assert that parser validated allOf as array of objects
walkAndSolve((ObjectNode) jsonNode, root, oas);
}
} else if (n.has("anyOf")) {
for (JsonNode jsonNode : n.get("anyOf")) {
walkAndSolve((ObjectNode) jsonNode, root, oas);
}
} else if (n.has("oneOf")) {
for (JsonNode jsonNode : n.get("oneOf")) {
walkAndSolve((ObjectNode) jsonNode, root, oas);
}
} else if (n.has("properties")) {
ObjectNode properties = (ObjectNode) n.get("properties");
Iterator it = properties.fieldNames();
while (it.hasNext()) {
walkAndSolve((ObjectNode) properties.get(it.next()), root, oas);
}
} else if (n.has("items")) {
walkAndSolve((ObjectNode) n.get("items"), root, oas);
} else if (n.has("additionalProperties")) {
JsonNode jsonNode = n.get("additionalProperties");
if (jsonNode.getNodeType().equals(JsonNodeType.OBJECT)) {
walkAndSolve((ObjectNode) n.get("additionalProperties"), root, oas);
}
}
}
private static void replaceRef(ObjectNode n, ObjectNode root, OpenAPI oas) {
/**
* If a ref is found, the structure of the schema is circular. The oas parser don't solve circular refs.
* So I bundle the schema:
* 1. I update the ref field with a #/definitions/schema_name uri
* 2. If #/definitions/schema_name is empty, I solve it
*/
String oldRef = n.get("$ref").asText();
Matcher m = COMPONENTS_REFS_MATCHER.matcher(oldRef);
if (m.lookingAt()) {
String schemaName = m.group(1);
String newRef = m.replaceAll(COMPONENTS_REFS_SUBSTITUTION);
n.remove("$ref");
n.put("$ref", newRef);
if (!root.has("definitions") || !root.get("definitions").has(schemaName)) {
Schema s = oas.getComponents().getSchemas().get(schemaName);
ObjectNode schema = ObjectMapperFactory.createJson().convertValue(s, ObjectNode.class);
// We need to search inside for other refs
if (!root.has("definitions")) {
ObjectNode definitions = root.putObject("definitions");
definitions.set(schemaName, schema);
} else {
((ObjectNode)root.get("definitions")).set(schemaName, schema);
}
walkAndSolve(schema, root, oas);
}
} else throw new RuntimeException("Wrong ref! " + oldRef);
}
public static List extractTypesFromMediaTypesMap(Map types, Predicate matchingFunction) {
return types
.entrySet().stream()
.filter(e -> matchingFunction.test(e.getKey()))
.map(Map.Entry::getValue).collect(Collectors.toList());
}
public final static List SERVICE_PROXY_METHOD_PARAMETERS = Arrays.asList(new Class[]{OperationRequest.class, Handler.class});
public static boolean serviceProxyMethodIsCompatibleHandler(Method method) {
java.lang.reflect.Parameter[] parameters = method.getParameters();
if (parameters.length < 2) return false;
if (!parameters[parameters.length - 1].getType().equals(Handler.class)) return false;
return parameters[parameters.length - 2].getType().equals(OperationRequest.class);
}
public static JsonObject sanitizeDeliveryOptionsExtension(JsonObject jsonObject) {
JsonObject newObj = new JsonObject();
if (jsonObject.containsKey("timeout")) newObj.put("timeout", jsonObject.getValue("timeout"));
if (jsonObject.containsKey("headers")) newObj.put("headers", jsonObject.getValue("headers"));
return newObj;
}
public static String sanitizeOperationId(String operationId) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < operationId.length(); i++) {
char c = operationId.charAt(i);
if (c == '-' || c == ' ' || c == '_') {
try {
while (c == '-' || c == ' ' || c == '_') {
i++;
c = operationId.charAt(i);
}
result.append(Character.toUpperCase(operationId.charAt(i)));
} catch (StringIndexOutOfBoundsException e) {}
} else {
result.append(c);
}
}
return result.toString();
}
public static Object getAndMergeServiceExtension(String extensionKey, String addressKey, String methodKey, PathItem pathModel, Operation operationModel) {
Object pathExtension = pathModel.getExtensions() != null ? pathModel.getExtensions().get(extensionKey) : null;
Object operationExtension = operationModel.getExtensions() != null ? operationModel.getExtensions().get(extensionKey) : null;
// Cases:
// 1. both strings or path extension null: operation extension overrides all
// 2. path extension map and operation extension string: path extension interpreted as delivery options and operation extension as address
// 3. path extension string and operation extension map: path extension interpreted as address
// 4. both maps: extension map overrides path map elements
// 5. operation extension null: path extension overrides all
if ((operationExtension instanceof String && pathExtension instanceof String) || pathExtension == null) return operationExtension;
if (operationExtension instanceof String && pathExtension instanceof Map) {
Map result = new HashMap<>();
result.put(addressKey, operationExtension);
Map pathExtensionMap = (Map) pathExtension;
if (pathExtensionMap.containsKey(methodKey)) throw RouterFactoryException.createWrongExtension("Extension " + extensionKey + " in path declaration must not contain " + methodKey);
pathExtensionMap.forEach(result::putIfAbsent);
return result;
}
if (operationExtension instanceof Map && pathExtension instanceof String) {
Map result = (Map) operationExtension;
result.putIfAbsent(addressKey, pathExtension);
return result;
}
if (operationExtension instanceof Map && pathExtension instanceof Map) {
Map result = (Map) operationExtension;
((Map)pathExtension).forEach(result::putIfAbsent);
return result;
}
if (operationExtension == null) return pathExtension;
return null;
}
public static final UnaryOperator safeBoolean = in -> in == null? Boolean.FALSE: in.booleanValue();
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy