packages feed

guardian-0.5.0.0: src/Development/Guardian/Graph.hs

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Development.Guardian.Graph where

import qualified Algebra.Graph as G
import qualified Algebra.Graph.Class as GC
import Algebra.Graph.Label (Path)
import qualified Algebra.Graph.Labelled as LG
import qualified Algebra.Graph.Relation as Rel
import qualified Algebra.Graph.Relation.Preorder as Preorder
import qualified Algebra.Graph.ToGraph as GC
import Control.Monad (guard, void)
import Data.Bifunctor (Bifunctor)
import qualified Data.Bifunctor as Bi
import Data.Coerce (coerce)
import qualified Data.DList as DL
import Data.DList.DNonEmpty (DNonEmpty)
import qualified Data.DList.DNonEmpty as DLNE
import qualified Data.HashMap.Strict as HM
import qualified Data.List.NonEmpty as NE
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Monoid (Ap (..))
import Data.Semigroup.Generic
import Data.Set (Set)
import qualified Data.Set as Set
import qualified Data.Vector as V
import Development.Guardian.Types
import GHC.Generics (Generic)
import Validation

buildDomainInfo :: Domains -> Validation (NE.NonEmpty DomainGraphError) DomainInfo
buildDomainInfo domainConfig = do
  let packageDic = buildPackageDic domainConfig
  domainGraph <- toDomainGraph domainConfig
  pure DomainInfo {..}

buildRawDomainGraph ::
  (GC.Graph gr, GC.Vertex gr ~ DomainName) =>
  Domains ->
  gr
buildRawDomainGraph Domains {..} =
  getOverlayed $
    HM.foldMapWithKey
      ( \dom Domain {..} ->
          Overlayed (GC.vertex dom)
            <> maybe mempty (foldMap (Overlayed . GC.edge dom)) dependsOn
      )
      getDomains

toDomainGraph :: Domains -> Validation (NE.NonEmpty DomainGraphError) DomainGraph
toDomainGraph doms =
  Bi.first DLNE.toNonEmpty $
    ans
      <$ Bi.first DLNE.singleton (detectCycle raw)
      <* Bi.first DLNE.singleton (detectPackageOverlaps doms)
  where
    !raw = buildRawDomainGraph doms
    !ans = Preorder.fromRelation raw

detectPackageOverlaps ::
  Domains -> Validation DomainGraphError ()
detectPackageOverlaps Domains {..}
  | Map.null overlaps = Success ()
  | otherwise = Failure $ OverlappingPackages overlaps
  where
    overlaps =
      Map.filter ((> 1) . Set.size) $
        getCatMap $
          HM.foldMapWithKey
            ( \dom ->
                foldMap
                  ( CatMap
                      . flip Map.singleton (Set.singleton dom)
                      . packageName
                  )
                  . packages
            )
            getDomains

newtype CatMap k v = CatMap {getCatMap :: Map k v}

