hackage-server-0.4: Distribution/Server/Features/PreferredVersions/State.hs
{-# LANGUAGE DeriveDataTypeable, TypeFamilies, TemplateHaskell #-}
module Distribution.Server.Features.PreferredVersions.State where
import Distribution.Server.Framework.Instances ()
import Distribution.Server.Framework.MemSize
import Distribution.Package
import Distribution.Version
import Data.Acid (Query, Update, makeAcidic)
import Data.Maybe (isJust, fromMaybe)
import Data.Typeable (Typeable)
import Control.Arrow (second)
import Control.Monad (ap)
import Control.Monad.State (put, modify)
import Control.Monad.Reader (ask, asks)
import Data.Map (Map)
import qualified Data.Map as Map
import Data.SafeCopy (base, deriveSafeCopy)
import Data.Set (Set)
import qualified Data.Set as Set
data PreferredVersions = PreferredVersions {
preferredMap :: Map PackageName PreferredInfo,
deprecatedMap :: Map PackageName [PackageName]
} deriving (Typeable, Show, Eq)
emptyPreferredVersions :: PreferredVersions
emptyPreferredVersions = PreferredVersions Map.empty Map.empty
data PreferredInfo = PreferredInfo {
preferredRanges :: [VersionRange],
deprecatedVersions :: [Version],
sumRange :: Maybe VersionRange
} deriving (Typeable, Show, Eq)
emptyPreferredInfo :: PreferredInfo
emptyPreferredInfo = PreferredInfo [] [] Nothing
consolidateRanges :: [VersionRange] -> [Version] -> Maybe VersionRange
consolidateRanges ranges depr =
let range = simplifyVersionRange $ foldr intersectVersionRanges anyVersion (map notThisVersion depr ++ ranges)
in if isAnyVersion range || isNoVersion range
then Nothing
else Just range
data VersionStatus = NormalVersion | DeprecatedVersion | UnpreferredVersion deriving (Show, Typeable, Eq, Ord, Enum)
getVersionStatus :: PreferredInfo -> Version -> VersionStatus
getVersionStatus info version = case version `elem` deprecatedVersions info of
True -> DeprecatedVersion
False -> case maybe True (withinRange version) (sumRange info) of
True -> NormalVersion
False -> UnpreferredVersion
classifyVersions :: PreferredInfo -> [Version] -> [(Version, VersionStatus)]
classifyVersions (PreferredInfo [] [] _) = map (flip (,) NormalVersion)
classifyVersions info = map ((,) `ap` getVersionStatus info)
partitionVersions :: PreferredInfo -> [Version] -> ([Version], [Version], [Version])
partitionVersions info versions = if (not . isJust $ sumRange info) then (versions, [], []) else go versions
where go :: [Version] -> ([Version], [Version], [Version]) -- foldr-type approach
go (v:vs) = let ~(norm, depr, unpref) = go vs in case getVersionStatus info v of
NormalVersion -> (v:norm, depr, unpref)
DeprecatedVersion -> (norm, v:depr, unpref)
UnpreferredVersion -> (norm, depr, v:unpref)
go [] = ([], [], [])
------------------------------------------
$(deriveSafeCopy 0 'base ''PreferredVersions)
$(deriveSafeCopy 0 'base ''PreferredInfo)
$(deriveSafeCopy 0 'base ''VersionStatus)
instance MemSize PreferredVersions where
memSize (PreferredVersions a b) = memSize2 a b
instance MemSize PreferredInfo where
memSize (PreferredInfo a b c) = memSize3 a b c
initialPreferredVersions :: PreferredVersions
initialPreferredVersions = emptyPreferredVersions
setPreferredRanges :: PackageName -> [VersionRange] -> Update PreferredVersions ()
setPreferredRanges name ranges = alterPreferredInfo name $ \p ->
p { preferredRanges = ranges }
setDeprecatedVersions :: PackageName -> [Version] -> Update PreferredVersions ()
setDeprecatedVersions name versions = alterPreferredInfo name $ \p ->
p { deprecatedVersions = versions }
alterPreferredInfo :: PackageName -> (PreferredInfo -> PreferredInfo) -> Update PreferredVersions ()
alterPreferredInfo name func = modify $ \p -> p { preferredMap = Map.alter (res . func . fromMaybe emptyPreferredInfo) name $ preferredMap p }
where res (PreferredInfo [] [] _) = Nothing
res (PreferredInfo ranges depr _) = Just (PreferredInfo ranges depr $ consolidateRanges ranges depr)
getPreferredInfo :: PackageName -> Query PreferredVersions PreferredInfo
getPreferredInfo name = asks $ Map.findWithDefault emptyPreferredInfo name . preferredMap
setDeprecatedFor :: PackageName -> Maybe [PackageName] -> Update PreferredVersions ()
setDeprecatedFor name forName = modify $ \p -> p { deprecatedMap = Map.alter (const forName) name $ deprecatedMap p }
getDeprecatedFor :: PackageName -> Query PreferredVersions (Maybe [PackageName])
getDeprecatedFor name = asks $ Map.lookup name . deprecatedMap
isDeprecated :: PackageName -> Query PreferredVersions Bool
isDeprecated name = asks $ Map.member name . deprecatedMap
getPreferredVersions :: Query PreferredVersions PreferredVersions
getPreferredVersions = ask
replacePreferredVersions :: PreferredVersions -> Update PreferredVersions ()
replacePreferredVersions = put
makeAcidic ''PreferredVersions ['setPreferredRanges
,'setDeprecatedVersions
,'getPreferredInfo
,'setDeprecatedFor
,'getDeprecatedFor
,'isDeprecated
,'getPreferredVersions
,'replacePreferredVersions
]
---------------
maybeBestVersion :: PreferredInfo -> [Version] -> Set Version -> Maybe (Version, Maybe VersionStatus)
maybeBestVersion info allVersions versions = if null allVersions || Set.null versions then Nothing else Just $ findBestVersion info allVersions versions
{-
findBestVersion attempts to find the best version to display out of a set
of versions. The quality of a given version is encoded in a pair (VersionStatus,
Bool). If the version is a NormalVersion, then the boolean indicates whether if
it the most recently uploaded preferred version (and all higher versions are
either deprecated or unpreferred). Otherwise, if it is a DeprecatedVersion or
UnpreferredVersion, the boolean indicates that it is the maximum of all uploaded
versions.
The list of available versions is scanned from the back (most recent) to the
front (first one uploaded). If a 'better' version is found than the current
best version, it is replaced. If no better version can be found, the algorithm
finishes up. The exact ordering is defined as:
1. (NormalVersion, True) means the latest preferred version of the package is
available. This option may appear anywhere, although it is always seen before
(NormalVersion, False). In this case, the algorithm finishes up.
2. (UnpreferredVersion, True) means the latest available version of the package
is not preferred, but the latest preferred version is not available. If this
option appears anywhere, it will be the most recent version in the set,
excluding deprecated versions.
3. (NormalVersion, False) means neither the actual latest version nor the
preferred latest version are available, but there is some preferred version
that's available. It can only be scanned after (NormalVersion, True) and
(UnpreferredVersion, True), so the algorithm finishes up in this case.
4. (UnpreferredVersion, False) means no preferred versions are available, and
only an older version is available. It is still possible to see a NormalVersion
after this option, so the algorithm continues.
5. (DeprecatedVersion, True) and (DeprecatedVersion, False) mean only a
deprecated version is available. This is not so great.
This is a bit complex but I think it has the most intuitive result, and is
rather efficient in 99% of cases.
The version set and version list should both be non-empty; otherwise this
function is partial. Use maybeBestVersion for a safe check.
-}
findBestVersion :: PreferredInfo -> [Version] -> Set Version -> (Version, Maybe VersionStatus)
findBestVersion info allVersions versions =
let topStatus = getVersionStatus info maxVersion
in if maxAllVersion == maxVersion && topStatus == NormalVersion
then (maxVersion, Just NormalVersion) -- most common case
else second classifyOpt $ newSearch (reverse $ Set.toList versions) (maxVersion, (topStatus, True))
where
maxVersion = Set.findMax versions
maxAllVersion = last allVersions
newestPreferred = case filter ((==NormalVersion) . (infoMap Map.!)) $ allVersions of
[] -> Nothing
prefs -> Just $ last prefs
infoMap = Map.fromDistinctAscList $ classifyVersions info allVersions
newSearch (v:vs) _ = case infoMap Map.! v of
NormalVersion | v == maxAllVersion -> (v, (NormalVersion, True))
NormalVersion -> oldSearch vs (v, (NormalVersion, False))
DeprecatedVersion -> newSearch vs (v, (DeprecatedVersion, True))
UnpreferredVersion -> oldSearch vs (v, (UnpreferredVersion, True))
newSearch [] opt = opt
oldSearch (v:vs) opt = case infoMap Map.! v of
NormalVersion -> replaceBetter opt (v, (NormalVersion, newestPreferred == Just v))
other -> oldSearch vs $ replaceBetter opt (v, (other, False))
oldSearch [] opt = opt
replaceBetter keep@(_, old) replace@(_, new) = if optionPrefs new > optionPrefs old then replace else keep
optionPrefs :: (VersionStatus, Bool) -> Int
optionPrefs opt = case opt of
(NormalVersion, True) -> 4
(UnpreferredVersion, True) -> 3
(NormalVersion, False) -> 2
(UnpreferredVersion, False) -> 1
_ -> 0
classifyOpt opt = case opt of
(NormalVersion, True) -> Just NormalVersion
(UnpreferredVersion, True) -> Just UnpreferredVersion
(DeprecatedVersion, _) -> Just DeprecatedVersion
_ -> Nothing