packages feed

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

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}

module Development.Guardian.Types where

import Algebra.Graph (Graph)
import qualified Algebra.Graph as G
import Algebra.Graph.AdjacencyMap.Algorithm (Cycle)
import qualified Algebra.Graph.Class as GC
import Algebra.Graph.Label
import qualified Algebra.Graph.Labelled as L
import Algebra.Graph.Relation.Preorder (PreorderRelation)
import Control.Applicative
import Control.Exception (Exception)
import Control.Monad
import Control.Monad.Trans.Reader (ReaderT (ReaderT, runReaderT))
import Control.Monad.Trans.Writer.CPS (runWriter, tell)
import Data.Aeson (FromJSON (..), FromJSONKey, genericParseJSON, withObject, withText, (.:), (.:?))
import qualified Data.Aeson as J
import qualified Data.Bifunctor as Bi
import Data.Coerce (coerce)
import qualified Data.DList.DNonEmpty as DLNE
import Data.Foldable (fold)
import qualified Data.Foldable as F
import Data.Functor.Compose (Compose (..))
import Data.Generics.Labels ()
import qualified Data.HashMap.Strict as HM
import Data.Hashable (Hashable)
import Data.List.NonEmpty (NonEmpty)
import Data.Map (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (isJust, mapMaybe)
import Data.Monoid (Last (..))
import Data.Set (Set)
import qualified Data.Set as Set
import Data.String (IsString)
import Data.Text (Text)
import qualified Data.Text.Encoding as T
import qualified Data.Trie as Trie
import qualified Data.Vector as V
import GHC.Generics (Generic)
import RIO ((^.))
import Text.Pattern (Pattern, concretePrefix)
import qualified Text.Pattern as P

type DomainGraph = PreorderRelation DomainName

type PackageGraph = Graph PackageName

type ActualGraph' e = L.Graph e DomainName

type ActualGraph = ActualGraph' (Path PackageName)

type PackageDic = Map PackageName (DomainName, V.Vector Dependency)

data DomainGraphError
  = CyclicDomainDep (Cycle DomainName)
  | OverlappingPackages (Map PackageName (Set DomainName))
  deriving (Show, Eq, Ord, Generic)
  deriving anyclass (Exception)

data CheckResult
  = Ok
  | OkWithDiagnostics Diagnostics
  deriving (Show, Eq, Ord, Generic)

isEmptyDiagnostics :: Diagnostics -> Bool
isEmptyDiagnostics Diagnostics {..} =
  Map.null redundantExtraDeps
    -- && Set.null redundantDomainDeps
    && Map.null usedExceptionalRules

data Diagnostics = Diagnostics
  { redundantExtraDeps :: Map PackageName (Set Dependency)
  , -- , redundantDomainDeps :: Set (DomainName, DomainName)
    usedExceptionalRules :: Map PackageName (Set Dependency)
  }
  deriving (Show, Eq, Ord, Generic)

data DomainInfo = DomainInfo
  { domainConfig :: Domains
  , domainGraph :: DomainGraph
  , packageDic :: PackageDic
  }
  deriving (Show, Eq, Ord, Generic)

data PackageViolation
  = DomainBoundaryViolation
      { fromDom :: DomainName
      , toDom :: DomainName
      , introducedBy :: [(PackageName, PackageName)]
      }
  | CyclicPackageDep (Cycle PackageName)
  | OrphanPackage PackageName [PackageName]
  | UncoveredPackages [PackageName]
  deriving (Show, Eq, Ord, Generic)
  deriving anyclass (Exception)

newtype PackageName = PackageName {getPackageName :: Text}
  deriving (Eq, Ord, Generic)
  deriving newtype (Show, IsString, FromJSON, Hashable)

newtype DomainName = DomainName {getDomainName :: Text}
  deriving (Eq, Ord, Generic)
  deriving newtype (Show, IsString, FromJSON, FromJSONKey, Hashable)

data Domain' a = Domain
  { dependsOn :: Maybe (V.Vector DomainName)
  , packages :: V.Vector (PackageDef' a)
  }
  deriving (Show, Eq, Ord, Generic, Functor, Foldable, Traversable)

type Domain = Domain' PackageName

instance FromJSON a => FromJSON (Domain' a) where
  parseJSON =
    genericParseJSON
      J.defaultOptions
        { J.omitNothingFields = True
        , J.fieldLabelModifier = J.camelTo2 '_'
        }

type PackageDef = PackageDef' PackageName

newtype Domains = Domains
  { getDomains :: HM.HashMap DomainName Domain
  }
  deriving (Show, Eq, Ord, Generic)

data DomainsConfig
  = -- | Domain definition without wildcards
    ConcreteDomains Domains
  | -- | Domain definition containing wildcards
    WildcardDomains (HM.HashMap DomainName (Domain' Pattern))
  deriving (Show, Eq, Ord, Generic)

instance FromJSON DomainsConfig where
  parseJSON = withObject "{domains: ... [, wildcards: true]}" $ \obj -> do
    wildcards <- obj .:? "wildcards" J..!= False
    if wildcards
      then WildcardDomains <$> obj .: "domains"
      else ConcreteDomains . Domains <$> obj .: "domains"

data DomainResult = DomainResult {domains :: Domains, warnings :: Maybe (NonEmpty String)}
  deriving (Show, Eq, Ord, Generic)

{- |
Resolves patterns in packages in the domains.
If the multiple patterns matched, it picks one with the longest prefix.
-}
resolveDomainName :: DomainsConfig -> PackageGraph -> DomainResult
resolveDomainName (ConcreteDomains doms) _ = DomainResult doms Nothing
resolveDomainName (WildcardDomains patDoms) pkgGraph =
  let pkgs = G.vertexList pkgGraph
      pats =
        Trie.fromList $
          Map.toList $
            Map.fromListWith (<>) $
              map ((,) <$> T.encodeUtf8 . concretePrefix <*> Set.singleton) $
                F.toList $
                  Compose patDoms
      pkgAssigns =
        fmap (V.fromList . Set.toList) $
          HM.fromList $
            Map.toList $
              Map.fromListWith (<>) $
                mapMaybe
                  ( \pn@(PackageName nm) ->
                      fmap (,Set.singleton pn)
                        $ getLast
                        $ foldMap
                          (\(_, ps, _) -> Last $ F.find (isJust . (`P.match` nm)) ps)
                        $ Trie.matches pats (T.encodeUtf8 nm)
                  )
                  pkgs
      (doms, warns) = Bi.second (fmap DLNE.toNonEmpty) $
        runWriter $
          forM patDoms $ \dom -> do
            pkgs' <- fmap fold $ V.forM (packages dom) $ \pkgDef -> do
              let aPat = pkgDef ^. #packageName
                  cands = HM.lookupDefault mempty aPat pkgAssigns
              when (V.null cands) $
                tell $
                  Just $
                    DLNE.singleton $
                      "No match found for pattern: " <> show aPat
              pure $ V.map (\pn -> pkgDef {packageName = pn}) cands
            pure dom {packages = pkgs'}
   in DomainResult {domains = Domains doms, warnings = warns}

data PackageDef' a = PackageDef
  { packageName :: !a
  , extraDeps :: V.Vector Dependency
  }
  deriving (Show, Eq, Ord, Generic, Functor, Foldable, Traversable)

instance FromJSON a => FromJSON (PackageDef' a) where
  parseJSON =
    runReaderT $
      ReaderT (fmap (`PackageDef` V.empty) . J.parseJSON)
        <|> ReaderT do
          withObject "object" $ \dic -> do
            packageName <- dic .: "package"
            excepts <- dic .:? "exception"
            extraDeps <- maybe (pure V.empty) (.: "depends_on") excepts
            pure PackageDef {..}

data Dependency
  = DomainDep !DomainName
  | PackageDep !PackageName
  deriving (Show, Eq, Ord, Generic)

instance FromJSON Dependency where
  parseJSON =
    runReaderT $
      ReaderT (withText "domain name" (pure . DomainDep . DomainName))
        <|> ReaderT
          ( withObject
              "{package: ...} or {domain: ...}"
              ( \obj ->
                  DomainDep <$> obj .: "domain" <|> PackageDep <$> obj .: "package"
              )
          )

newtype Overlayed gr = Overlayed {getOverlayed :: gr}
  deriving (Show, Eq, Ord)

instance GC.Graph gr => Semigroup (Overlayed gr) where
  (<>) = coerce $ GC.overlay @gr

instance GC.Graph gr => Monoid (Overlayed gr) where
  mempty = Overlayed GC.empty