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