packages feed

hinagata-core-0.1.0.0: src/Hinagata/Fixture/Graph.hs

module Hinagata.Fixture.Graph
  ( GraphError (..),
    resolveFixtures,
  )
where

import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import Hinagata.Fixture.Types
import Hinagata.Prelude ()

data GraphError
  = DuplicateFixture !FixtureName
  | MissingFixture !FixtureName ![FixtureName]
  | FixtureCycle ![FixtureName]
  deriving stock (Eq, Show)

-- | Stable depth-first postorder: includes precede their consumer, each name
-- appears once, and requested roots keep caller order.
resolveFixtures :: [FixtureDefinition] -> [FixtureName] -> Either GraphError [FixtureDefinition]
resolveFixtures definitions roots = do
  definitionsByName <- buildIndex definitions
  (_, reverseOrder) <- visitMany definitionsByName [] Set.empty [] roots
  pure (reverse reverseOrder)

buildIndex :: [FixtureDefinition] -> Either GraphError (Map FixtureName FixtureDefinition)
buildIndex = foldl step (Right Map.empty)
  where
    step (Left failure) _ = Left failure
    step (Right definitionsByName) definition@FixtureDefinition {name} =
      if Map.member name definitionsByName
        then Left (DuplicateFixture name)
        else Right (Map.insert name definition definitionsByName)

visitMany ::
  Map FixtureName FixtureDefinition ->
  [FixtureName] ->
  Set FixtureName ->
  [FixtureDefinition] ->
  [FixtureName] ->
  Either GraphError (Set FixtureName, [FixtureDefinition])
visitMany _ _ visited output [] = Right (visited, output)
visitMany definitionsByName stack visited output (next : rest) = do
  (visited', output') <- visit definitionsByName stack visited output next
  visitMany definitionsByName stack visited' output' rest

visit ::
  Map FixtureName FixtureDefinition ->
  [FixtureName] ->
  Set FixtureName ->
  [FixtureDefinition] ->
  FixtureName ->
  Either GraphError (Set FixtureName, [FixtureDefinition])
visit definitionsByName stack visited output current
  | current `elem` stack = Left (FixtureCycle (dropWhile (/= current) (reverse stack) ++ [current]))
  | Set.member current visited = Right (visited, output)
  | otherwise = case Map.lookup current definitionsByName of
      Nothing -> Left (MissingFixture current (reverse stack ++ [current]))
      Just definition@FixtureDefinition {includes} -> do
        (visited', output') <- visitMany definitionsByName (current : stack) visited output includes
        pure (Set.insert current visited', definition : output')