hwm-0.4.0: src/HWM/Domain/Bounds.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE NoImplicitPrelude #-}
module HWM.Domain.Bounds
( Bounds (..),
BoundsByName,
versionBounds,
updateDepBounds,
Bound (..),
Restriction (..),
hasBounds,
boundsBetter,
auditBounds,
BoundAudit (..),
BoundCompliance (..),
BoundsAudit (..),
formatAudit,
auditHasAny,
TestedRange (..),
deriveBounds,
)
where
import Control.Monad.Except
import Data.Aeson
( FromJSON (..),
ToJSON (..),
Value (..),
)
import HWM.Core.Formatting (Color (..), Format (..), chalk, formatList)
import HWM.Core.Has (Has)
import HWM.Core.Parsing (Parse (..), fromToString, removeHead, sepBy, unconsM)
import HWM.Core.Pkg (PkgName)
import HWM.Core.Result (Issue)
import HWM.Core.Version (Bump (..), Version, dropPatch, nextVersion)
import HWM.Runtime.Cache (Cache, Snapshot, getVersion, getVersions)
import HWM.Runtime.UI
import Relude
data Restriction = Min | Max deriving (Show, Eq, Ord)
instance Parse Restriction where
parse ">" = pure Min -- > 0.7.0
parse "<" = pure Max -- < 1.0.0
parse x = fail ("unsorted bound type" <> toString x)
instance ToString Restriction where
toString Min = ">" -- > 0.7.0
toString Max = "<" -- < 1.0.0
instance ToText Restriction where
toText = fromToString
data Bound = Bound
{ restriction :: Restriction,
orEquals :: Bool,
version :: Version
}
deriving (Show, Eq)
instance Format Bound where
format Bound {..} = unwords $ (toText restriction <> eq) : [toText version]
where
eq = if orEquals then "=" else " "
instance Ord Bound where
compare a b =
compare (version a) (version b)
<> compare (restriction a) (restriction b)
<> compare (orEquals a) (orEquals b)
instance Parse Bound where
parse txt = do
(ch, str) <- unconsM "unsorted bound type" txt
let (orEquals, value) = removeHead '=' str
restriction <- parse ch
version <- parse value
pure Bound {..}
data Bounds = Bounds
{ lowerBound :: Maybe Bound,
upperBound :: Maybe Bound
}
deriving (Generic, Show, Eq)
type BoundsByName = '[]
instance Parse Bounds where
parse "" = pure $ Bounds Nothing Nothing
parse str = do
bounds <- sepBy "&&" str
let lower = find (\Bound {..} -> restriction == Min) bounds
let upper = find (\Bound {..} -> restriction == Max) bounds
pure $ Bounds lower upper
instance Format Bounds where
format (Bounds lower upper) = formatList " && " $ sort $ catMaybes [lower, upper]
instance ToString Bounds where
toString = toString . format
instance FromJSON Bounds where
parseJSON (String p) = parse p
parseJSON v = fail $ fromString ("cant parse Bounds expected string got" <> toString (format v))
instance ToJSON Bounds where
toJSON = String . fromToString
versionBounds :: Version -> Bounds
versionBounds version =
Bounds
{ lowerBound = Just $ Bound Min True (dropPatch version),
upperBound = Just $ Bound Max False (nextVersion Minor version)
}
hasBounds :: Bounds -> Bool
hasBounds Bounds {..} = isJust lowerBound || isJust upperBound
boundsScore :: Bounds -> Int
boundsScore Bounds {..} = length (maybeToList lowerBound) + length (maybeToList upperBound)
boundsBetter :: Bounds -> Bounds -> Bool
boundsBetter a b = boundsScore a > boundsScore b
auditBound :: Maybe Bound -> Maybe Version -> (Version -> Version -> Bool) -> BoundAudit
auditBound registryBound matrixVersion isConflict
| null registryBound = BoundAudit {auditStatus = Missing, ..}
| match isConflict registryBound matrixVersion = BoundAudit {auditStatus = Conflict, ..}
| match (flip isConflict) registryBound matrixVersion = BoundAudit {auditStatus = Unverified, ..}
| otherwise = BoundAudit {auditStatus = Valid, ..}
where
match :: (Version -> Version -> Bool) -> Maybe Bound -> Maybe Version -> Bool
match f (Just Bound {version}) (Just target) = f version target
match _ _ _ = False
updateBound :: Bool -> Restriction -> BoundCompliance -> Maybe Bound -> Maybe Version -> Maybe Bound
updateBound forceOverride res compliance registryBound matrixVersion
| compliance == Conflict = preferMatrix
| forceOverride && compliance == Unverified = preferMatrix
| otherwise = registryBound <|> matrixBound
where
preferMatrix = matrixBound <|> registryBound
matrixBound = Bound res True <$> matrixVersion
auditBounds :: TestedRange -> PkgName -> Bounds -> BoundsAudit
auditBounds TestedRange {..} name Bounds {..} =
BoundsAudit
{ auditPkgName = name,
auditMinBound = auditBound lowerBound (getVersion name legacy) (>),
auditMaxBound = auditBound upperBound (getVersion name nightly) (<)
}
updateDepBounds :: Bool -> TestedRange -> PkgName -> Bounds -> Bounds
updateDepBounds forceOverride TestedRange {..} name Bounds {..} =
Bounds
{ lowerBound = updateBound forceOverride Min (auditStatus $ auditMinBound audit) lowerBound (getVersion name legacy),
upperBound = updateBound forceOverride Max (auditStatus $ auditMaxBound audit) upperBound (getVersion name nightly)
}
where
audit = auditBounds TestedRange {..} name Bounds {..}
auditHasAny :: (BoundCompliance -> Bool) -> BoundsAudit -> Bool
auditHasAny f BoundsAudit {..} = any (f . auditStatus) [auditMinBound, auditMaxBound]
data BoundCompliance
= Conflict
| Unverified
| Missing
| Valid
deriving (Show, Eq)
data BoundAudit = BoundAudit
{ registryBound :: Maybe Bound,
matrixVersion :: Maybe Version,
auditStatus :: BoundCompliance
}
deriving (Show, Eq)
formatStatus :: BoundCompliance -> Text
formatStatus Conflict = chalk Red "-> "
formatStatus Unverified = chalk Yellow "-> "
formatStatus Missing = chalk Cyan " ! "
formatStatus Valid = chalk Green " ✓ "
formatBoundAudit :: BoundAudit -> [Text]
formatBoundAudit (BoundAudit Nothing Nothing s) = ["", formatStatus s]
formatBoundAudit (BoundAudit Nothing (Just x) s) = [chalk Dim (format x), formatStatus s]
formatBoundAudit (BoundAudit (Just x) Nothing s) = [chalk Dim (format x), formatStatus s]
formatBoundAudit (BoundAudit (Just reg) (Just mat) s) = [chalk Dim (format reg), formatStatus s <> " " <> format mat]
data BoundsAudit = BoundsAudit
{ auditPkgName :: PkgName,
auditMinBound :: BoundAudit,
auditMaxBound :: BoundAudit
}
formatAudit :: BoundsAudit -> [Text]
formatAudit a = [format $ auditPkgName a] <> formatBoundAudit (auditMinBound a) <> [chalk Dim " && "] <> formatBoundAudit (auditMaxBound a)
data TestedRange = TestedRange
{ legacy :: Snapshot,
nightly :: Snapshot
}
deriveBounds :: (MonadIO m, MonadError Issue m, MonadReader env m, Has env Cache, MonadUI m) => PkgName -> TestedRange -> m Bounds
deriveBounds name TestedRange {..} = do
let lower = getVersion name legacy
let upper = getVersion name nightly
newUpper <- maybe (head <$> getVersions name) pure upper
section_ "discovery" $ do
uiRow minRowSize "registry" "missing (initiating lookup)"
uiRow minRowSize "legacy" (maybe (chalk Red "missing") (chalk Green . format) lower <> " (min)")
uiRow minRowSize "nightly" (maybe (chalk Red "missing") ((<> " (max)") . chalk Green . format) upper)
unless (isJust lower || isJust upper) $ uiRow minRowSize "hackage" (chalk Green (format newUpper) <> " (max)")
pure
Bounds
{ lowerBound = Bound Min True <$> lower,
upperBound = Just (Bound Max True newUpper)
}