guardian-0.5.0.0: src/Development/Guardian/Graph/Adapter/Custom.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
module Development.Guardian.Graph.Adapter.Custom (
Custom,
buildPackageGraph,
CustomPackageOptions (..),
CustomAdapterException (..),
fromDotGraph,
) where
import qualified Algebra.Graph as G
import Control.Applicative (liftA2, (<|>))
import Control.Exception (Exception, throwIO)
import Control.Monad (when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson (FromJSON)
import qualified Data.Aeson as J
import qualified Data.Bifunctor as Bi
import qualified Data.CaseInsensitive as CI
import qualified Data.DList.DNonEmpty as DLNE
import Data.Function (on, (&))
import Data.Functor ((<&>))
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.Monoid (Ap (..))
import Data.String (IsString (..))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Encoding as LT
import Development.Guardian.Graph.Adapter.Types
import Development.Guardian.Types
import GHC.Generics (Generic)
import Language.Dot (parseDot)
import qualified Language.Dot as Dot
import Path (File, SomeBase, fromAbsDir, fromAbsFile)
import Path.IO (makeAbsolute)
import RIO (tshow)
import System.Environment (getEnvironment)
import System.Process.Typed (ProcessConfig, proc, readProcessStdout_, setEnv, shell)
import qualified Text.Parsec as Parsec
import Validation
data Custom
newtype instance CustomPackageOptions Custom = CustomOptions {custom :: CustomOpts}
deriving (Show, Eq, Ord, Generic)
deriving anyclass (J.FromJSON)
data CustomOpts = CustomOpts
{ adapter :: ExternalProcess
, ignoreLoop :: Bool
}
deriving (Show, Eq, Ord, Generic)
instance FromJSON CustomOpts where
parseJSON = J.withObject "dict" $ \obj -> do
adapter <-
Shell <$> (obj J..: "shell")
<|> Program <$> (obj J..: "program")
ignoreLoop <- obj J..:? "ignore_loop" J..!= False
pure CustomOpts {..}
data ExternalProcess = Shell Text | Program (SomeBase File)
deriving (Show, Eq, Ord, Generic)
createExternalAdapter ::
MonadIO m =>
PackageGraphOptions Custom ->
m (ProcessConfig () () ())
createExternalAdapter PackageGraphOptions {..} = do
env0 <- liftIO getEnvironment
cfg0 <- case adapter $ custom customOptions of
Shell txt ->
pure $ shell $ fromString $ T.unpack txt
Program prog -> do
progAbs <- makeAbsolute prog
pure $ proc (fromAbsFile progAbs) [fromAbsDir targetPath]
let componentEnvs =
[(includeTestVar, "1") | tests components]
++ [(includeBenchVar, "1") | benchmarks components]
env' =
("GUARDIAN_ROOT_DIR", fromAbsDir targetPath)
: componentEnvs
++ filter ((`notElem` [includeTestVar, includeBenchVar]) . fst) env0
pure $ cfg0 & setEnv env'
includeBenchVar :: String
includeBenchVar = "GUARDIAN_INCLUDE_BENCHMARKS"
includeTestVar :: String
includeTestVar = "GUARDIAN_INCLUDE_TESTS"
data CustomAdapterException
= InvalidAdapterOutput String Parsec.ParseError
| InvalidGraph String Dot.Graph (NonEmpty GraphViolation)
deriving (Eq, Generic)
deriving anyclass (Exception)
instance Show CustomAdapterException where
showsPrec d exc = showParen (d > 10) $
case exc of
InvalidAdapterOutput src pe ->
showString "Parse error in adapter output:\n"
. showString "Error: \n"
. showString (unlines $ map ('\t' :) $ lines $ show pe)
. showString "Input: \n"
. showString (unlines $ map ('\t' :) $ lines src)
InvalidGraph src gr ne ->
showString "Invalid Dot Graph was passed: "
. shows (NE.toList ne)
. showString "\nParsedGraph:\n\t"
. shows gr
. showString "\nInput:\n"
. showString (unlines $ map ('\t' :) $ lines src)
data GraphViolation
= DirectedGraphExpected
| EdgeToSubgraphNotSupported (Maybe Dot.Id)
| MultiEdgeNotSupported [String]
deriving (Show, Eq, Ord, Generic)
deriving anyclass (Exception)
buildPackageGraph :: PackageGraphOptions Custom -> IO PackageGraph
buildPackageGraph opts = do
pc <- createExternalAdapter opts
src <- LT.unpack . LT.decodeUtf8 <$> readProcessStdout_ pc
gr <- either (throwIO . InvalidAdapterOutput src) pure $ parseDot "<adapter output>" src
let pkgGr = fromDotGraph (custom $ customOptions opts) gr
either (throwIO . InvalidGraph src gr) pure pkgGr
data DotEntity = DotId Text | Pkg PackageName
deriving (Show, Eq, Ord, Generic)
fromDotGraph :: CustomOpts -> Dot.Graph -> Either (NonEmpty GraphViolation) PackageGraph
fromDotGraph CustomOpts {..} (Dot.Graph _ dir _ stmts) =
Bi.first DLNE.toNonEmpty $
validationToEither $
when
(dir /= Dot.DirectedGraph)
(failed DirectedGraphExpected)
*> getAp (foldMap (Ap . (liftA2 (,) <$> go <*> (pure . buildDic))) stmts)
<&> \(gr0, dic) ->
fmap
( \case
DotId txt -> fromMaybe (PackageName txt) $ Map.lookup txt dic
Pkg pn -> pn
)
gr0
where
buildDic (Dot.NodeStatement ni atts) =
Map.singleton (prettyNodeId ni) $ parseNode ni atts
buildDic _ = mempty
go (Dot.NodeStatement ni atts) =
pure $
G.vertex $
Pkg $
parseNode ni atts
go (Dot.EdgeStatement [l, r] _)
| ignoreLoop, parseEntity l == parseEntity r = mempty
| otherwise =
(G.connect `on` G.vertex) <$> parseEntity l <*> parseEntity r
go (Dot.EdgeStatement ents _) =
failed $ MultiEdgeNotSupported $ map (show . Dot.pp) ents
go Dot.AttributeStatement {} = pure mempty
go Dot.AssignmentStatement {} = pure mempty
go Dot.SubgraphStatement {} = mempty
prettyNodeId :: Dot.NodeId -> Text
prettyNodeId (Dot.NodeId ni _) = prettyId ni
parseNode :: Dot.NodeId -> [Dot.Attribute] -> PackageName
parseNode (Dot.NodeId origId _) attrs =
PackageName $
prettyId $
fromMaybe origId $
lookup
(CI.mk "label")
[ (CI.mk $ prettyId l, v)
| Dot.AttributeSetValue l v <- attrs
]
prettyId :: Dot.Id -> Text
prettyId (Dot.NameId s) = T.pack s
prettyId (Dot.StringId s) = T.pack s
prettyId (Dot.IntegerId n) = tshow n
prettyId (Dot.FloatId x) = tshow x
prettyId (Dot.XmlId xml) = tshow $ Dot.pp xml
failed :: e -> Validation (DLNE.DNonEmpty e) b
failed = Failure . DLNE.singleton
parseEntity :: Dot.Entity -> Validation (DLNE.DNonEmpty GraphViolation) DotEntity
parseEntity (Dot.ENodeId _ (Dot.NodeId ni _)) = pure $ DotId $ prettyId ni
parseEntity (Dot.ESubgraph _ sub) = failed $ EdgeToSubgraphNotSupported $ subGraphId sub
subGraphId :: Dot.Subgraph -> Maybe Dot.Id
subGraphId (Dot.NewSubgraph mid _) = mid
subGraphId (Dot.SubgraphRef ident) = Just ident