instance (Semigroup v, Ord k) => Semigroup (CatMap k v) where
  (<>) = coerce $ Map.unionWith @k @v (<>)
  {-# INLINE (<>) #-}

instance (Semigroup v, Ord k) => Monoid (CatMap k v) where
  mempty = CatMap Map.empty
  {-# INLINE mempty #-}

detectCycle ::
  (GC.ToGraph t, Ord (GC.ToVertex t), GC.ToVertex t ~ DomainName) =>
  t ->
  Validation DomainGraphError ()
detectCycle gr =
  Bi.first CyclicDomainDep $
    eitherToValidation $
      void $
        GC.topSort gr

buildPackageDic :: Domains -> PackageDic
buildPackageDic =
  HM.foldMapWithKey
    ( \domName Domain {..} ->
        Map.fromList $
          map (\PackageDef {..} -> (packageName, (domName, extraDeps))) $
            V.toList packages
    )
    . getDomains

matches :: PackageDic -> Dependency -> PackageName -> Bool
matches pkgDic (DomainDep dn) pkg =
  maybe False ((dn ==) . fst) $ Map.lookup pkg pkgDic
matches _ (PackageDep pn) pkg = pn == pkg

newtype LOverlayed e a = LOverlayed {getLOverlayed :: LG.Graph e a}
  deriving (Show, Eq, Ord, Generic)

instance Monoid e => Semigroup (LOverlayed e a) where
  (<>) = coerce $ LG.overlay @e @a
  {-# INLINE (<>) #-}

instance Monoid e => Monoid (LOverlayed e a) where
  mempty = LOverlayed LG.empty
  {-# INLINE mempty #-}

data ActualGraphs a b = AGs {activatedGraph :: a, exceptionGraph :: b}
  deriving (Functor, Show, Eq, Ord, Generic)
  deriving (Semigroup, Monoid) via GenericSemigroupMonoid (ActualGraphs a b)

instance Bifunctor ActualGraphs where
  bimap f g (AGs x y) = AGs (f x) (g y)
  {-# INLINE bimap #-}
  first f (AGs x y) = AGs (f x) y
  {-# INLINE first #-}
  second g (AGs x y) = AGs x (g y)
  {-# INLINE second #-}

buildActualGraphs ::
  PackageDic ->
  PackageGraph ->
  Validation
    (DNonEmpty PackageViolation)
    (ActualGraphs ActualGraph (Map PackageName (Set Dependency)))
buildActualGraphs pkgDic =
  let pkgs = Map.keys pkgDic
   in fmap
        ( Bi.bimap
            (Bi.first DL.toList . getLOverlayed)
            (Map.filter (not . Set.null) . getCatMap)
        )
        . getAp
        . foldMap
          ( \e@(src, dst) -> Ap $ do
              src' <-
                maybe (failed $ OrphanPackage src pkgs) Success $
                  Map.lookup src pkgDic

              (dstDomain, _) <-
                maybe (failed $ OrphanPackage dst pkgs) Success $
                  Map.lookup dst pkgDic

              pure $
                let (srcDomain, srcExcept) = src'
                    aGraph = LOverlayed $ LG.edge (DL.singleton e) srcDomain dstDomain
                    excepts = Set.fromList $ V.toList $ V.filter (flip (matches pkgDic) dst) srcExcept
                 in if Set.null excepts
                      then AGs {exceptionGraph = mempty, activatedGraph = aGraph}
                      else AGs {activatedGraph = mempty, exceptionGraph = CatMap $ Map.singleton src excepts}
          )
        . G.edgeList

failed :: e -> Validation (DNonEmpty e) a
failed = Failure . pure

validatePackageGraph ::
  DomainInfo -> PackageGraph -> Validation (NE.NonEmpty PackageViolation) CheckResult
validatePackageGraph DomainInfo {..} pg =
  Bi.first DLNE.toNonEmpty $
    case buildActualGraphs packageDic pg of
      Failure e -> Failure e
      Success AGs {..} ->
        let redundantExtras = findRedundantExtraDeps packageDic pg
            diags =
              Diagnostics
                { redundantExtraDeps =
                    Set.fromList . V.toList <$> redundantExtras
                , usedExceptionalRules = exceptionGraph
                }
            resl
              | isEmptyDiagnostics diags = Ok
              | otherwise = OkWithDiagnostics diags
         in resl
              <$ ( case Bi.first DLNE.singleton (detectPackageCycle pg) of
                    f@Failure {} -> f
                    Success {} ->
                      Bi.first DLNE.singleton (coversAllPackages packageDic pg)
                        <* satisfiesDomainGraph domainGraph activatedGraph
                 )

detectPackageCycle ::
  PackageGraph -> Validation PackageViolation ()
detectPackageCycle pkgs =
  void $
    eitherToValidation $
      Bi.first CyclicPackageDep $
        GC.topSort pkgs

type ExemptDomDeps = ActualGraph

findRedundantExtraDeps ::
  PackageDic ->
  PackageGraph ->
  Map PackageName (V.Vector Dependency)
findRedundantExtraDeps pkgDic pg =
  Map.mapMaybeWithKey
    ( \pkg (_, specifiedDeps) -> do
        let actualDepPkgs = GC.postSet pkg pg
            actualDepDoms =
              Set.map
                (\dpkg -> maybe (error $ "No pkg find: " <> show (dpkg, actualDepPkgs)) fst (Map.lookup dpkg pkgDic))
                actualDepPkgs
            deps =
              V.filter
                ( \case
                    DomainDep dn -> dn `Set.notMember` actualDepDoms
                    PackageDep pn -> pn `Set.notMember` actualDepPkgs
                )
                specifiedDeps
        guard $ not $ V.null deps
        pure deps
    )
    pkgDic

coversAllPackages ::
  PackageDic -> PackageGraph -> Validation PackageViolation ()
coversAllPackages pkgDic pg =
  if null remain
    then Success ()
    else Failure $ UncoveredPackages remain
  where
    remain = Set.toList $ G.vertexSet pg Set.\\ Map.keysSet pkgDic

satisfiesDomainGraph ::
  DomainGraph -> ActualGraph -> Validation (DLNE.DNonEmpty PackageViolation) ()
satisfiesDomainGraph domGr ag =
  maybeToFailure () (DLNE.fromNonEmpty <$> NE.nonEmpty violatingEdges)
  where
    expectedEdges = Rel.edgeSet $ Preorder.toRelation domGr
    actualEdges :: Map (DomainName, DomainName) (Path PackageName)
    actualEdges =
      Map.fromList $
        map (\(x1, dn, dn') -> ((dn, dn'), x1)) $
          LG.edgeList ag
    violatingEdges =
      map
        ( uncurry $ uncurry DomainBoundaryViolation
        )
        $ Map.toList
        $ actualEdges `Map.withoutKeys` expectedEdges