zio.magic.macros.utils.ZLayerExprBuilder.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of zio-magic-macros_2.13 Show documentation
Show all versions of zio-magic-macros_2.13 Show documentation
Magically construct ZLayers at compile-time (with friendly errors)
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 {}