dojang-0.1.0: src/Dojang/Types/EnvironmentPredicate.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedRecordUpdate #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoFieldSelectors #-}
-- | A predicate expression that can be used to match an environment, e.g.,
-- @And [OperatingSystem "linux", Architecture "x86_64"]@.
module Dojang.Types.EnvironmentPredicate
( EnvironmentPredicate (..)
, normalizePredicate
) where
import Data.List.NonEmpty (NonEmpty, filter, nonEmpty, nub, sortWith, toList)
import Prelude hiding (filter)
import Data.CaseInsensitive (CI)
import Data.Hashable (Hashable (hashWithSalt))
import Data.Text (Text)
import Data.Ord (Down (..))
import Dojang.Types.Environment (Architecture, OperatingSystem)
import Dojang.Types.MonikerName (MonikerName)
-- | A predicate that can be used to match an environment.
data EnvironmentPredicate
= -- | A predicate that always matches.
Always
| -- | A predicate that matches if the given predicate does not match.
Not EnvironmentPredicate
| -- | A predicate that matches if all of the given predicates match.
And (NonEmpty EnvironmentPredicate)
| -- | A predicate that matches if any of the given predicates match.
Or (NonEmpty EnvironmentPredicate)
| -- | A predicate that matches to another moniker. Mostly used to eliminate
-- duplication.
Moniker MonikerName
| -- | A predicate that matches to the given operating system (e.g. @linux@).
-- For a list of possible values, see the 'System.Info.os' field.
OperatingSystem OperatingSystem
| -- | A predicate that matches to the given architecture (e.g. @x86_64@).
-- For a list of possible values, see the 'System.Info.arch' field.
Architecture Architecture
| -- | A predicate that matches to the given kernel name (e.g. @Darwin@).
KernelName (CI Text)
| -- | A predicate that exactly matches to the given kernel release
-- (e.g. @23.1.0@).
KernelRelease (CI Text)
| -- | A predicate that matches to the given kernel release prefix
-- (e.g. @23.1@ for @23.1.0@).
KernelReleasePrefix (CI Text)
| -- | A predicate that matches to the given kernel release suffix
-- (e.g. @amd64@ for @4.19.0-16-amd64@).
KernelReleaseSuffix (CI Text)
deriving (Show)
instance Eq EnvironmentPredicate where
Always == Always = True
Always == _ = False
Not p == Not p' = p == p'
Not _ == _ = False
And ps == And ps' = normalizePredicateList ps == normalizePredicateList ps'
And _ == _ = False
Or ps == Or ps' = normalizePredicateList ps == normalizePredicateList ps'
Or _ == _ = False
Moniker monikerName == Moniker monikerName' = monikerName == monikerName'
Moniker _ == _ = False
OperatingSystem os == OperatingSystem os' = os == os'
OperatingSystem _ == _ = False
Architecture arch == Architecture arch' = arch == arch'
Architecture _ == _ = False
KernelName kernel == KernelName kernel' = kernel == kernel'
KernelName _ == _ = False
KernelRelease ver == KernelRelease ver' = ver == ver'
KernelRelease _ == _ = False
KernelReleasePrefix prefix == KernelReleasePrefix prefix' = prefix == prefix'
KernelReleasePrefix _ == _ = False
KernelReleaseSuffix suffix == KernelReleaseSuffix suffix' = suffix == suffix'
KernelReleaseSuffix _ == _ = False
instance Hashable EnvironmentPredicate where
hashWithSalt salt Always =
salt `hashWithSalt` ("Always" :: Text)
hashWithSalt salt (Not predicate) =
salt `hashWithSalt` ("Not" :: Text) `hashWithSalt` predicate
hashWithSalt salt (And predicates) =
salt
`hashWithSalt` ("And" :: Text)
`hashWithSalt` normalizePredicateList predicates
hashWithSalt salt (Or predicates) =
salt
`hashWithSalt` ("Or" :: Text)
`hashWithSalt` normalizePredicateList predicates
hashWithSalt salt (Moniker monikerName) =
salt `hashWithSalt` ("Moniker" :: Text) `hashWithSalt` monikerName
hashWithSalt salt (OperatingSystem os') =
salt `hashWithSalt` ("OperatingSystem" :: Text) `hashWithSalt` os'
hashWithSalt salt (Architecture arch') =
salt `hashWithSalt` ("Architecture" :: Text) `hashWithSalt` arch'
hashWithSalt salt (KernelName kernel) =
salt `hashWithSalt` ("KernelName" :: Text) `hashWithSalt` kernel
hashWithSalt salt (KernelRelease ver) =
salt `hashWithSalt` ("KernelRelease" :: Text) `hashWithSalt` ver
hashWithSalt salt (KernelReleasePrefix prefix) =
salt `hashWithSalt` ("KernelReleasePrefix" :: Text) `hashWithSalt` prefix
hashWithSalt salt (KernelReleaseSuffix suffix) =
salt `hashWithSalt` ("KernelReleaseSuffix" :: Text) `hashWithSalt` suffix
normalizePredicateList
:: NonEmpty EnvironmentPredicate -> NonEmpty EnvironmentPredicate
normalizePredicateList = nub . sortWith (Down . hashWithSalt 1)
-- | Normalize an environment predicate by removing redundant predicates.
--
-- >>> normalizePredicate $ And [Not Always, Always]
-- Not Always
-- >>> normalizePredicate $ Or [Not Always, Always]
-- Always
-- >>> import Dojang.Types.MonikerName (parseMonikerName)
-- >>> let Right foo = parseMonikerName "foo"
-- >>> normalizePredicate $ And [Moniker foo, Not $ Moniker foo]
-- Not Always
-- >>> normalizePredicate $ Or [Moniker foo, Not $ Moniker foo]
-- Always
-- >>> let Right a = parseMonikerName "a"
-- >>> let Right b = parseMonikerName "b"
-- >>> let Right c = parseMonikerName "c"
-- >>> normalizePredicate $ And [Moniker a, And [Moniker b, Moniker c]]
-- And (Moniker (MonikerName "a") :| [Moniker (MonikerName "b"),Moniker (MonikerName "c")])
normalizePredicate :: EnvironmentPredicate -> EnvironmentPredicate
normalizePredicate pred'
| normalized == normalized2 = normalized
| otherwise = normalizePredicate normalized2
where
normalized :: EnvironmentPredicate
normalized = normalizePredicate' pred'
normalized2 :: EnvironmentPredicate
normalized2 = normalizePredicate' normalized
normalizePredicate' :: EnvironmentPredicate -> EnvironmentPredicate
normalizePredicate' (And [p]) = normalizePredicate' p
normalizePredicate' (And ps) =
if Not Always `elem` ps'
then Not Always
else
let filtered = filter (/= Always) ps'
in if (`any` filtered) $ \case Not p' -> p' `elem` filtered; _ -> False
then Not Always
else
let reduced =
maybe Always And
$ nonEmpty
[ p'
| p <- filtered
, p' <- case p of
And ps'' -> normalizePredicate' <$> toList ps''
p'' -> [p'']
]
in case reduced of
And [p'] -> p'
_ -> reduced
where
ps' :: NonEmpty EnvironmentPredicate
ps' = normalizePredicate' <$> ps
normalizePredicate' (Or [p]) = normalizePredicate' p
normalizePredicate' (Or ps) =
if Always `elem` ps'
then Always
else
let filtered = filter (/= Not Always) ps'
in if (`any` filtered) $ \case Not p' -> p' `elem` filtered; _ -> False
then Always
else
let reduced =
maybe (Not Always) Or
$ nonEmpty
[ p'
| p <- filtered
, p' <- case p of
Or ps'' -> normalizePredicate' <$> toList ps''
p'' -> [p'']
]
in case reduced of
Or [p'] -> p'
_ -> reduced
where
ps' :: NonEmpty EnvironmentPredicate
ps' = normalizePredicate' <$> ps
normalizePredicate' (Not (Not p)) = normalizePredicate' p
normalizePredicate' (Not p) = Not $ normalizePredicate' p
normalizePredicate' p = p