From 91d7c4202e17c39457711ddabb4fca2facd407c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Lemaitre?= Date: Mon, 2 Dec 2024 15:20:35 +0100 Subject: [PATCH] refactor(core): #188 Move topological sort away from Pillars object --- .../core/src/main/scala/pillars/Pillars.scala | 53 +----------------- .../core/src/main/scala/pillars/graph.scala | 56 +++++++++++++++++++ .../scala/pillars/TopologicalSortSuite.scala | 6 +- 3 files changed, 60 insertions(+), 55 deletions(-) create mode 100644 modules/core/src/main/scala/pillars/graph.scala diff --git a/modules/core/src/main/scala/pillars/Pillars.scala b/modules/core/src/main/scala/pillars/Pillars.scala index f4c7340b..d17dd389 100644 --- a/modules/core/src/main/scala/pillars/Pillars.scala +++ b/modules/core/src/main/scala/pillars/Pillars.scala @@ -11,13 +11,10 @@ import cats.syntax.all.* import fs2.io.file.Path import fs2.io.net.Network import io.circe.Decoder -import io.github.iltotore.iron.* import org.typelevel.otel4s.trace.Tracer import pillars.Config.PillarsConfig import pillars.Config.Reader -import pillars.PillarsError.Code -import pillars.PillarsError.ErrorNumber -import pillars.PillarsError.Message +import pillars.graph.* import pillars.probes.ProbeManager import pillars.probes.probesController import scribe.* @@ -135,52 +132,4 @@ object Pillars: end match end loadModules - extension [T](items: Seq[T]) - def topologicalSort(dependencies: T => Iterable[T]): Either[StartupError, List[T]] = - @annotation.tailrec - def loop( - remaining: Iterable[T], - sorted: List[T], - visited: Set[T], - recursionStack: Set[T] - ): Either[StartupError, List[T]] = - if remaining.isEmpty then Right(sorted) - else - val (allDepsResolved, hasUnresolvedDeps) = remaining.partition: value => - dependencies(value).forall(visited.contains) - if allDepsResolved.isEmpty then - if hasUnresolvedDeps.exists(recursionStack.contains) then - Left(StartupError.CyclicDependencyError) - else loop(hasUnresolvedDeps, sorted, visited, recursionStack ++ hasUnresolvedDeps) - else - loop( - hasUnresolvedDeps, - sorted ++ allDepsResolved.toList, - visited ++ allDepsResolved.toSet, - recursionStack - ) - end if - end if - end loop - - val missing = items.flatMap(dependencies).toSet -- items.toSet - if missing.nonEmpty then - Left(StartupError.MissingDependency(missing)) - else - loop(items, List.empty, Set.empty, Set.empty) - - enum StartupError(val number: ErrorNumber) extends PillarsError: - override def code: Code = Code("STARTUP") - - case CyclicDependencyError extends StartupError(ErrorNumber(1)) - case MissingDependency[T](missing: Set[T]) extends StartupError(ErrorNumber(2)) - - override def message: Message = this match - case StartupError.CyclicDependencyError => Message("Cyclic dependency found") - case StartupError.MissingDependency(missing) => - if missing.size == 1 then - Message(s"Missing dependency: ${missing.head}".assume) - else - Message(s"${missing.size} missing dependencies: ${missing.mkString(", ")}".assume) - end StartupError end Pillars diff --git a/modules/core/src/main/scala/pillars/graph.scala b/modules/core/src/main/scala/pillars/graph.scala new file mode 100644 index 00000000..8f9f3e71 --- /dev/null +++ b/modules/core/src/main/scala/pillars/graph.scala @@ -0,0 +1,56 @@ +package pillars + +import io.github.iltotore.iron.* +import pillars.PillarsError.Code +import pillars.PillarsError.ErrorNumber +import pillars.PillarsError.Message +object graph: + extension [T](items: Seq[T]) + def topologicalSort(dependencies: T => Iterable[T]): Either[GraphError, List[T]] = + @annotation.tailrec + def loop( + remaining: Iterable[T], + sorted: List[T], + visited: Set[T], + recursionStack: Set[T] + ): Either[GraphError, List[T]] = + if remaining.isEmpty then Right(sorted) + else + val (allDepsResolved, hasUnresolvedDeps) = remaining.partition: value => + dependencies(value).forall(visited.contains) + if allDepsResolved.isEmpty then + if hasUnresolvedDeps.exists(recursionStack.contains) then + Left(GraphError.CyclicDependencyError) + else loop(hasUnresolvedDeps, sorted, visited, recursionStack ++ hasUnresolvedDeps) + else + loop( + hasUnresolvedDeps, + sorted ++ allDepsResolved.toList, + visited ++ allDepsResolved.toSet, + recursionStack + ) + end if + end if + end loop + + val missing = items.flatMap(dependencies).toSet -- items.toSet + if missing.nonEmpty then + Left(GraphError.MissingDependency(missing)) + else + loop(items, List.empty, Set.empty, Set.empty) + + enum GraphError(val number: ErrorNumber) extends PillarsError: + override def code: Code = Code("GRAPH") + + case CyclicDependencyError extends GraphError(ErrorNumber(1)) + case MissingDependency[T](missing: Set[T]) extends GraphError(ErrorNumber(2)) + + override def message: Message = this match + case GraphError.CyclicDependencyError => Message("Cyclic dependency found") + case GraphError.MissingDependency(missing) => + if missing.size == 1 then + Message(s"Missing dependency: ${missing.head}".assume) + else + Message(s"${missing.size} missing dependencies: ${missing.mkString(", ")}".assume) + end GraphError +end graph diff --git a/modules/core/src/test/scala/pillars/TopologicalSortSuite.scala b/modules/core/src/test/scala/pillars/TopologicalSortSuite.scala index 35b69a8f..9adaaa7f 100644 --- a/modules/core/src/test/scala/pillars/TopologicalSortSuite.scala +++ b/modules/core/src/test/scala/pillars/TopologicalSortSuite.scala @@ -7,7 +7,7 @@ package pillars import munit.FunSuite class TopologicalSortSuite extends FunSuite: - import Pillars.* + import graph.* test("topologicalSort returns sorted list for acyclic graph defined as List"): val items: List[Char] = List('A', 'B', 'C', 'D', 'E') assertEquals( @@ -33,7 +33,7 @@ class TopologicalSortSuite extends FunSuite: case 'C' => List('A') case _ => ??? , - Left(StartupError.CyclicDependencyError) + Left(GraphError.CyclicDependencyError) ) test("topologicalSort returns error if a dependency is missing"): @@ -45,7 +45,7 @@ class TopologicalSortSuite extends FunSuite: case 'C' => List('A') case _ => ??? , - Left(StartupError.MissingDependency(Set('B'))) + Left(GraphError.MissingDependency(Set('B'))) ) test("topologicalSort returns sorted list for single node graph"):