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

pl.touk.nussknacker.engine.compile.PartSubGraphCompiler.scala Maven / Gradle / Ivy

The newest version!
package pl.touk.nussknacker.engine.compile

import cats.Applicative
import cats.data.Validated._
import cats.data.{NonEmptyList, ValidatedNel}
import cats.instances.list._
import cats.instances.option._
import pl.touk.nussknacker.engine.api.context.ProcessCompilationError._
import pl.touk.nussknacker.engine.api.context.{OutputVar, ProcessCompilationError, ValidationContext}
import pl.touk.nussknacker.engine.api.definition.Parameter
import pl.touk.nussknacker.engine.api.expression.ExpressionTypingInfo
import pl.touk.nussknacker.engine.api.typed.typing.Unknown
import pl.touk.nussknacker.engine.api.{JobData, MetaData, NodeId}
import pl.touk.nussknacker.engine.compile.nodecompilation.NodeCompiler
import pl.touk.nussknacker.engine.compile.nodecompilation.NodeCompiler.NodeCompilationResult
import pl.touk.nussknacker.engine.compiledgraph
import pl.touk.nussknacker.engine.compiledgraph.node
import pl.touk.nussknacker.engine.compiledgraph.node.{FragmentUsageEnd, Node}
import pl.touk.nussknacker.engine.graph.node._
import pl.touk.nussknacker.engine.splittedgraph._
import pl.touk.nussknacker.engine.splittedgraph.splittednode.{Next, SplittedNode}

class PartSubGraphCompiler(nodeCompiler: NodeCompiler) {

  import CompilationResult._

  def validate(n: splittednode.SplittedNode[_], ctx: ValidationContext)(
      implicit jobData: JobData
  ): CompilationResult[Unit] = {
    compile(n, ctx).map(_ => ())
  }

  /* TODO:
  1. Separate validation logic for expressions in nodes and expression not bounded to nodes (e.g. expressions in process properties).
     This way we can make non-optional fieldName
   */
  def compile(n: SplittedNode[_], ctx: ValidationContext)(
      implicit jobData: JobData
  ): CompilationResult[compiledgraph.node.Node] = {
    implicit val nodeId: NodeId = NodeId(n.id)

    def toCompilationResult[T](
        validated: ValidatedNel[ProcessCompilationError, T],
        expressionsTypingInfo: Map[String, ExpressionTypingInfo]
    ) =
      CompilationResult(Map(n.id -> NodeTypingInfo(ctx, expressionsTypingInfo, None)), validated)

    n match {
      case splittednode.SourceNode(nodeData, next)          => handleSourceNode(nodeData, ctx, next)
      case splittednode.OneOutputSubsequentNode(data, next) => compileSubsequent(ctx, data, next)

      case splittednode.SplitNode(bareNode, nexts) =>
        val compiledNexts = nexts.map(n => compile(n, ctx)).sequence
        compiledNexts.andThen(nx =>
          toCompilationResult(Valid(compiledgraph.node.SplitNode(bareNode.id, nx)), Map.empty)
        )

      case splittednode.FilterNode(f @ Filter(id, _, _, _), nextTrue, nextFalse) =>
        val NodeCompilationResult(typingInfo, _, _, compiledExpression, _) =
          nodeCompiler.compileFilter(f, ctx)

        CompilationResult.map3(
          f0 = toCompilationResult(compiledExpression, typingInfo),
          f1 = nextTrue.map(next => compile(next, ctx)).sequence,
          f2 = nextFalse.map(next => compile(next, ctx)).sequence
        )((expr, next, nextFalse) =>
          compiledgraph.node.Filter(
            id = id,
            expression = expr,
            nextTrue = next,
            nextFalse = nextFalse,
            isDisabled = f.isDisabled.contains(true)
          )
        )

      case splittednode.SwitchNode(Switch(id, expression, varName, _), nexts, defaultNext) =>
        val result = nodeCompiler.compileSwitch(
          Applicative[Option].product(varName, expression),
          nexts.map(c => (c.node.id, c.expression)),
          ctx
        )
        val contextAfter = result.validationContext.getOrElse(ctx)

        CompilationResult.map4(
          f0 = CompilationResult(result.validationContext),
          f1 = toCompilationResult(result.compiledObject, result.expressionTypingInfo),
          f2 = nexts.map(caseNode => compile(caseNode.node, contextAfter)).sequence,
          f3 = defaultNext.map(dn => compile(dn, contextAfter)).sequence
        ) { case (_, (expr, caseExpressions), cases, defaultNext) =>
          val compiledCases = caseExpressions.zip(cases).map(k => compiledgraph.node.Case(k._1, k._2))
          compiledgraph.node.Switch(id, Applicative[Option].product(varName, expr), compiledCases, defaultNext)
        }
      case splittednode.EndingNode(data) => compileEndingNode(ctx, data)

    }
  }

  private def handleSourceNode(nodeData: StartingNodeData, ctx: ValidationContext, next: splittednode.Next)(
      implicit jobData: JobData
  ): CompilationResult[node.Source] = {
    // just like in a custom node we can't add input context here because it contains output variable context (not input)
    nodeData match {
      case Source(id, ref, _) =>
        compile(next, ctx).map(nwc => compiledgraph.node.Source(id, Some(ref.typ), nwc))
      case Join(id, _, _, _, _, _) =>
        compile(next, ctx).map(nwc => compiledgraph.node.Source(id, None, nwc))
      case FragmentInputDefinition(id, _, _) =>
        // TODO: should we recognize we're compiling only fragment?
        compile(next, ctx).map(nwc => compiledgraph.node.Source(id, None, nwc))
    }
  }

  private def compileEndingNode(
      ctx: ValidationContext,
      data: EndingNodeData
  )(implicit nodeId: NodeId, jobData: JobData): CompilationResult[compiledgraph.node.Node] = {
    def toCompilationResult[T](
        validated: ValidatedNel[ProcessCompilationError, T],
        expressionsTypingInfo: Map[String, ExpressionTypingInfo],
        parameters: Option[List[Parameter]]
    ) =
      CompilationResult(Map(nodeId.id -> NodeTypingInfo(ctx, expressionsTypingInfo, parameters)), validated)

    data match {
      case processor @ Processor(id, _, disabled, _) =>
        val NodeCompilationResult(typingInfo, parameters, _, validatedServiceRef, _) =
          nodeCompiler.compileProcessor(processor, ctx)
        toCompilationResult(
          validatedServiceRef.map(ref => compiledgraph.node.EndingProcessor(id, ref, disabled.contains(true))),
          typingInfo,
          parameters
        )

      case Sink(id, ref, _, disabled, _) =>
        toCompilationResult(Valid(compiledgraph.node.Sink(id, ref.typ, disabled.contains(true))), Map.empty, None)

      case CustomNode(id, _, nodeType, _, _) =>
        toCompilationResult(Valid(compiledgraph.node.EndingCustomNode(id, nodeType)), Map.empty, None)

      // probably this shouldn't occur - otherwise we'd have empty fragment?
      case FragmentInput(id, _, _, _, _) =>
        toCompilationResult(Invalid(NonEmptyList.of(UnresolvedFragment(id))), Map.empty, None)

      case FragmentOutputDefinition(id, _, List(), _) =>
        // TODO: should we validate it's process?
        // TODO: does it make sense to validate FragmentOutput?
        toCompilationResult(
          Valid(compiledgraph.node.FragmentOutput(id, Map.empty, isDisabled = false)),
          Map.empty,
          None
        )
      case FragmentOutputDefinition(id, _, fields, _) =>
        val fieldTypedExpressions = nodeCompiler.fieldToTypedExpression(fields, ctx)
        toCompilationResult(
          fieldTypedExpressions.map(typedExpressions =>
            compiledgraph.node.FragmentOutput(id, typedExpressions, isDisabled = false)
          ),
          expressionsTypingInfo = Map.empty,
          parameters = None
        )
      // TODO JOIN: a lot of additional validations needed here - e.g. that join with that name exists, that it
      // accepts this join, maybe we should also validate the graph is connected?
      case BranchEndData(definition) =>
        toCompilationResult(Valid(compiledgraph.node.BranchEnd(definition)), Map.empty, None)
    }
  }

  private def compileSubsequent(ctx: ValidationContext, data: OneOutputSubsequentNodeData, next: Next)(
      implicit nodeId: NodeId,
      jobData: JobData
  ): CompilationResult[Node] = {
    def toCompilationResult[T](
        validated: ValidatedNel[ProcessCompilationError, T],
        expressionsTypingInfo: Map[String, ExpressionTypingInfo],
        parameters: Option[List[Parameter]]
    ) =
      CompilationResult(Map(data.id -> NodeTypingInfo(ctx, expressionsTypingInfo, parameters)), validated)

    data match {
      case variable @ Variable(id, varName, _, _) =>
        val NodeCompilationResult(typingInfo, parameters, newCtx, compiledExpression, t) =
          nodeCompiler.compileVariable(variable, ctx)
        CompilationResult.map3(
          f0 = CompilationResult(newCtx),
          f1 = toCompilationResult(compiledExpression, typingInfo, parameters),
          f2 = compile(next, newCtx.getOrElse(ctx))
        ) { (_, compiled, compiledNext) =>
          compiledgraph.node.VariableBuilder(id, varName, Left(compiled), compiledNext)
        }
      case VariableBuilder(id, varName, fields, _) =>
        val NodeCompilationResult(typingInfo, parameters, newCtxV, compiledFields, _) =
          nodeCompiler.compileFields(fields, ctx, outputVar = Some(OutputVar.variable(varName)))
        CompilationResult.map3(
          f0 = CompilationResult(newCtxV),
          f1 = toCompilationResult(compiledFields, typingInfo, parameters),
          f2 = compile(next, newCtxV.getOrElse(ctx))
        ) { (_, compiledFields, compiledNext) =>
          compiledgraph.node.VariableBuilder(id, varName, Right(compiledFields), compiledNext)
        }

      case processor @ Processor(id, _, isDisabled, _) =>
        val NodeCompilationResult(typingInfo, parameters, _, validatedServiceRef, _) =
          nodeCompiler.compileProcessor(processor, ctx)
        CompilationResult.map2(toCompilationResult(validatedServiceRef, typingInfo, parameters), compile(next, ctx))(
          (ref, next) => compiledgraph.node.Processor(id, ref, next, isDisabled.contains(true))
        )

      case enricher @ Enricher(id, _, output, _) =>
        val NodeCompilationResult(typingInfo, parameters, newCtx, validatedServiceRef, _) =
          nodeCompiler.compileEnricher(enricher, ctx, outputVar = OutputVar.enricher(output))

        CompilationResult.map3(
          toCompilationResult(validatedServiceRef, typingInfo, parameters),
          CompilationResult(newCtx),
          compile(next, newCtx.getOrElse(ctx))
        )((ref, _, next) => compiledgraph.node.Enricher(id, ref, output, next))

      // here we don't do anything, in subgraphcompiler it's just pass through, we can't add input context here because it contains output variable context (not input)
      case CustomNode(id, _, nodeType, _, _) =>
        CompilationResult.map(fa = compile(next, ctx))(
          f = compiledNext => compiledgraph.node.CustomNode(id, nodeType, compiledNext)
        )

      case fragmentInput: FragmentInput =>
        val NodeCompilationResult(typingInfo, parameters, newCtx, combinedValidParams, _) =
          nodeCompiler.compileFragmentInput(fragmentInput, ctx)
        CompilationResult.map2(
          toCompilationResult(combinedValidParams, typingInfo, parameters),
          compile(next, newCtx.getOrElse(ctx))
        )((params, next) => compiledgraph.node.FragmentUsageStart(fragmentInput.id, params, next))

      case FragmentUsageOutput(id, outputName, None, _) =>
        // Missing 'parent context' means that fragment has used some component which cleared context. We compile next parts using empty context (but with copied global variables).
        val parentContext = ctx.popContextOrEmptyWithGlobals()
        compile(next, parentContext)
          .andThen(compiledNext =>
            toCompilationResult(Valid(FragmentUsageEnd(id, None, compiledNext)), Map.empty, None)
          )
      case FragmentUsageOutput(id, outputName, Some(outputVar), _) =>
        val NodeCompilationResult(typingInfo, parameters, ctxWithSubOutV, compiledFields, typingResult) =
          nodeCompiler.compileFields(outputVar.fields, ctx, outputVar = None)
        // Missing 'parent context' means that fragment has used some component which cleared context. We compile next parts using empty context (but with copied global variables).
        val parentCtx = ctx.popContextOrEmptyWithGlobals()
        val parentCtxWithSubOut = parentCtx
          .withVariable(OutputVar.fragmentOutput(outputName, outputVar.name), typingResult.getOrElse(Unknown))

        CompilationResult.map4(
          f0 = CompilationResult(ctxWithSubOutV),
          f1 = CompilationResult(parentCtxWithSubOut),
          f2 = toCompilationResult(compiledFields, typingInfo, parameters),
          f3 = compile(next, parentCtxWithSubOut.getOrElse(parentCtx))
        ) { (_, _, compiledFields, compiledNext) =>
          compiledgraph.node.FragmentUsageEnd(
            id,
            Some(node.FragmentOutputVarDefinition(outputVar.name, compiledFields)),
            compiledNext
          )
        }
    }
  }

  private def compile(next: splittednode.Next, ctx: ValidationContext)(
      implicit jobData: JobData
  ): CompilationResult[compiledgraph.node.Next] = {
    next match {
      case splittednode.NextNode(n) => compile(n, ctx).map(cn => compiledgraph.node.NextNode(cn))
      case splittednode.PartRef(ref) =>
        CompilationResult(Map(ref -> NodeTypingInfo(ctx, Map.empty, None)), Valid(compiledgraph.node.PartRef(ref)))
    }
  }

  def withLabelsDictTyper: PartSubGraphCompiler =
    new PartSubGraphCompiler(nodeCompiler.withLabelsDictTyper)

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy