All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.microcks.util.asyncapi.AsyncAPI3Importer 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.asyncapi;

import io.github.microcks.domain.EventMessage;
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.Resource;
import io.github.microcks.domain.ResourceType;
import io.github.microcks.domain.Service;
import io.github.microcks.domain.ServiceType;
import io.github.microcks.domain.UnidirectionalEvent;
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.ObjectMapper;
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.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import static io.github.microcks.util.asyncapi.AsyncAPICommons.*;

/**
 * An implementation of MockRepositoryImporter that deals with AsyncAPI v3.0.x specification file ; whether encoding
 * into JSON or YAML documents.
 * @author laurent
 */
public class AsyncAPI3Importer extends AbstractJsonRepositoryImporter implements MockRepositoryImporter {

   /** A simple logger for diagnostic messages. */
   private static final Logger log = LoggerFactory.getLogger(AsyncAPI3Importer.class);

   private static final List VALID_VERBS = Arrays.asList("send", "receive");


   /**
    * Build a new importer.
    * @param specificationFilePath The path to local AsyncAPI spec file
    * @param referenceResolver     An optional resolver for references present into the AsyncAPI file
    * @throws IOException if project file cannot be found or read.
    */
   public AsyncAPI3Importer(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.EVENT);

      // 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.ASYNC_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 {
      List messageDefs = new ArrayList<>();

      // Retrieve default content type, defaulting to application/json.
      String defaultContentType = "application/json";
      if (rootSpecification.has("defaultContentType")) {
         defaultContentType = rootSpecification.get("defaultContentType").asText("application/json");
      }

