io.github.microcks.util.openapi.OpenAPIImporter Maven / Gradle / Ivy
The newest version!
/*
* Copyright The Microcks Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.microcks.util.openapi;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.github.microcks.domain.Exchange;
import io.github.microcks.domain.Header;
import io.github.microcks.domain.Metadata;
import io.github.microcks.domain.Operation;
import io.github.microcks.domain.Parameter;
import io.github.microcks.domain.Request;
import io.github.microcks.domain.RequestResponsePair;
import io.github.microcks.domain.Resource;
import io.github.microcks.domain.ResourceType;
import io.github.microcks.domain.Response;
import io.github.microcks.domain.Service;
import io.github.microcks.domain.ServiceType;
import io.github.microcks.util.AbstractJsonRepositoryImporter;
import io.github.microcks.util.DispatchCriteriaHelper;
import io.github.microcks.util.DispatchStyles;
import io.github.microcks.util.MockRepositoryImportException;
import io.github.microcks.util.MockRepositoryImporter;
import io.github.microcks.util.ReferenceResolver;
import io.github.microcks.util.URIBuilder;
import io.github.microcks.util.metadata.MetadataExtensions;
import io.github.microcks.util.metadata.MetadataExtractor;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* An implementation of MockRepositoryImporter that deals with OpenAPI v3.x.x specification file ; whether encoding into
* JSON or YAML documents.
* @author laurent
*/
public class OpenAPIImporter extends AbstractJsonRepositoryImporter implements MockRepositoryImporter {
/** A simple logger for diagnostic messages. */
private static final Logger log = LoggerFactory.getLogger(OpenAPIImporter.class);
private static final List VALID_VERBS = Arrays.asList("get", "put", "post", "delete", "options", "head",
"patch", "trace");
private static final String PARAMETERS_NODE = "parameters";
private static final String PARAMETERS_QUERY_VALUE = "query";
private static final String CONTENT_NODE = "content";
private static final String HEADERS_NODE = "headers";
private static final String EXAMPLES_NODE = "examples";
private static final String EXAMPLE_VALUE_NODE = "value";
/**
* Build a new importer.
* @param specificationFilePath The path to local OpenAPI spec file
* @param referenceResolver An optional resolver for references present into the OpenAPI file
* @throws IOException if project file cannot be found or read.
*/
public OpenAPIImporter(String specificationFilePath, ReferenceResolver referenceResolver) throws IOException {
super(specificationFilePath, referenceResolver);
}
@Override
public List getServiceDefinitions() throws MockRepositoryImportException {
List result = new ArrayList<>();
// Build a new service.
Service service = new Service();
service.setName(rootSpecification.path("info").path("title").asText());
service.setVersion(rootSpecification.path("info").path("version").asText());
service.setType(ServiceType.REST);
// Complete metadata if specified via extension.
if (rootSpecification.path("info").has(MetadataExtensions.MICROCKS_EXTENSION)) {
Metadata metadata = new Metadata();
MetadataExtractor.completeMetadata(metadata,
rootSpecification.path("info").path(MetadataExtensions.MICROCKS_EXTENSION));
service.setMetadata(metadata);
}
// Before extraction operations, we need to get and build external reference if we have a resolver.
initializeReferencedResources(service);
// Then build its operations.
service.setOperations(extractOperations());
result.add(service);
return result;
}
@Override
public List getResourceDefinitions(Service service) {
List results = new ArrayList<>();
// Build a suitable name.
String name = service.getName() + "-" + service.getVersion();
if (Boolean.TRUE.equals(isYaml)) {
name += ".yaml";
} else {
name += ".json";
}
// Build a brand-new resource just with spec content.
Resource resource = new Resource();
resource.setName(name);
resource.setType(ResourceType.OPEN_API_SPEC);
results.add(resource);
// Set the content of main OpenAPI that may have been updated with normalized dependencies with initializeReferencedResources().
resource.setContent(rootSpecificationContent);
// Add the external resources that were imported during service discovery.
results.addAll(externalResources);
return results;
}
@Override
public List getMessageDefinitions(Service service, Operation operation)
throws MockRepositoryImportException {
Map result = new HashMap<>();
// Iterate on specification "paths" nodes.
Iterator> paths = rootSpecification.path("paths").fields();
while (paths.hasNext()) {
Entry path = paths.next();
String pathName = path.getKey();
JsonNode pathValue = followRefIfAny(path.getValue());
// Find examples fragments defined at the path level.
Map> pathPathParametersByExample = extractParametersByExample(pathValue,
"path");
// Iterate on specification path, "verbs" nodes.
Iterator> verbs = pathValue.fields();
while (verbs.hasNext()) {
Entry verb = verbs.next();
String verbName = verb.getKey();
// Find the correct operation.
if (operation.getName().equals(verbName.toUpperCase() + " " + pathName.trim())) {
// Find examples fragments defined at the verb level.
Map> pathParametersByExample = extractParametersByExample(
verb.getValue(), "path");
pathParametersByExample.putAll(pathPathParametersByExample);
Map> queryParametersByExample = extractParametersByExample(
verb.getValue(), PARAMETERS_QUERY_VALUE);
Map> headerParametersByExample = extractParametersByExample(
verb.getValue(), "header");
Map requestBodiesByExample = extractRequestBodies(verb.getValue());
// No need to go further if no examples.
if (verb.getValue().has("responses")) {
// If we previously override the dispatcher with a Fallback, we must be sure to get wrapped elements.
DispatchCriteriaHelper.DispatcherDetails details = DispatchCriteriaHelper
.extractDispatcherWithRules(operation);
String rootDispatcher = details.rootDispatcher();
String rootDispatcherRules = details.rootDispatcherRules();
Iterator> responseCodes = verb.getValue().path("responses").fields();
while (responseCodes.hasNext()) {
Entry responseCode = responseCodes.next();
Iterator> contents = getResponseContent(responseCode.getValue()).fields();
if (!contents.hasNext() && responseCode.getValue().has("x-microcks-refs")) {
result.putAll(getNoContentRequestResponsePair(operation, rootDispatcher, rootDispatcherRules,
requestBodiesByExample, pathParametersByExample, queryParametersByExample,
headerParametersByExample, responseCode));
}
while (contents.hasNext()) {
Entry content = contents.next();
result.putAll(getContentRequestResponsePairs(operation, rootDispatcher, rootDispatcherRules,
requestBodiesByExample, pathParametersByExample, queryParametersByExample,
headerParametersByExample, responseCode, content));
}
}
}
}
}
}
// Adapt map to list of Exchanges.
return result.entrySet().stream().map(entry -> new RequestResponsePair(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
/**
* Extract the list of operations from Specification.
*/
private List extractOperations() {
List results = new ArrayList<>();
// Iterate on specification "paths" nodes.
Iterator> paths = rootSpecification.path("paths").fields();
while (paths.hasNext()) {
Entry path = paths.next();
String pathName = path.getKey();
JsonNode pathValue = followRefIfAny(path.getValue());
// Iterate on specification path, "verbs" nodes.
Iterator> verbs = pathValue.fields();
while (verbs.hasNext()) {
Entry verb = verbs.next();
String verbName = verb.getKey();
// Only deal with real verbs for now.
if (VALID_VERBS.contains(verbName)) {
String operationName = verbName.toUpperCase() + " " + pathName.trim();
Operation operation = new Operation();
operation.setName(operationName);
operation.setMethod(verbName.toUpperCase());
// Complete operation properties if any.
if (verb.getValue().has(MetadataExtensions.MICROCKS_OPERATION_EXTENSION)) {
MetadataExtractor.completeOperationProperties(operation,
verb.getValue().path(MetadataExtensions.MICROCKS_OPERATION_EXTENSION));
}
// Deal with dispatcher stuffs if needed.
if (operation.getDispatcher() == null) {
if (operationHasParameters(verb.getValue(), PARAMETERS_QUERY_VALUE) && urlHasParts(pathName)) {
operation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromURIPattern(pathName) + " ?? "
+ extractOperationParams(verb.getValue()));
operation.setDispatcher(DispatchStyles.URI_ELEMENTS);
} else if (operationHasParameters(verb.getValue(), PARAMETERS_QUERY_VALUE)) {
operation.setDispatcherRules(extractOperationParams(verb.getValue()));
operation.setDispatcher(DispatchStyles.URI_PARAMS);
} else if (urlHasParts(pathName)) {
operation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromURIPattern(pathName));
operation.setDispatcher(DispatchStyles.URI_PARTS);
} else {
operation.addResourcePath(pathName);
}
} else {
// If dispatcher has been forced via Metadata, we should still put a generic resourcePath
// (maybe containing {} parts) to later force operation matching at the mock controller level.
operation.addResourcePath(pathName);
}
results.add(operation);
}
}
}
return results;
}
/**
* Extract parameters within a specification node and organize them by example. Parameter can be of type 'path',
* 'query', 'header' or 'cookie'. Allow to filter them using parameterType. Key of returned map is example name. Key
* of value map is param name. Value of value map is param value ;-)
*/
private Map> extractParametersByExample(JsonNode node, String parameterType) {
Map> results = new HashMap<>();
Iterator parameters = node.path(PARAMETERS_NODE).elements();
while (parameters.hasNext()) {
JsonNode parameter = followRefIfAny(parameters.next());
String parameterName = parameter.path("name").asText();
if (parameter.has("in") && parameter.path("in").asText().equals(parameterType)
&& parameter.has(EXAMPLES_NODE)) {
Iterator exampleNames = parameter.path(EXAMPLES_NODE).fieldNames();
while (exampleNames.hasNext()) {
String exampleName = exampleNames.next();
JsonNode example = parameter.path(EXAMPLES_NODE).path(exampleName);
JsonNode exampleValue = getExampleValue(example);
if (exampleValue == null) {
log.warn("Couldn't find example value for example node: name: {}, data: {}", exampleName, example);
continue;
}
Multimap exampleParams = results.computeIfAbsent(exampleName,
k -> ArrayListMultimap.create());
if (PARAMETERS_QUERY_VALUE.equals(parameterType) && exampleValue.isArray()) {
// Array of query params.
for (JsonNode current : (ArrayNode) exampleValue) {
exampleParams.put(parameterName, getValueString(current));
}
} else if (PARAMETERS_QUERY_VALUE.equals(parameterType) && exampleValue.isObject()) {
final var fieldsIterator = ((ObjectNode) exampleValue).fields();
while (fieldsIterator.hasNext()) {
var current = fieldsIterator.next();
exampleParams.put(current.getKey(), getValueString(current.getValue()));
}
} else {
exampleParams.put(parameterName, getValueString(exampleValue));
}
}
}
}
return results;
}
/**
* Extract request bodies within verb specification and organize them by example. Key of returned map is example
* name. Value is basic Microcks Request object (no query params, no headers)
*/
private Map extractRequestBodies(JsonNode verbNode) {
Map results = new HashMap<>();
JsonNode requestBody = verbNode.path("requestBody");
Iterator contentTypeNames = requestBody.path(CONTENT_NODE).fieldNames();
while (contentTypeNames.hasNext()) {
String contentTypeName = contentTypeNames.next();
JsonNode contentType = requestBody.path(CONTENT_NODE).path(contentTypeName);
if (contentType.has(EXAMPLES_NODE)) {
Iterator exampleNames = contentType.path(EXAMPLES_NODE).fieldNames();
while (exampleNames.hasNext()) {
String exampleName = exampleNames.next();
JsonNode example = contentType.path(EXAMPLES_NODE).path(exampleName);
String exampleValue = getSerializedExampleValue(example);
// Build and store a request object.
Request request = new Request();
request.setName(exampleName);
request.setContent(exampleValue);
// We should add a Content-type header here for request body.
Header header = new Header();
header.setName("Content-Type");
HashSet values = new HashSet<>();
values.add(contentTypeName);
header.setValues(values);
request.addHeader(header);
results.put(exampleName, request);
}
}
}
return results;
}
/**
* Extract headers within a header specification node and organize them by example. Key of returned map is example
* name. Value is a list of Microcks Header objects.
*/
private Map> extractHeadersByExample(JsonNode responseNode) {
Map> results = new HashMap<>();
responseNode = followRefIfAny(responseNode);
if (responseNode.has(HEADERS_NODE)) {
JsonNode headersNode = responseNode.path(HEADERS_NODE);
Iterator headerNames = headersNode.fieldNames();
while (headerNames.hasNext()) {
String headerName = headerNames.next();
JsonNode headerNode = headersNode.path(headerName);
if (headerNode.has(EXAMPLES_NODE)) {
Iterator exampleNames = headerNode.path(EXAMPLES_NODE).fieldNames();
while (exampleNames.hasNext()) {
String exampleName = exampleNames.next();
JsonNode example = headerNode.path(EXAMPLES_NODE).path(exampleName);
String exampleValue = getSerializedExampleValue(example);
List headersForExample = results.computeIfAbsent(exampleName, k -> new ArrayList<>());
// Example may be multiple CSV.
Set values = Arrays.stream(exampleValue.split(",")).map(String::trim)
.collect(Collectors.toSet());
Header header = new Header();
header.setName(headerName);
header.setValues(values);
headersForExample.add(header);
results.put(exampleName, headersForExample);
}
}
}
}
return results;
}
/**
* Get the request/response pairs for a response content.
*/
private Map getContentRequestResponsePairs(Operation operation, String rootDispatcher,
String rootDispatcherRules, Map requestBodiesByExample,
Map> pathParametersByExample,
Map> queryParametersByExample,
Map> headerParametersByExample, Entry responseCode,
Entry content) {
Map results = new HashMap<>();
String contentValue = content.getKey();
// Find here potential headers for output of this operation examples.
Map> headersByExample = extractHeadersByExample(responseCode.getValue());
JsonNode examplesNode = followRefIfAny(content.getValue().path(EXAMPLES_NODE));
Iterator exampleNames = examplesNode.fieldNames();
while (exampleNames.hasNext()) {
String exampleName = exampleNames.next();
JsonNode example = examplesNode.path(exampleName);
// We should have everything at hand to build response here.
Response response = new Response();
response.setName(exampleName);
response.setMediaType(contentValue);
response.setStatus(responseCode.getKey());
response.setContent(getSerializedExampleValue(example));
if (!responseCode.getKey().startsWith("2")) {
response.setFault(true);
}
List responseHeaders = headersByExample.get(exampleName);
if (responseHeaders != null) {
responseHeaders.stream().forEach(response::addHeader);
}
// Do we have a request for this example?
Request request = requestBodiesByExample.get(exampleName);
if (request == null) {
request = new Request();
request.setName(exampleName);
}
// Complete request accept-type with response content-type.
Header header = new Header();
header.setName("Accept");
HashSet values = new HashSet<>();
values.add(contentValue);
header.setValues(values);
request.addHeader(header);
// Do we have to complete request with path parameters?
Multimap pathParameters = pathParametersByExample.get(exampleName);
if (pathParameters != null) {
completeRequestWithPathParameters(request, pathParameters);
} else if (DispatchStyles.URI_PARTS.equals(operation.getDispatcher())
|| DispatchStyles.URI_ELEMENTS.equals(operation.getDispatcher())) {
// We must have at least one path parameters but none!
// Do not register this request / response pair.
break;
}
// Complete request with query parameters if any.
completeRequestWithQueryParameters(request, queryParametersByExample.get(exampleName));
// Complete request with header parameters if any.
completeRequestWithHeaderParameters(request, headerParametersByExample.get(exampleName));
// Finally, take care about dispatchCriteria and complete operation resourcePaths.
completeDispatchCriteriaAndResourcePaths(operation, rootDispatcher, rootDispatcherRules,
pathParametersByExample, queryParametersByExample, exampleName, response);
results.put(request, response);
}
return results;
}
/**
* Get the request/response pairs for a response without content. A response without content has a x-microcks-refs
* property to get bounds to requests.
*/
private Map getNoContentRequestResponsePair(Operation operation, String rootDispatcher,
String rootDispatcherRules, Map requestBodiesByExample,
Map> pathParametersByExample,
Map> queryParametersByExample,
Map> headerParametersByExample, Entry responseCode) {
Map results = new HashMap<>();
JsonNode requestRefs = responseCode.getValue().path("x-microcks-refs");
if (requestRefs.isArray()) {
// Find here potential headers for output of this operation examples.
Map> headersByExample = extractHeadersByExample(responseCode.getValue());
Iterator requestRefsIterator = requestRefs.elements();
while (requestRefsIterator.hasNext()) {
String exampleName = requestRefsIterator.next().textValue();
// Do we have a request or path or query or header parameters?
Request request = requestBodiesByExample.get(exampleName);
Multimap pathParameters = pathParametersByExample.get(exampleName);
Multimap queryParameters = queryParametersByExample.get(exampleName);
Multimap headerParameters = headerParametersByExample.get(exampleName);
if (request != null || pathParameters != null || queryParameters != null || headerParameters != null) {
if (request == null) {
request = new Request();
request.setName(exampleName);
}
if (pathParameters != null) {
completeRequestWithPathParameters(request, pathParameters);
} else if (DispatchStyles.URI_PARTS.equals(operation.getDispatcher())
|| DispatchStyles.URI_ELEMENTS.equals(operation.getDispatcher())) {
// We must have at least one path parameters but none!
// Do not register this request / response pair.
break;
}
// We should have everything at hand to build response here.
Response response = new Response();
response.setName(exampleName);
response.setStatus(responseCode.getKey());
if (!responseCode.getKey().startsWith("2")) {
response.setFault(true);
}
List responseHeaders = headersByExample.get(exampleName);
if (responseHeaders != null) {
responseHeaders.stream().forEach(response::addHeader);
}
// Complete request with query parameters if any.
completeRequestWithQueryParameters(request, queryParametersByExample.get(exampleName));
// Complete request with header parameters if any.
completeRequestWithHeaderParameters(request, headerParametersByExample.get(exampleName));
// Finally, take care about dispatchCriteria and complete operation resourcePaths.
completeDispatchCriteriaAndResourcePaths(operation, rootDispatcher, rootDispatcherRules,
pathParametersByExample, queryParametersByExample, exampleName, response);
results.put(request, response);
}
}
}
return results;
}
private void completeRequestWithPathParameters(Request request, Multimap pathParameters) {
for (Entry paramEntry : pathParameters.entries()) {
Parameter param = new Parameter();
param.setName(paramEntry.getKey());
param.setValue(paramEntry.getValue());
request.addQueryParameter(param);
}
}
private void completeRequestWithQueryParameters(Request request, Multimap queryParameters) {
if (queryParameters != null) {
for (Entry paramEntry : queryParameters.entries()) {
Parameter param = new Parameter();
param.setName(paramEntry.getKey());
param.setValue(paramEntry.getValue());
request.addQueryParameter(param);
}
}
}
private void completeRequestWithHeaderParameters(Request request, Multimap headerParameters) {
if (headerParameters != null) {
for (Entry headerEntry : headerParameters.entries()) {
Header header = new Header();
header.setName(headerEntry.getKey());
// Values may be multiple and CSV.
Set headerValues = Arrays.stream(headerEntry.getValue().split(",")).map(String::trim)
.collect(Collectors.toSet());
header.setValues(headerValues);
request.addHeader(header);
}
}
}
private void completeDispatchCriteriaAndResourcePaths(Operation operation, String rootDispatcher,
String rootDispatcherRules, Map> pathParametersByExample,
Map> queryParametersByExample, String exampleName, Response response) {
String dispatchCriteria = null;
String resourcePathPattern = operation.getName().split(" ")[1];
if (DispatchStyles.URI_PARAMS.equals(rootDispatcher)) {
Multimap queryParams = queryParametersByExample.get(exampleName);
dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, queryParams);
// We only need the pattern here.
operation.addResourcePath(resourcePathPattern);
} else if (DispatchStyles.URI_PARTS.equals(rootDispatcher)) {
Multimap parts = pathParametersByExample.get(exampleName);
dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, parts);
// We should complete resourcePath here.
String resourcePath = URIBuilder.buildURIFromPattern(resourcePathPattern, parts);
operation.addResourcePath(resourcePath);
} else if (DispatchStyles.URI_ELEMENTS.equals(rootDispatcher)) {
Multimap parts = pathParametersByExample.get(exampleName);
Multimap queryParams = queryParametersByExample.get(exampleName);
dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, parts);
dispatchCriteria += DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, queryParams);
// We should complete resourcePath here.
String resourcePath = URIBuilder.buildURIFromPattern(resourcePathPattern, parts);
operation.addResourcePath(resourcePath);
}
response.setDispatchCriteria(dispatchCriteria);
}
/** Get the value of an example. This can be direct value field or those of followed $ref */
private JsonNode getExampleValue(JsonNode example) {
if (example.has(EXAMPLE_VALUE_NODE)) {
return followRefIfAny(example.path(EXAMPLE_VALUE_NODE));
}
if (example.has("$ref")) {
JsonNode component = followRefIfAny(example);
return getExampleValue(component);
}
return null;
}
/** Get the serialized value of an example. This can be direct value field or those of followed $ref */
private String getSerializedExampleValue(JsonNode example) {
JsonNode exampleValue = getExampleValue(example);
return exampleValue != null ? getValueString(exampleValue) : null;
}
/** Get the content of a response. This can be direct content field or those of followed $ref */
private JsonNode getResponseContent(JsonNode response) {
if (response.has("$ref")) {
JsonNode component = followRefIfAny(response);
return getResponseContent(component);
}
return response.path(CONTENT_NODE);
}
/** Build a string representing operation parameters as used in dispatcher rules (param1 && param2) */
private String extractOperationParams(JsonNode operation) {
StringBuilder params = new StringBuilder();
Iterator parameters = operation.path(PARAMETERS_NODE).elements();
while (parameters.hasNext()) {
JsonNode parameter = followRefIfAny(parameters.next());
String parameterIn = parameter.path("in").asText();
String parameterType = followRefIfAny(parameter.path("schema")).path("type").asText();
if (!"path".equals(parameterIn)) {
if (params.length() > 0) {
params.append(" && ");
}
if (parameterType.equals("object")) {
params.append(StreamSupport.stream(Spliterators.spliteratorUnknownSize(
followRefIfAny(parameter.path("schema")).path("properties").fieldNames(), Spliterator.ORDERED),
false).collect(Collectors.joining(" && ")));
} else {
params.append(parameter.path("name").asText());
}
}
}
return params.toString();
}
/** Check parameters presence into given operation node. */
private boolean operationHasParameters(JsonNode operation, String parameterType) {
if (!operation.has(PARAMETERS_NODE)) {
return false;
}
Iterator parameters = operation.path(PARAMETERS_NODE).elements();
while (parameters.hasNext()) {
JsonNode parameter = followRefIfAny(parameters.next());
String parameterIn = parameter.path("in").asText();
if (parameterIn.equals(parameterType)) {
return true;
}
}
return false;
}
/** Check variables parts presence into given url. */
private static boolean urlHasParts(String url) {
return (url.indexOf("/:") != -1 || url.indexOf("/{") != -1);
}
}