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

com.ing.baker.runtime.model.InteractionManager.scala Maven / Gradle / Ivy

The newest version!
package com.ing.baker.runtime.model

import cats.MonadError
import cats.effect.Sync
import cats.implicits._
import com.ing.baker.il.petrinet.InteractionTransition
import com.ing.baker.il.{EventDescriptor, IngredientDescriptor, checkpointEventInteractionPrefix}
import com.ing.baker.runtime.common.RecipeRecord
import com.ing.baker.runtime.model.recipeinstance.RecipeInstance.{FatalInteractionException, empty}
import com.ing.baker.runtime.scaladsl.{EventInstance, IngredientInstance, InteractionInstanceInput}
import com.ing.baker.types.Type
import com.typesafe.config.ConfigFactory

import scala.collection.immutable.Seq

/**
  * The InteractionManager is responsible for keeping and calling al InteractionInstances.
  * The other components of Baker will not call InteractionInstances and will always go through the InteractionManager.
  * @tparam F
  */
trait InteractionManager[F[_]] {

  protected def noneIfEmpty(str: String) = if (str.isEmpty) None else Some(str)

  /**
    * If this is set to true it will also allow fur supersets of the output types to be given by the implementations
    * This can be helpful in case an ENUM type or similar is extended upon and you know these new values will not be given.
    * If this new value is given from the implementation this will result in te runtime error and a technical failure of the interaction.
    */
  def allowSupersetForOutputTypes: Boolean = AllowSupersetForOutputTypes
  private lazy val AllowSupersetForOutputTypes: Boolean =
    ConfigFactory.load().getBoolean("baker.interactions.allow-superset-for-output-types")

  def listAll: F[List[InteractionInstance[F]]]

  def recipeAdded(recipeRecord: RecipeRecord)(implicit sync: Sync[F]): F[Unit] = Sync[F].unit

  def findFor(transition: InteractionTransition)(implicit sync: Sync[F]): F[Option[InteractionInstance[F]]] =
    listAll.flatMap(all => sync.delay(all.find(compatible(transition, _))))

  def existsFor(interaction: InteractionTransition)(implicit sync: Sync[F]): F[Boolean] = findFor(interaction).map(_.nonEmpty)

  def execute(interaction: InteractionTransition, input: Seq[IngredientInstance], metadata: Option[Map[String, String]])(implicit sync: Sync[F], effect: MonadError[F, Throwable]): F[Option[EventInstance]] = {
    if(interaction.interactionName.startsWith(checkpointEventInteractionPrefix)){
      effect.pure(Some(EventInstance(interaction.interactionName.stripPrefix(checkpointEventInteractionPrefix))))
    } else{
      findFor(interaction)
        .flatMap {
          case Some(implementation) => implementation.execute(input, metadata.getOrElse(Map()))
          case None => effect.raiseError(new FatalInteractionException(s"No implementation available for interaction ${interaction.interactionName}"))
        }
    }

  }

  private def interactionNameMatches(transition: InteractionTransition, implementation: InteractionInstance[F]): Boolean =
    transition.originalInteractionName == implementation.name || transition.interactionName == implementation.name

  private def inputSizeMatches(transition: InteractionTransition, implementation: InteractionInstance[F]): Boolean =
    implementation.input.size == transition.requiredIngredients.size

  private def inputNamesAndTypesMatches(transition: InteractionTransition, implementation: InteractionInstance[F]): Boolean =
    transition.requiredIngredients.forall(transitionIngredient => {
      implementation.input.exists(implementationIngredient => {
        //We cannot use the name of the interaction input since we do not store the original input name.
        //This means it will not bind on renamed ingredients.
        implementationIngredient.`type`.isAssignableFrom(transitionIngredient.`type`)
      }
      )
    })

  private def outputEventNamesAndTypesMatches(transition: InteractionTransition, implementation: InteractionInstance[F]): Boolean = {
    if (implementation.output.isDefined)
    //For the output it is allowed
      implementation.output.get.size <= transition.originalEvents.size &&
        implementation.output.exists(doesOutputMatch(_, transition.originalEvents))
    else true //If the implementation output is not defined the output validation should be not done
  }

  protected def compatible(transition: InteractionTransition, implementation: InteractionInstance[F]): Boolean = {
    interactionNameMatches(transition, implementation) &&
      inputSizeMatches(transition, implementation) &&
      inputNamesAndTypesMatches(transition, implementation) &&
      outputEventNamesAndTypesMatches(transition, implementation)
  }