      // Iterate on specification "operations" nodes.
      Iterator> operations = rootSpecification.path("operations").fields();
      while (operations.hasNext()) {
         Map.Entry operationEntry = operations.next();
         JsonNode operationNode = operationEntry.getValue();

         // Got to filter out for current operation only.
         String action = operationNode.path("action").asText();
         String operationName = action.toUpperCase() + " " + operationEntry.getKey();

         if (operationName.equals(operation.getName()) && operationNode.path(MESSAGES).isArray()) {
            // Search for event messages.
            List eventMessages = buildEventMessages(operationNode, defaultContentType);

            if (eventMessages != null && !eventMessages.isEmpty()) {
               // Update dispatch information if necessary.
               completeDispatchingCriteria(operation, operationNode, eventMessages);

               eventMessages.stream().forEach(eventMessage -> messageDefs.add(new UnidirectionalEvent(eventMessage)));
            }
            break;
         }
      }
      return messageDefs;
   }

   /** Extract the list of operations from Specification. */
   private List extractOperations() {
      List results = new ArrayList<>();

      // Iterate on specification "operations" nodes.
      Iterator> operations = rootSpecification.path("operations").fields();
      while (operations.hasNext()) {
         Map.Entry operationEntry = operations.next();
         String operationShortName = operationEntry.getKey();

         JsonNode operationNode = operationEntry.getValue();
         String action = operationNode.path("action").asText();
         if (VALID_VERBS.contains(action)) {
            // Build and add this operation to the list.
            Operation operation = buildValidOperation(operationShortName, action, operationNode);
            results.add(operation);
         }
      }

      return results;
   }

   /** Build a single operation having its name, action and Json. */
   private Operation buildValidOperation(String name, String action, JsonNode operationNode) {
      String operationName = action.toUpperCase() + " " + name;

      Operation operation = new Operation();
      operation.setName(operationName);
      operation.setMethod(action.toUpperCase());

      // Complete operation properties if any.
      if (operationNode.has(MetadataExtensions.MICROCKS_OPERATION_EXTENSION)) {
         MetadataExtractor.completeOperationProperties(operation,
               operationNode.path(MetadataExtensions.MICROCKS_OPERATION_EXTENSION));
      }

      // Look for bindings at the operation level.
      if (operationNode.has(BINDINGS)) {
         AsyncAPICommons.completeOperationLevelBindings(operation, operationNode.get(BINDINGS));
      }

      // Then, check the related channels and extract dispatching info and bindings.
      if (operationNode.path(CHANNEL_NODE).isObject()) {
         JsonNode channel = operationNode.get(CHANNEL_NODE);
         JsonNode channelNode = followRefIfAny(channel);

         if (channelNode.has(ADDRESS_NODE)) {
            String address = channelNode.get(ADDRESS_NODE).asText();
            if (AsyncAPICommons.channelAddressHasParts(address)) {
               operation.setDispatcher(DispatchStyles.URI_PARTS);
               operation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromStringPattern(address));
            }
            operation.addResourcePath(address);
         }

         if (channelNode.has(BINDINGS)) {
            AsyncAPICommons.completeChannelLevelBindings(operation, channelNode.get(BINDINGS));
         }
      }

      // Then, check the related messages and complete bindings.
      if (operationNode.path(MESSAGES).isArray()) {
         Iterator messages = operationNode.path(MESSAGES).elements();
         while (messages.hasNext()) {
            JsonNode messageInChannelNode = followRefIfAny(messages.next());
            JsonNode messageNode = followRefIfAny(messageInChannelNode);
            if (messageNode.has(BINDINGS)) {
               AsyncAPICommons.completeMessageLevelBindings(operation, messageNode.get(BINDINGS));
            }
         }
      }

      return operation;
   }

   /** If necessary, complete an operation and its messages dispatch information. */
   private void completeDispatchingCriteria(Operation operation, JsonNode operationNode,
         List eventMessages) {
      // Update dispatch information if necessary.
      if (DispatchStyles.URI_PARTS.equals(operation.getDispatcher())) {

         // Retrieve information on channel address and parameters.
         JsonNode channelNode = followRefIfAny(operationNode.get(CHANNEL_NODE));
         String address = channelNode.get(ADDRESS_NODE).asText();

         // We have 2 cases here: parameter value can be dynamic, expressed with a location in message
         // or parameter value can be static, expressed as an examples.
         List dynamicParameters = getDynamicParameters(channelNode);
         Map> parametersByMessage = getParametersByMessage(channelNode);

         ObjectMapper mapper = getObjectMapper(true);

         for (EventMessage eventMessage : eventMessages) {
            // Start initializing message parameter values with the static ones.
            Map parameterValues = parametersByMessage.getOrDefault(eventMessage.getName(),
                  new HashMap<>());

            // Extract each dynamic parameter with its value coming from message.
            for (AsyncAPIParameter parameter : dynamicParameters) {
               String parameterValue = null;

               try {
                  if (parameter.location().startsWith("$message.payload#")) {
                     String location = parameter.location().substring("$message.payload#".length());
                     JsonNode eventMessageRootNode = mapper.readTree(eventMessage.getContent());
                     parameterValue = eventMessageRootNode.at(location).asText();
                  }
               } catch (Exception e) {
                  log.warn("Failed to extract the location value in {} from message {} for operation {}",
                        parameter.location(), eventMessage.getName(), operation.getName());
                  log.warn("Pursuing with the other ones but dispatch will be incomplete");
               }
               if (parameterValue != null) {
                  parameterValues.put(parameter.name(), parameterValue);
               }
            }

            // UPdate operation resource paths and message dispatch criteria.
            String resourcePath = URIBuilder.buildURIFromPattern(address, parameterValues);
            operation.addResourcePath(resourcePath);
            eventMessage.setDispatchCriteria(DispatchCriteriaHelper.buildFromPartsMap(address, parameterValues));
         }
      }
   }

   /** Given a Channel Json node, get its dynamic parameter definitions (those having a location). */
   private List getDynamicParameters(JsonNode channelNode) {
      List results = new ArrayList<>();

      if (channelNode.path(PARAMETERS_NODE).isObject()) {
         Iterator> parameters = channelNode.get(PARAMETERS_NODE).fields();
         while (parameters.hasNext()) {
            Entry parameterEntry = parameters.next();
            JsonNode parameter = followRefIfAny(parameterEntry.getValue());

            String parameterName = parameterEntry.getKey();

            if (parameter.has(LOCATION_NODE)) {
               if (log.isDebugEnabled()) {
                  log.debug("Processing param {} for channel {}, with location {}", parameterName,
                        channelNode.get("address").asText(), parameter.get(LOCATION_NODE).asText());
               }
               results.add(new AsyncAPIParameter(parameterName, parameter.get(LOCATION_NODE).asText()));
            }
         }
      }
      return results;
   }

   /**
    * Given a Channel Json node, get its static parameter values (those without a location), organized by message
    * example name. Key of value map is parameter name. Value of value map is parameter value ;-)
    */
   private Map> getParametersByMessage(JsonNode channelNode) {
      Map> results = new HashMap<>();

      if (channelNode.path(PARAMETERS_NODE).isObject()) {
         Iterator> parameters = channelNode.get(PARAMETERS_NODE).fields();
         while (parameters.hasNext()) {
            Entry parameterEntry = parameters.next();
            JsonNode parameter = followRefIfAny(parameterEntry.getValue());
            String parameterName = parameterEntry.getKey();

            if (!parameter.has(LOCATION_NODE) && parameter.path(EXAMPLES_NODE).isArray()) {
               Iterator examples = parameter.get(EXAMPLES_NODE).elements();
               while (examples.hasNext()) {
                  String example = examples.next().asText();
                  if (example.contains(":")) {
                     String exampleKey = example.substring(0, example.indexOf(":"));
                     String exampleValue = example.substring(example.indexOf(":") + 1);

                     Map exampleParams = results.getOrDefault(exampleKey, new HashMap<>());

                     if (log.isDebugEnabled()) {
                        log.debug("Processing param {} for channel {} for message {}", parameterName,
                              channelNode.get("address").asText(), exampleKey);
                     }
                     exampleParams.put(parameterName, exampleValue);
                     results.put(exampleKey, exampleParams);
                  }
               }
            }
         }
      }
      return results;
   }


   /** Build a list of EventMessages from an operation Json node. */
   private List buildEventMessages(JsonNode operationNode, String defaultContentType) {
      List eventMessages = null;
      Iterator messages = operationNode.path(MESSAGES).elements();
      while (messages.hasNext()) {
         JsonNode operationMessageNode = messages.next();
         JsonNode messageInChannelNode = followRefIfAny(operationMessageNode);
         JsonNode messageNode = followRefIfAny(messageInChannelNode);

         // Get message content type.
         String contentType = defaultContentType;
         if (messageNode.has("contentType")) {
            contentType = messageNode.path("contentType").asText();
         }

         // Retrieve the messageName from message ref found in operation.
         String messageName = operationMessageNode.path("$ref").textValue();

         if (messageName != null && messageNode.has(EXAMPLES_NODE)) {
            // Compute a short message name if examples have no name attribute.
            messageName = messageName.substring(messageName.lastIndexOf("/") + 1);
            eventMessages = buildEventMessageFromExamples(messageName, contentType, messageNode.get(EXAMPLES_NODE));
         }
      }
      return eventMessages;
   }

   /** Build a list of EventMessages from a Message examples Json node. */
   private List buildEventMessageFromExamples(String messageName, String contentType,
         JsonNode examplesNode) {
      List exchanges = new ArrayList<>();

      Iterator examples = examplesNode.elements();
      while (examples.hasNext()) {
         JsonNode exampleNode = examples.next();

         EventMessage eventMessage = new EventMessage();
         // Use name attribute if present, otherwise generate from message name.
         if (exampleNode.has("name")) {
            eventMessage.setName(exampleNode.get("name").asText());
         } else {
            eventMessage.setName(messageName + "-" + (exchanges.size() + 1));
         }

         eventMessage.setMediaType(contentType);
         eventMessage.setContent(getExamplePayload(exampleNode));

         // Now complete with specified headers.
         List
headers = AsyncAPICommons.getExampleHeaders(exampleNode); for (Header header : headers) { eventMessage.addHeader(header); } exchanges.add(eventMessage); } return exchanges; } /** * Get the value of an example. This can be direct value field or those of followed $ref. */ private String getExamplePayload(JsonNode example) { if (example.has(EXAMPLE_PAYLOAD_NODE)) { return getValueString(example.path(EXAMPLE_PAYLOAD_NODE)); } if (example.has("$payloadRef")) { // $ref: '#/components/examples/param_laurent' String ref = example.path("$payloadRef").asText(); JsonNode component = rootSpecification.at(ref.substring(1)); return getExamplePayload(component); } return null; } private record AsyncAPIParameter(String name, String location) { } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy