Skip to content

Commit

Permalink
refactor(core): #188 Move topological sort away from Pillars object
Browse files Browse the repository at this point in the history
  • Loading branch information
rlemaitre committed Dec 3, 2024
1 parent c88f7cf commit 91d7c42
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 55 deletions.
53 changes: 1 addition & 52 deletions modules/core/src/main/scala/pillars/Pillars.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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
56 changes: 56 additions & 0 deletions modules/core/src/main/scala/pillars/graph.scala
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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"):
Expand All @@ -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"):
Expand Down

0 comments on commit 91d7c42

Please sign in to comment.