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

pl.touk.nussknacker.engine.Interpreter.scala Maven / Gradle / Ivy

package pl.touk.nussknacker.engine

import cats.Monad
import cats.effect.IO
import cats.syntax.all._
import com.github.ghik.silencer.silent
import pl.touk.nussknacker.engine.Interpreter._
import pl.touk.nussknacker.engine.api._
import pl.touk.nussknacker.engine.api.exception.NuExceptionInfo
import pl.touk.nussknacker.engine.api.process.{ComponentUseCase, ServiceExecutionContext}
import pl.touk.nussknacker.engine.compiledgraph.node._
import pl.touk.nussknacker.engine.compiledgraph.service._
import pl.touk.nussknacker.engine.compiledgraph.variable._
import pl.touk.nussknacker.engine.expression.ExpressionEvaluator
import pl.touk.nussknacker.engine.expression.parse.CompiledExpression
import pl.touk.nussknacker.engine.node.NodeComponentInfoExtractor
import pl.touk.nussknacker.engine.util.SynchronousExecutionContextAndIORuntime

import scala.concurrent.{ExecutionContext, Future}
import scala.language.higherKinds
import scala.util.control.NonFatal
import scala.util.{Failure, Success}

// TODO Interpreter and things around for sake of clarity of responsibilities between modules should be moved to the separate
//      module used only on runtime side. In the scenario-compiler module would stay only things responsible for
//      for compilation of graph into the runtime logic of components referenced by nodes
private class InterpreterInternal[F[_]: Monad](
    listeners: Seq[ProcessListener],
    expressionEvaluator: ExpressionEvaluator,
    interpreterShape: InterpreterShape[F],
    componentUseCase: ComponentUseCase,
    serviceExecutionContext: ServiceExecutionContext
)(implicit jobData: JobData) {

  type Result[T] = Either[T, NuExceptionInfo[_ <: Throwable]]

  private val expressionName = "expression"

  def interpret(node: Node, ctx: Context): F[List[Result[InterpretationResult]]] = {
    try {
      interpretNode(node, ctx)
    } catch {
      case NonFatal(ex) =>
        Monad[F].pure(List(Right(handleError(node, ctx)(ex))))
    }
  }

  private implicit def nodeToId(implicit node: Node): NodeId = NodeId(node.id)

  private def handleError(node: Node, ctx: Context): Throwable => NuExceptionInfo[_ <: Throwable] = {
    NuExceptionInfo(Some(NodeComponentInfoExtractor.fromCompiledNode(node)), _, ctx)
  }

  @silent("deprecated")
  private def interpretNode(node: Node, ctx: Context): F[List[Result[InterpretationResult]]] = {
    implicit val nodeImplicit: Node = node
    node match {
      // We do not invoke listener 'nodeEntered' here for nodes which are wrapped in PartRef by ProcessSplitter.
      // These are handled in interpretNext method
      case CustomNode(_, _, _) | EndingCustomNode(_, _) | Sink(_, _, _) | FragmentOutput(_, _, _) =>
      case _ => listeners.foreach(_.nodeEntered(node.id, ctx, jobData.metaData))
    }
    node match {
      case Source(_, _, next) =>
        interpretNext(next, ctx)
      case VariableBuilder(_, varName, Right(fields), next) =>
        val variable = createOrUpdateVariable(ctx, varName, fields)
        interpretNext(next, variable)
      case VariableBuilder(_, varName, Left(expression), next) =>
        val valueWithModifiedContext = expressionEvaluator.evaluate[Any](expression, varName, node.id, ctx)
        interpretNext(next, ctx.withVariable(varName, valueWithModifiedContext.value))
      case FragmentUsageStart(_, params, next) =>
        val (newCtx, vars) = expressionEvaluator.evaluateParameters(params, ctx)
        interpretNext(next, newCtx.pushNewContext(vars.map { case (paramName, value) => (paramName.value, value) }))
      case FragmentUsageEnd(_, outputVar, next) =>
        // Here we need parent context so we can compile rest of scenario. Unfortunately some component inside fragment
        // could've cleared that context. In that case, we take current (fragment's) context so we can keep the id,
        // clear it's variables, and keep using it in further processing.
        val parentContext = ctx.parentContext.getOrElse(ctx.copy(variables = Map.empty))
        val newParentContext = outputVar match {
          case Some(FragmentOutputVarDefinition(varName, fields)) =>
            val parsedFieldsMap = evaluateFragmentOutput(ctx, fields)
            parentContext.withVariable(varName, parsedFieldsMap)
          case None => parentContext
        }
        interpretNext(next, newParentContext)
      case Processor(_, ref, next, false) =>
        invokeWrappedInInterpreterShape(ref, ctx).flatMap {
          // for Processor the result is null/BoxedUnit/Void etc. so we ignore it
          case Left(ValueWithContext(_, newCtx)) => interpretNext(next, newCtx)
          case Right(exInfo)                     => Monad[F].pure(List(Right(exInfo)))
        }
      case Processor(_, _, next, true) => interpretNext(next, ctx)
      case EndingProcessor(id, ref, false) =>
        listeners.foreach(_.endEncountered(id, ref.id, ctx, jobData.metaData))
        invokeWrappedInInterpreterShape(ref, ctx).map {
          // for Processor the result is null/BoxedUnit/Void etc. so we ignore it
          case Left(ValueWithContext(_, newCtx)) =>
            List(Left(InterpretationResult(EndReference(id), newCtx)))
          case Right(exInfo) => List(Right(exInfo))
        }
      case EndingProcessor(id, _, true) =>
        Monad[F].pure(List(Left(InterpretationResult(EndReference(id), ctx))))
      case FragmentOutput(id, _, true) =>
        Monad[F].pure(List(Left(InterpretationResult(FragmentEndReference(id, Map.empty), ctx))))
      case FragmentOutput(id, fieldsWithExpression, false) =>
        fieldsWithExpression.toList
          .traverse(a =>
            Either
              .catchNonFatal(a._1 -> expressionEvaluator.evaluate(a._2.expression, a._1, id, ctx).value)
              .toValidatedNel
          )
          .map(_.toMap)
          .fold(
            exceptions => {
              listeners.foreach(_.nodeEntered(node.id, ctx, jobData.metaData))
              Monad[F].pure(exceptions.toList.map(exc => Right(handleError(node, ctx)(exc))))
            },
            fields => {
              val newCtx = ctx.withVariables(fields)
              listeners.foreach(_.nodeEntered(node.id, newCtx, jobData.metaData))
              Monad[F].pure(List(Left(InterpretationResult(FragmentEndReference(id, fields), newCtx))))
            }
          )
      case Enricher(_, ref, outName, next) =>
        invokeWrappedInInterpreterShape(ref, ctx).flatMap {
          case Left(ValueWithContext(out, newCtx)) => interpretNext(next, newCtx.withVariable(outName, out))
          case Right(exInfo)                       => Monad[F].pure(List(Right(exInfo)))
        }
      case Filter(_, expression, nextTrue, nextFalse, disabled) =>
        val valueWithModifiedContext =
          if (disabled) ValueWithContext(true, ctx) else evaluateExpression[Boolean](expression, ctx, expressionName)
        if (disabled || valueWithModifiedContext.value)
          interpretOptionalNext(node, nextTrue, valueWithModifiedContext.context)
        else
          interpretOptionalNext(node, nextFalse, valueWithModifiedContext.context)
      case Switch(_, expr, nexts, defaultNext) =>
        val newCtx = expr
          .map { case (exprVal, expression) =>
            val vmc = evaluateExpression[Any](expression, ctx, expressionName)
            vmc.context.withVariable(exprVal, vmc.value)
          }
          .getOrElse(ctx)

        nexts.zipWithIndex.foldLeft((newCtx, Option.empty[Next])) { case (acc, (casee, i)) =>
          acc match {
            case (accCtx, None) =>
              val valueWithModifiedContext =
                evaluateExpression[Boolean](casee.expression, accCtx, s"$expressionName-$i")
              if (valueWithModifiedContext.value) {
                (valueWithModifiedContext.context, Some(casee.node))
              } else {
                (valueWithModifiedContext.context, None)
              }
            case a => a
          }
        } match {
          case (accCtx, Some(nextNode)) =>
            interpretNext(nextNode, accCtx)
          case (accCtx, None) =>
            interpretOptionalNext(node, defaultNext, accCtx)
        }
      case Sink(id, _, true) =>
        Monad[F].pure(List(Left(InterpretationResult(EndReference(id), ctx))))
      case Sink(id, ref, false) =>
        listeners.foreach(_.endEncountered(id, ref, ctx, jobData.metaData))
        Monad[F].pure(List(Left(InterpretationResult(EndReference(id), ctx))))
      case BranchEnd(e) =>
        Monad[F].pure(List(Left(InterpretationResult(e.joinReference, ctx))))
      case CustomNode(_, _, next) =>
        interpretNext(next, ctx)
      case EndingCustomNode(id, ref) =>
        listeners.foreach(_.endEncountered(id, ref, ctx, jobData.metaData))
        Monad[F].pure(List(Left(InterpretationResult(EndReference(id), ctx))))
      case SplitNode(_, nexts) =>
        import cats.implicits._
        nexts.map(interpretNext(_, ctx)).sequence.map(_.flatten)
    }
  }

  private def interpretOptionalNext(
      node: Node,
      optionalNext: Option[Next],
      ctx: Context
  ): F[List[Result[InterpretationResult]]] = {
    optionalNext match {
      case Some(next) =>
        interpretNext(next, ctx)
      case None =>
        listeners.foreach(_.deadEndEncountered(node.id, ctx, jobData.metaData))
        Monad[F].pure(List(Left(InterpretationResult(DeadEndReference(node.id), ctx))))
    }
  }

  private def interpretNext(next: Next, ctx: Context): F[List[Result[InterpretationResult]]] =
    next match {
      case NextNode(node) => interpret(node, ctx)
      case pr @ PartRef(ref) => {
        listeners.foreach(_.nodeEntered(pr.id, ctx, jobData.metaData))
        Monad[F].pure(List(Left(InterpretationResult(NextPartReference(ref), ctx))))
      }
    }

  private def createOrUpdateVariable(ctx: Context, varName: String, fields: Seq[Field])(
      implicit node: Node
  ): Context = {
    val contextWithInitialVariable =
      ctx.modifyOptionalVariable[java.util.Map[String, Any]](varName, _.getOrElse(new java.util.HashMap[String, Any]()))

    fields.foldLeft(contextWithInitialVariable) { case (context, field) =>
      val valueWithContext = expressionEvaluator.evaluate[Any](field.expression, field.name, node.id, context)
      valueWithContext.context.modifyVariable[java.util.Map[String, Any]](
        varName,
        { m =>
          val newMap = new java.util.HashMap[String, Any](m)
          newMap.put(field.name, valueWithContext.value)
          newMap
        }
      )
    }
  }

  // We need java HashMap here because spel evaluator will fail on scala Map
  private def evaluateFragmentOutput(ctx: Context, fields: Seq[Field])(
      implicit node: Node
  ): java.util.HashMap[String, Any] = {
    {
      import scala.jdk.CollectionConverters._
      val fieldsMap = fields
        .map(field => (field.name, expressionEvaluator.evaluate[Any](field.expression, field.name, node.id, ctx).value))
        .toMap
        .asJava

      new java.util.HashMap[String, Any](fieldsMap)
    }
  }

  private def invokeWrappedInInterpreterShape(ref: ServiceRef, ctx: Context)(
      implicit node: Node
  ): F[Result[ValueWithContext[Any]]] = {
    interpreterShape.fromFuture
      .apply(invoke(ref, ctx)(node))
      .map {
        case Right(ex)   => Right(handleError(node, ctx)(ex))
        case Left(value) => Left(value)
      }
  }

  private def invoke(ref: ServiceRef, ctx: Context)(implicit node: Node) = {
    implicit val implicitComponentUseCase: ComponentUseCase = componentUseCase
    val resultFuture                                        = ref.invoke(ctx, serviceExecutionContext)
    import SynchronousExecutionContextAndIORuntime.syncEc
    resultFuture.onComplete { result =>
      listeners.foreach(_.serviceInvoked(node.id, ref.id, ctx, jobData.metaData, result))
    }
    resultFuture.map(ValueWithContext(_, ctx))
  }

  private def evaluateExpression[R](expr: CompiledExpression, ctx: Context, name: String)(
      implicit node: Node
  ): ValueWithContext[R] = {
    expressionEvaluator.evaluate(expr, name, node.id, ctx)
  }

}

class Interpreter(
    listeners: Seq[ProcessListener],
    expressionEvaluator: ExpressionEvaluator,
    componentUseCase: ComponentUseCase
) {

  def interpret[F[_]](
      node: Node,
      jobData: JobData,
      ctx: Context,
      serviceExecutionContext: ServiceExecutionContext
  )(
      implicit monad: Monad[F],
      shape: InterpreterShape[F],
  ): F[List[Either[InterpretationResult, NuExceptionInfo[_ <: Throwable]]]] = {
    implicit val jobDataImplicit: JobData = jobData
    new InterpreterInternal[F](listeners, expressionEvaluator, shape, componentUseCase, serviceExecutionContext)
      .interpret(node, ctx)
  }

}

object Interpreter {

  def apply(
      listeners: Seq[ProcessListener],
      expressionEvaluator: ExpressionEvaluator,
      componentUseCase: ComponentUseCase
  ): Interpreter = {
    new Interpreter(listeners, expressionEvaluator, componentUseCase)
  }

  object InterpreterShape {

    def transform[T](f: Future[T])(implicit ec: ExecutionContext): Future[Either[T, Throwable]] = f.transformWith {
      case Failure(exception) => Future.successful(Right(exception))
      case Success(value)     => Future.successful(Left(value))
    }

  }

  // Interpreter can be invoked with various effects, we require MonadError capabilities and ability to convert service invocation results
  trait InterpreterShape[F[_]] {

    def fromFuture[T]: Future[T] => F[Either[T, Throwable]]
  }

  import InterpreterShape._

  implicit object IOShape extends InterpreterShape[IO] {

    override def fromFuture[T]: Future[T] => IO[Either[T, Throwable]] = {
      implicit val ctx: ExecutionContext = SynchronousExecutionContextAndIORuntime.syncEc
      f => IO.fromFuture(IO.pure(transform(f)))
    }

  }

  implicit object FutureShape extends InterpreterShape[Future] {

    override def fromFuture[T]: Future[T] => Future[Either[T, Throwable]] = {
      implicit val ctx: ExecutionContext = SynchronousExecutionContextAndIORuntime.syncEc
      transform(_)
    }

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy