packages feed

graphwiz-3.0.0: src/Text/Dot/Monad.hs

module Text.Dot.Monad where

import "this" Prelude

import Control.Lens
import Control.Monad.Writer
import Data.HashMap.Strict  qualified as M
import Data.List.NonEmpty   qualified as NE
import Data.Text            qualified as T

import Text.Dot.Attributes
import Text.Dot.Types


--------------------------------------------------------------------------------
-- Dot monad

-- | Dot creation monad.
newtype DotT m a = DotT (DotGraph -> Path -> m (a, DotGraph))
  deriving
    ( Functor
    , Applicative
    , Monad
    , MonadReader Path
    , MonadState  DotGraph
    , MonadIO
    , MonadWriter w
    , MonadError  e
    ) via (StateT DotGraph (ReaderT Path m))

instance MonadTrans DotT where
  lift x = DotT \s _ -> fmap (,s) x

-- | An alias for @DotT Identity@.
type Dot = DotT Identity

-- | The constraint that all functions require.
--
-- We choose to express this as a constraint rather than a typeclass
-- for simplicity.
type MonadDot m = (MonadState DotGraph m, MonadReader Path m)

run :: Monad m => DotT m a -> m DotGraph
run action =
  let DotT f = action >> postProcess
  in  snd <$> f initialGraph (Path $ pure rootGraph)


--------------------------------------------------------------------------------
-- State manipulation

-- | Retrieve the current path.
--
-- The path is the stack of entities, representing the graph /
-- subgraphs / clusters between the root of the graph and the current
-- location.
--
-- > graph do
-- >   p1 <- currentPath     -- returns [$graphID]
-- >   subgraph do
-- >     cluster do
-- >       p2 <- currentPath -- returns [$clusterID, $subgraphID, $graphID]
-- >       doStuff
currentPath :: MonadDot m => m (NonEmpty Entity)
currentPath = asks unwrapPath

-- | Retrieves the unique ID of the last created t'Entity'.
itsID :: MonadDot m => m Entity
itsID = use latest

withPath :: MonadDot m => Entity -> m a -> m a
withPath e = local (_Path <>:~ pure e)

context :: Lens' DotGraph DotContext
context f d = fmap go (f c)
  where
    (c :| cs) = d ^. contextStack
    go nc = d & contextStack .~ nc :| cs

popContext :: MonadDot m => m DotContext
popContext = do
  c <- use context
  contextStack %= NE.fromList . NE.tail
  pure c

postProcess :: Monad m => DotT m ()
postProcess = do
  fixedEdges <- traverse fixEdge =<< use edgeInfo
  edgeInfo .= fixedEdges
  where
    fixEdge (EdgeInfo a b _ _) = do
      ea <- resolve a
      eb <- resolve b
      na <- getEnd ea
      nb <- getEnd eb
      pure $ EdgeInfo (KnownNode ea) (KnownNode eb) na nb

    resolve = \case
      KnownNode   eid  -> pure eid
      UnknownNode name -> dereference name

    getEnd eid =
      case getType eid of
        Cluster -> do
          attributes rootGraph . compound ?= "true"
          locateNode eid
        _ -> pure Nothing

    locateNode eid = do
      dg <- get
      pure $ eid ^? visit dg

    visit dg f eid =
      case getType eid of
        Cluster  -> foldMapOf (subgraphInfo . at eid . traverse . traverse) (visit dg f) dg
        Subgraph -> foldMapOf (subgraphInfo . at eid . traverse . traverse) (visit dg f) dg
        Node     -> f eid
        Edge     -> mempty

dereference :: Monad m => Text -> DotT m Entity
dereference name = do
  uses entityRegister (M.lookup name) >>= \case
    Just eid -> pure eid
    Nothing -> do
      error $ concat
        [ "Text.Dot.Monad.retrieve: unknown entity \""
        , T.unpack name
        , "\"\nall names must be registered via a call to `register` or `registerItAs`"
        ]