packages feed

dojang-0.2.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