  private def doesOutputMatch(implementationOutput: Map[String, Map[String, Type]], transitionOutput: Seq[EventDescriptor]): Boolean = {
    implementationOutput
      .forall { implEvent =>
        transitionOutput.exists(output =>
          output.name == implEvent._1 &&
          doesEventIngredientExists(implEvent._2, output.ingredients))
      }
  }

  private def doesEventIngredientExists(implementationIngredients: Map[String, Type], transitionIngredients: Seq[IngredientDescriptor]): Boolean = {
    transitionIngredients.forall(transitionIngredient =>
      implementationIngredients.exists(implementationIngredient =>
        transitionIngredient.name == implementationIngredient._1 &&
          (transitionIngredient.`type`.isAssignableFrom(implementationIngredient._2) ||
            allowSupersetForOutputTypes && implementationIngredient._2.isAssignableFrom(transitionIngredient.`type`))
      ))
  }

  sealed trait InteractionIncompatible
  case object NameNotFound extends InteractionIncompatible
  case class InteractionMatchInputSizeFailed(
                                              interactionName: String,
                                              transitionArgsSize: Int,
                                              implementationArgsSize: Int
                                            ) extends InteractionIncompatible {
    override def toString: String =
      s"$interactionName input size differs: transition expects $transitionArgsSize, implementation provides $implementationArgsSize"
  }

  case class InteractionMatchInputFailed(
                                         interactionName: String,
                                         transitionInputTypesMissing: Seq[IngredientDescriptor],
                                         implementationInputTypesExtra: Seq[InteractionInstanceInput]
                                       ) extends InteractionIncompatible {
    override def toString: String =
      s"$interactionName input types mismatch: transition expects $transitionInputTypesMissing, not provided by implementation, implementation provides extra: $implementationInputTypesExtra"
  }

  case class InteractionMatchOutputSizeFailed(interactionName: String,
                                              transitionArgsSize: Int,
                                              implementationArgsSize: Int) extends InteractionIncompatible {
    override def toString: String =
      s"$interactionName output size differs: transition expects $transitionArgsSize, implementation provides $implementationArgsSize"
  }

  case class InteractionMatchOutputNotFound(interactionName: String,
                                            eventDescriptors: Seq[EventDescriptor]) extends InteractionIncompatible {
    override def toString: String =
      s"$interactionName ouput mismatch: transition expects $eventDescriptors, not provided by implementation"
  }

  case class UnknownReason(interactionName: String) extends InteractionIncompatible {
    override def toString: String =
      s"$interactionName: unknown reason for no interaction matched"
  }

  def incompatibilities(transition: InteractionTransition)(implicit sync: Sync[F]): F[Seq[InteractionIncompatible]] = for {
    all <- listAll
  } yield {
    if(transition.originalInteractionName.startsWith(checkpointEventInteractionPrefix))
      Seq.empty
    else if (all.exists(compatible(transition, _)))
      Seq.empty
    else {
      all
        .filter(_.name == transition.originalInteractionName) match {
        case Nil => Seq(NameNotFound)
        case sameName => sameName.flatMap(incompatibilityReason(transition, _))
      }
    }
  }

  def incompatibilityReason(transition: InteractionTransition, implementation: InteractionInstance[F]): Option[InteractionIncompatible] =
    if (!inputSizeMatches(transition, implementation))
      Some(InteractionMatchInputSizeFailed(transition.interactionName, transition.requiredIngredients.size, implementation.input.size))
    else if (!inputNamesAndTypesMatches(transition, implementation)) {
      val missingTypes = transition.requiredIngredients.flatMap(i => {
        if (implementation.input.map(_.`type`).exists(_.isAssignableFrom(i.`type`))) None else Some(i)
      })
      val extraTypes = implementation.input.flatMap(i => {
        if (transition.requiredIngredients.map(_.`type`).exists(_.isAssignableFrom(i.`type`))) None else Some(i)
      })
      Some(InteractionMatchInputFailed(implementation.name, missingTypes, extraTypes))
    }
    else if (!outputEventNamesAndTypesMatches(transition, implementation))
      Some(InteractionMatchOutputNotFound(transition.interactionName, transition.originalEvents))
    else
      Some(UnknownReason("Unknown reason"))
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy