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

zio.magic.macros.utils.ZLayerExprBuilder.scala Maven / Gradle / Ivy

The newest version!
package zio.magic.macros.utils

import zio.magic.macros.LayerCompose
import zio.magic.macros.graph.{Graph, GraphError, Node}
import zio.magic.macros.utils.ansi.AnsiStringOps
import zio.{Chunk, NonEmptyChunk}

final case class ZLayerExprBuilder[Key, A](
    graph: Graph[Key, A],
    showKey: Key => String,
    showExpr: A => String,
    abort: String => Nothing,
    warn: String => Unit,
    emptyExpr: A,
    composeH: (A, A) => A,
    composeV: (A, A) => A
) {
  def buildLayerFor(output: List[Key]): A =
    output match {
      case Nil => emptyExpr
      case output =>
        assertNoDuplicateOutputs()

        graph.buildComplete(output) match {
          case Left(errors) => reportGraphErrors(errors)

          case Right(composed) =>
            assertNoLeftovers(composed)
            composed.fold(emptyExpr, identity, composeH, composeV)
        }
    }

  private def assertNoLeftovers(layerCompose: LayerCompose[A]): Unit = {
    val used      = layerCompose.toSet
    val leftovers = graph.nodes.filterNot(node => used.contains(node.value))

    if (leftovers.nonEmpty) {
      val message = leftovers
        .map { node =>
          s"${"unused".underlined} ${showExpr(node.value).blue.bold}"
        }
        .mkString("\n")

      reportWarning(message)
    }
  }

  private def assertNoDuplicateOutputs(): Unit = {
    val outputMap: Map[Key, List[Node[Key, A]]] = (for {
      node   <- graph.nodes
      output <- node.outputs
    } yield output -> node)
      .groupBy(_._1)
      .map { case (key, value) => key -> value.map(_._2) }
      .filter(_._2.length >= 2)

    if (outputMap.nonEmpty) {
      val message = outputMap
        .map { case (output, nodes) =>
          s"${output.toString.cyan} is provided by multiple layers:\n" +
            nodes.map(node => "— " + showExpr(node.value).bold.cyan).mkString("\n")
        }
        .mkString("\n")

      reportErrorMessage(message)
    }
  }

  private def reportErrorMessage(errorMessage: String): Nothing = {
    val body = errorMessage.linesIterator
      .map { line =>
        if (line.trim().isEmpty())
          line
        else
          "❯ ".red + line
      }
      .mkString("\n")

    abort(s"""
              
${s"  ZLayer Wiring Error  ".red.inverted.bold}

$body

""")
  }

  private def reportWarning(errorMessage: String): Unit = {
    val body = errorMessage.linesIterator
      .map { line =>
        if (line.trim().isEmpty())
          line
        else
          "❯ ".yellow + line
      }
      .mkString("\n")

    warn(s"""

${s"  ZLayer Wiring Error  ".yellow.inverted.bold}

$body

""")
  }

  private def reportGraphErrors(errors: ::[GraphError[Key, A]]): Nothing = {
    val allErrors = sortErrors(errors)

    val errorMessage =
      allErrors
        .map(renderError)
        .mkString("\n\n")
        .linesIterator
        .mkString("\n")

    reportErrorMessage(errorMessage)
  }

  /** Return only the first level of circular dependencies, as these will be the most relevant.
    */
  private def sortErrors(errors: ::[GraphError[Key, A]]): Chunk[GraphError[Key, A]] = {
    val (circularDependencyErrors, otherErrors) =
      NonEmptyChunk.fromIterable(errors.head, errors.tail).distinct.partitionMap {
        case circularDependency: GraphError.CircularDependency[Key, A] => Left(circularDependency)
        case other                                                     => Right(other)
      }
    val sorted                = circularDependencyErrors.sortBy(_.depth)
    val initialCircularErrors = sorted.takeWhile(_.depth == sorted.headOption.map(_.depth).getOrElse(0))

    val (transitiveDepErrors, remainingErrors) = otherErrors.partitionMap {
      case es: GraphError.MissingTransitiveDependencies[Key, A] => Left(es)
      case other                                                => Right(other)
    }

    val groupedTransitiveErrors = transitiveDepErrors.groupBy(_.node).map { case (node, errors) =>
      val deps = errors.flatMap(_.dependency)
      GraphError.MissingTransitiveDependencies(node, deps)
    }

    initialCircularErrors ++ groupedTransitiveErrors ++ remainingErrors
  }

  private def renderError(error: GraphError[Key, A]): String =
    error match {
      case GraphError.MissingTransitiveDependencies(node, dependencies) =>
        val styledDependencies = dependencies.zipWithIndex
          .map { case (dep, idx) =>
            val prefix = if (idx == 0) "missing".underlined else " " * 7
            val styled = showKey(dep).blue.bold
            s"""${prefix} $styled"""
          }
          .mkString("\n")
        val styledLayer = showExpr(node.value).blue
        s"""$styledDependencies
    ${"for".underlined} $styledLayer"""

      case GraphError.MissingTopLevelDependency(dependency) =>
        val styledDependency = showKey(dependency).blue.bold
        s"""${"missing".underlined} $styledDependency"""

      case GraphError.CircularDependency(node, dependency, _) =>
        val styledNode       = showExpr(node.value).blue.bold
        val styledDependency = showExpr(dependency.value).blue
        s"""
${"Circular Dependency".blue} 
$styledNode both requires ${"and".bold} is transitively required by $styledDependency"""
    }
}

object ZLayerExprBuilder extends ExprGraphCompileVariants {}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy