
io.vertx.openapi.contract.impl.OpenAPIContractImpl Maven / Gradle / Ivy
/*
* Copyright (c) 2023, 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.contract.impl;
import io.vertx.codegen.annotations.Nullable;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.JsonObject;
import io.vertx.json.schema.JsonSchema;
import io.vertx.json.schema.SchemaRepository;
import io.vertx.openapi.contract.OpenAPIContract;
import io.vertx.openapi.contract.OpenAPIVersion;
import io.vertx.openapi.contract.Operation;
import io.vertx.openapi.contract.Path;
import io.vertx.openapi.contract.SecurityRequirement;
import io.vertx.openapi.contract.SecurityScheme;
import io.vertx.openapi.contract.Server;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.UnaryOperator;
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_ARRAY;
import static io.vertx.openapi.impl.Utils.EMPTY_JSON_OBJECT;
import static java.util.Collections.unmodifiableList;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toUnmodifiableList;
public class OpenAPIContractImpl implements OpenAPIContract {
private static final String KEY_SERVERS = "servers";
private static final String KEY_PATHS = "paths";
private static final String KEY_SECURITY = "security";
private static final String PATH_PARAM_PLACEHOLDER_REGEX = "\\{(.*?)}";
private static final UnaryOperator ELIMINATE_PATH_PARAM_PLACEHOLDER =
path -> path.replaceAll(PATH_PARAM_PLACEHOLDER_REGEX, "{}");
private final List servers;
private final List paths;
private final Map operations;
private final OpenAPIVersion version;
private final JsonObject rawContract;
private final SchemaRepository schemaRepository;
private final PathFinder pathFinder;
private final List securityRequirements;
private final Map securitySchemes;
// VisibleForTesting
final String basePath;
public OpenAPIContractImpl(JsonObject resolvedSpec, OpenAPIVersion version, SchemaRepository schemaRepository) {
this.rawContract = resolvedSpec;
this.version = version;
this.schemaRepository = schemaRepository;
servers = resolvedSpec
.getJsonArray(KEY_SERVERS, EMPTY_JSON_ARRAY)
.stream()
.map(JsonObject.class::cast)
.map(ServerImpl::new).collect(toUnmodifiableList());
this.securityRequirements = resolvedSpec
.getJsonArray(KEY_SECURITY, EMPTY_JSON_ARRAY)
.stream()
.map(JsonObject.class::cast)
.map(SecurityRequirementImpl::new).collect(toUnmodifiableList());
if (servers.stream().collect(groupingBy(Server::getBasePath)).size() > 1) {
throw createUnsupportedFeature("Different base paths in server urls");
} else {
this.basePath = servers.isEmpty() ? "" : servers.get(0).getBasePath();
}
List unsortedPaths = resolvedSpec
.getJsonObject(KEY_PATHS, EMPTY_JSON_OBJECT)
.stream()
.filter(JsonSchema.EXCLUDE_ANNOTATION_ENTRIES)
.map(pathEntry -> new PathImpl(basePath, pathEntry.getKey(), (JsonObject) pathEntry.getValue(),
securityRequirements))
.collect(toList());
List sortedPaths = applyMountOrder(unsortedPaths);
this.paths = unmodifiableList(sortedPaths);
this.operations = paths.stream().flatMap(path -> path.getOperations().stream()).collect(toMap(
Operation::getOperationId, operation -> operation));
// It is important that PathFinder gets the ordered Paths
this.pathFinder = new PathFinder(sortedPaths);
this.securitySchemes =
resolvedSpec
.getJsonObject("components", EMPTY_JSON_OBJECT)
.getJsonObject("securitySchemes", EMPTY_JSON_OBJECT)
.stream()
.filter(JsonSchema.EXCLUDE_ANNOTATION_ENTRIES)
.collect(toMap(Map.Entry::getKey, value -> new SecuritySchemeImpl((JsonObject) value.getValue())));
}
/**
* From
* Paths documentation:
*
* Path templating is allowed. When matching URLs, concrete (non-templated) paths would be matched before their
* templated counterparts. Templated paths with the same hierarchy but different templated names MUST NOT exist as
* they are identical. In case of ambiguous matching, it's up to the tooling to decide which one to use.
*
* @return A List which contains paths without path variables first.
*/
// VisibleForTesting
public static List applyMountOrder(List unsorted) {
if (unsorted.size() <= 1) {
return unsorted;
}
List withTemplating = new ArrayList<>();
List withoutTemplating = new ArrayList<>();
for (PathImpl path : unsorted) {
if (path.getName().contains("{")) {
withTemplating.add(path);
} else {
withoutTemplating.add(path);
}
}
withTemplating.sort(comparing(p -> ELIMINATE_PATH_PARAM_PLACEHOLDER.apply(p.getName())));
withoutTemplating.sort(comparing(p -> ELIMINATE_PATH_PARAM_PLACEHOLDER.apply(p.getName())));
// Check for Paths with same hierarchy but different templated names
for (int x = 1; x < withTemplating.size(); x++) {
String first = withTemplating.get(x - 1).getName();
String firstWithoutPlaceHolder = ELIMINATE_PATH_PARAM_PLACEHOLDER.apply(first);
String second = withTemplating.get(x).getName();
String secondWithoutPlaceholder = ELIMINATE_PATH_PARAM_PLACEHOLDER.apply(second);
if (firstWithoutPlaceHolder.equals(secondWithoutPlaceholder)) {
if (first.equals(second)) {
throw createInvalidContract("Found Path duplicate: " + first);
} else {
throw createInvalidContract(
"Found Paths with same hierarchy but different templated names: " + firstWithoutPlaceHolder);
}
}
}
withoutTemplating.addAll(withTemplating);
return withoutTemplating;
}
public String basePath() {
return basePath;
}
@Override
public @Nullable Operation operation(String operationId) {
return operations.get(operationId);
}
@Override
public List operations() {
return List.copyOf(operations.values());
}
@Override
public List getPaths() {
return paths;
}
@Override
public JsonObject getRawContract() {
return rawContract;
}
@Override
public OpenAPIVersion getVersion() {
return version;
}
@Override
public SchemaRepository getSchemaRepository() {
return schemaRepository;
}
@Override
public List getServers() {
return servers;
}
@Override
public Path findPath(String urlPath) {
return pathFinder.findPath(urlPath);
}
@Override
public Operation findOperation(String urlPath, HttpMethod method) {
Path pathObject = findPath(urlPath);
if (pathObject != null) {
for (Operation op : pathObject.getOperations()) {
if (op.getHttpMethod().equals(method)) {
return op;
}
}
}
return null;
}
@Override
public List getSecurityRequirements() {
return securityRequirements;
}
@Override
public SecurityScheme securityScheme(String name) {
return securitySchemes.get(name);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy