io.github.microcks.util.metadata.ExamplesImporter 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.metadata;
import io.github.microcks.domain.EventMessage;
import io.github.microcks.domain.Exchange;
import io.github.microcks.domain.Header;
import io.github.microcks.domain.Message;
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.Response;
import io.github.microcks.domain.Service;
import io.github.microcks.domain.ServiceType;
import io.github.microcks.domain.UnidirectionalEvent;
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.URIBuilder;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
/**
* Mock repository importer that uses a {@code ApiExamples} YAML descriptor as a source artifact.
* @author laurent
*/
public class ExamplesImporter implements MockRepositoryImporter {
/** A simple logger for diagnostic messages. */
private static final Logger log = LoggerFactory.getLogger(ExamplesImporter.class);
private static final String REQUEST_NODE = "request";
private static final String RESPONSE_NODE = "response";
private static final String PARAMETERS_NODE = "parameters";
private static final String HEADERS_NODE = "headers";
private static final String BODY_NODE = "body";
private static final String PAYLOAD_NODE = "payload";
private final ObjectMapper mapper;
private final JsonNode spec;
/**
* Build a new importer.
* @param specificationFilePath The path to local APIExamples spec file
* @throws IOException if project file cannot be found or read.
*/
public ExamplesImporter(String specificationFilePath) throws IOException {
try {
// Read spec bytes.
byte[] bytes = Files.readAllBytes(Paths.get(specificationFilePath));
String specContent = new String(bytes, StandardCharsets.UTF_8);
// Convert them to Node using Jackson object mapper.
mapper = new ObjectMapper(new YAMLFactory());
spec = mapper.readTree(specContent.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
log.error("Exception while parsing APIMetadata specification file " + specificationFilePath, e);
throw new IOException("APIMetadata spec file parsing error");
}
}
@Override
public List getServiceDefinitions() throws MockRepositoryImportException {
List result = new ArrayList<>();
// Build a new service.
Service service = new Service();
JsonNode metadataNode = spec.get("metadata");
if (metadataNode == null) {
log.error("Missing mandatory metadata in {}", spec.asText());
throw new MockRepositoryImportException("Mandatory metadata property is missing in APIMetadata");
}
service.setName(metadataNode.path("name").asText());
service.setVersion(metadataNode.path("version").asText());
// Then build its operations.
service.setOperations(extractOperations());
result.add(service);
return result;
}
@Override
public List getResourceDefinitions(Service service) throws MockRepositoryImportException {
return Collections.emptyList();
}
@Override
public List getMessageDefinitions(Service service, Operation operation)
throws MockRepositoryImportException {
List result = new ArrayList<>();
// Iterate on specification "operations" nodes.
Iterator> operations = spec.path("operations").fields();
while (operations.hasNext()) {
Map.Entry operationEntry = operations.next();
// Select the matching operation.
if (operation.getName().equals(operationEntry.getKey())) {
// Browse examples and extract exchanges.
Iterator> examples = operationEntry.getValue().fields();
while (examples.hasNext()) {
Map.Entry exampleEntry = examples.next();
// Extract and add this exchange.
result.add(extractExchange(service, operation, exampleEntry.getKey(), exampleEntry.getValue()));
}
}
}
return result;
}
/** Extract the list of operations from Specification. */
private List extractOperations() {
List results = new ArrayList<>();
// Iterate on specification "operations" nodes.
Iterator> operations = spec.path("operations").fields();
while (operations.hasNext()) {
Map.Entry operation = operations.next();
// Build a new operation.
Operation op = new Operation();
op.setName(operation.getKey());
results.add(op);
}
return results;
}
/** Extract exchange information from an example node. */
private Exchange extractExchange(Service service, Operation operation, String exampleName, JsonNode exampleNode) {
Exchange exchange;
if (ServiceType.EVENT.equals(service.getType())) {
exchange = extractUnidirectionalEvent(exampleName, exampleNode);
} else {
exchange = extractRequestResponsePair(service, operation, exampleName, exampleNode);
}
return exchange;
}
/** Extract unidirectional event messages from an example node. */
private UnidirectionalEvent extractUnidirectionalEvent(String exampleName, JsonNode exampleNode) {
if (exampleNode.has("eventMessage")) {
JsonNode eventNode = exampleNode.get("eventMessage");
EventMessage event = new EventMessage();
event.setName(exampleName);
if (eventNode.has(HEADERS_NODE)) {
completeWithHeaders(event, eventNode.get(HEADERS_NODE));
}
event.setContent(getSerializedValue(eventNode.get(PAYLOAD_NODE)));
return new UnidirectionalEvent(event);
}
return null;
}
/** Extract request/response pair from an example node. */
private RequestResponsePair extractRequestResponsePair(Service service, Operation operation, String exampleName,
JsonNode exampleNode) {
// Build and return something only if request and response.
if (exampleNode.has(REQUEST_NODE) && exampleNode.has(RESPONSE_NODE)) {
JsonNode requestNode = exampleNode.get(REQUEST_NODE);
JsonNode responseNode = exampleNode.get(RESPONSE_NODE);
// Initialize and complete the request.
Request request = new Request();
request.setName(exampleName);
request.setContent(getSerializedValue(requestNode.get(BODY_NODE)));
Multimap parameters = null;
if (requestNode.has(PARAMETERS_NODE)) {
parameters = completeWithParameters(request, requestNode.get(PARAMETERS_NODE));
}
if (requestNode.has(HEADERS_NODE)) {
completeWithHeaders(request, requestNode.get(HEADERS_NODE));
}
// Initialize and complete the response.
Response response = new Response();
response.setName(exampleName);
if (responseNode.has(HEADERS_NODE)) {
completeWithHeaders(response, responseNode.get(HEADERS_NODE));
}
if (responseNode.has("mediaType")) {
String mediaType = responseNode.get("mediaType").asText();
response.setMediaType(mediaType);
}
if (responseNode.has("status")) {
String status = responseNode.get("status").asText();
response.setStatus(status);
if (!status.startsWith("2") || !status.startsWith("3")) {
response.setFault(true);
}
}
response.setContent(getSerializedValue(responseNode.get(BODY_NODE)));
// Finally, take care about dispatchCriteria and complete operation resourcePaths.
// If we previously override the dispatcher with a Fallback, we must be sure to get wrapped elements.
DispatchCriteriaHelper.DispatcherDetails details = DispatchCriteriaHelper
.extractDispatcherWithRules(operation);
// Finally, take care about dispatchCriteria and complete operation resourcePaths.
completeDispatchCriteriaAndResourcePaths(service, operation, details.rootDispatcher(),
details.rootDispatcherRules(), parameters, requestNode, responseNode, request, response);
return new RequestResponsePair(request, response);
}
return null;
}
/** Complete a request by extracting parameters. */
private Multimap completeWithParameters(Request request, JsonNode parametersNode) {
Multimap result = ArrayListMultimap.create();
Iterator> parameters = parametersNode.fields();
while (parameters.hasNext()) {
Map.Entry parameterNode = parameters.next();
Parameter parameter = new Parameter();
parameter.setName(parameterNode.getKey());
parameter.setValue(getSerializedValue(parameterNode.getValue()));
request.addQueryParameter(parameter);
// Depending on node type, extract different representations to MultiMap result.
if (parameterNode.getValue().isArray()) {
for (JsonNode current : parameterNode.getValue()) {
result.put(parameterNode.getKey(), getSerializedValue(current));
}
} else if (parameterNode.getValue().isObject()) {
final var fieldsIterator = parameterNode.getValue().fields();
while (fieldsIterator.hasNext()) {
var current = fieldsIterator.next();
result.put(current.getKey(), getSerializedValue(current.getValue()));
}
} else {
result.put(parameterNode.getKey(), getSerializedValue(parameterNode.getValue()));
}
}
return result;
}
/** Complete a message by extracting headers. */
private void completeWithHeaders(Message message, JsonNode headersNode) {
Iterator> headers = headersNode.fields();
while (headers.hasNext()) {
Map.Entry headerNode = headers.next();
Header header = new Header();
header.setName(headerNode.getKey());
// Header value may be multiple CSV.
Set values = Arrays.stream(getSerializedValue(headerNode.getValue()).split(",")).map(String::trim)
.collect(Collectors.toSet());
header.setValues(values);
message.addHeader(header);
}
}
/** */
private void completeDispatchCriteriaAndResourcePaths(Service service, Operation operation, String rootDispatcher,
String rootDispatcherRules, Multimap parameters, JsonNode requestNode, JsonNode responseNode,
Request request, Response response) {
String dispatchCriteria = null;
String resourcePathPattern = operation.getName().contains(" ") ? operation.getName().split(" ")[1]
: operation.getName();
if (DispatchStyles.URI_PARAMS.equals(rootDispatcher)) {
dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, parameters);
// We only need the pattern here.
operation.addResourcePath(resourcePathPattern);
} else if (DispatchStyles.URI_PARTS.equals(rootDispatcher)) {
dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, parameters);
// We should complete resourcePath here.
String resourcePath = URIBuilder.buildURIFromPattern(resourcePathPattern, parameters);
operation.addResourcePath(resourcePath);
} else if (DispatchStyles.URI_ELEMENTS.equals(rootDispatcher)) {
// Split parameters between path and query.
Multimap pathParameters = Multimaps.filterEntries(parameters,
entry -> operation.getName().contains("/{" + entry.getKey() + "}")
|| operation.getName().contains("/:" + entry.getKey()));
Multimap queryParameters = Multimaps.filterEntries(parameters,
entry -> !pathParameters.containsKey(entry.getKey()));
dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, pathParameters);
dispatchCriteria += DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, queryParameters);
// We should complete resourcePath here.
String resourcePath = URIBuilder.buildURIFromPattern(resourcePathPattern, parameters);
operation.addResourcePath(resourcePath);
} else if (DispatchStyles.QUERY_ARGS.equals(rootDispatcher)) {
if (ServiceType.GRAPHQL.equals(service.getType()) && requestNode.has(BODY_NODE)) {
JsonNode variables = requestNode.get(BODY_NODE).path("variables");
dispatchCriteria = extractQueryArgsCriteria(rootDispatcherRules, variables);
} else if (ServiceType.GRPC.equals(service.getType())) {
dispatchCriteria = extractQueryArgsCriteria(rootDispatcherRules, request.getContent());
}
} else if (responseNode.has("dispatchCriteria")) {
dispatchCriteria = responseNode.get("dispatchCriteria").asText();
}
// In any case (dispatcher forced via Metadata or set to SCRIPT, we should still put a generic resourcePath
// (maybe containing {} parts) to later force operation matching at the mock controller level.
operation.addResourcePath(resourcePathPattern);
response.setDispatchCriteria(dispatchCriteria);
}
/** Extract the QUERY_ARGS Dispatch criteria from the variable provided as JSON string representation. */
private String extractQueryArgsCriteria(String dispatcherRules, JsonNode variables) {
String dispatchCriteria = "";
try {
Map paramsMap = mapper.convertValue(variables,
TypeFactory.defaultInstance().constructMapType(TreeMap.class, String.class, String.class));
dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(dispatcherRules, paramsMap);
} catch (Exception e) {
log.error("Exception while converting dispatch criteria from JSON body: {}", e.getMessage());
}
return dispatchCriteria;
}
/** Extract the QUERY_ARGS Dispatch criteria from the variable provided as JSON string representation. */
private String extractQueryArgsCriteria(String dispatcherRules, String variables) {
String dispatchCriteria = "";
try {
Map paramsMap = mapper.readValue(variables,
TypeFactory.defaultInstance().constructMapType(TreeMap.class, String.class, String.class));
dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(dispatcherRules, paramsMap);
} catch (Exception e) {
log.error("Exception while extracting dispatch criteria from JSON body: {}", e.getMessage());
}
return dispatchCriteria;
}
/** Get the serialized value of a content node, or null if node is null ;-) */
private String getSerializedValue(JsonNode contentNode) {
if (contentNode != null) {
// Get string representation if array or object.
if (contentNode.getNodeType() == JsonNodeType.ARRAY || contentNode.getNodeType() == JsonNodeType.OBJECT) {
return contentNode.toString();
}
// Else get raw representation.
return contentNode.asText();
}
return null;
}
}