hackage-server-0.4: Distribution/Server/Packages/Render.hs
-- TODO: Review and possibly move elsewhere. This code was part of the
-- RecentPackages (formerly "Check") feature, but that caused some cyclic
-- dependencies.
module Distribution.Server.Packages.Render (
-- * Package render
PackageRender(..)
, doPackageRender
-- * Utils
, categorySplit,
) where
import Data.Maybe (catMaybes)
import Control.Monad (guard, liftM2)
import Data.Char (toLower, isSpace)
import qualified Data.Map as Map
import Data.Map (Map)
import qualified Data.Foldable as Foldable
import qualified Data.Traversable as Traversable
import Data.Ord (comparing)
import Data.List (sort, sortBy, partition)
import Data.Time.Clock (UTCTime)
-- Cabal
import Distribution.PackageDescription
import Distribution.PackageDescription.Configuration
import Distribution.Package
import Distribution.Text
import Distribution.Version
-- hackage-server
import Distribution.Server.Packages.Types
import Distribution.Server.Packages.ModuleForest
import qualified Distribution.Server.Users.Users as Users
import Distribution.Server.Users.Types
-- This should provide the caller enough information to encode the package information
-- in its particular format (text, html, json) with minimal effort on its part.
-- This is why some fields of PackageDescription are preprocessed, and others aren't.
data PackageRender = PackageRender {
rendPkgId :: PackageIdentifier,
rendDepends :: [[Dependency]],
rendExecNames :: [String],
rendLicenseName :: String,
rendMaintainer :: Maybe String,
rendCategory :: [String],
rendRepoHeads :: [(RepoType, String, SourceRepo)],
rendModules :: Maybe ModuleForest,
rendHasTarball :: Bool,
rendHasChangeLog :: Bool,
rendUploadInfo :: (UTCTime, Maybe UserInfo),
rendPkgUri :: String,
-- rendOther contains other useful fields which are merely strings, possibly empty
-- for example: description, home page, copyright, author, stability
-- If PackageRender is the One True Resource Representation, should they
-- instead be fields of PackageRender?
rendOther :: PackageDescription
} deriving (Show)
doPackageRender :: Users.Users -> PkgInfo -> Bool -> IO PackageRender
doPackageRender users info hasChangeLog = return $ PackageRender
{ rendPkgId = pkgInfoId info
, rendDepends = flatDependencies genDesc
, rendExecNames = map exeName (executables flatDesc)
, rendLicenseName = display (license desc) -- maybe make this a bit more human-readable
, rendMaintainer = case maintainer desc of
"None" -> Nothing
"none" -> Nothing
"" -> Nothing
person -> Just person
, rendCategory = case category desc of
[] -> []
str -> categorySplit str
, rendRepoHeads = catMaybes (map rendRepo $ sourceRepos desc)
, rendModules = fmap (moduleForest . exposedModules) (library flatDesc)
, rendHasTarball = not . null $ pkgTarball info
, rendHasChangeLog = hasChangeLog
, rendUploadInfo = let (utime, uid) = pkgUploadData info
in (utime, Users.lookupUserId uid users)
, rendPkgUri = pkgUri
, rendOther = desc
}
where
genDesc = pkgDesc info
flatDesc = flattenPackageDescription genDesc
desc = packageDescription genDesc
pkgUri = "/package/" ++ display (pkgInfoId info)
rendRepo r = do
guard $ repoKind r == RepoHead
ty <- repoType r
loc <- repoLocation r
return (ty, loc, r)
{-------------------------------------------------------------------------------
Util
-------------------------------------------------------------------------------}
categorySplit :: String -> [String]
categorySplit xs | all isSpace xs = []
categorySplit xs = map (dropWhile isSpace) $ splitOn ',' xs
where
splitOn x ys = front : case back of
[] -> []
(_:ys') -> splitOn x ys'
where (front, back) = break (== x) ys
-----------------------------------------------------------------------
--
-- Flatten the dependencies of a GenericPackageDescription into
-- disjunctive normal form.
--
-- This could be in its own module.
--
flatDependencies :: GenericPackageDescription -> [[Dependency]]
flatDependencies pkg =
map (map asDependency . sortOn (map toLower . display . fst)) $ sort $
map get_deps $
foldr reduceDisjunct [] $
foldr intersectDisjunct head_deps $
maybe id ((:) . fromCondTree) (condLibrary pkg) $
map (fromCondTree . snd) (condExecutables pkg)
where
-- put the constrained ones first, for sorting purposes
get_deps m = ranges ++ others
where (others, ranges) = partition (simple . snd) (Map.toList m)
asDependency (pkgname, interval) = Dependency pkgname $ intervalToRange interval
intervalToRange :: VersionInterval -> VersionRange
intervalToRange (lower, upper) = simplifyVersionRange $
intersectVersionRanges (rangeLower lower) (rangeUpper upper)
where rangeLower (LowerBound v bound) = case bound of
InclusiveBound -> orLaterVersion v
ExclusiveBound -> laterVersion v
rangeUpper (UpperBound v bound) = case bound of
InclusiveBound -> orEarlierVersion v
ExclusiveBound -> earlierVersion v
rangeUpper NoUpperBound = anyVersion
simple :: VersionInterval -> Bool
simple (l, NoUpperBound) = isMinLowerBound l
simple _ = False
head_deps = fromDependencies (buildDepends (packageDescription pkg))
fromDependencies :: [Dependency] -> Disjunct
fromDependencies = foldr addDep unitDisjunct
where addDep (Dependency p vr) = liftM2 (\ x y -> Map.alter (add x) p y)
(asVersionIntervals vr)
add x Nothing = Just x
add x (Just y) = intersectInterval x y
fromCondTree :: CondTree v [Dependency] a -> Disjunct
fromCondTree (CondNode _ ds comps) =
foldr intersectDisjunct (fromDependencies ds) $
map fromComponent comps
fromComponent (_, _, Nothing) = unitDisjunct
fromComponent (_, then_part, Just else_part) =
unionDisjunct (fromCondTree then_part)
(fromCondTree else_part)
reduceDisjunct :: Conjunct -> [Conjunct] -> [Conjunct]
reduceDisjunct c cs
| any (c `subConjunct`) cs = cs
| otherwise = c : filter (not . (`subConjunct` c)) cs
-- Same as @sortBy (comparing f)@, but without recomputing @f@.
sortOn :: Ord b => (a -> b) -> [a] -> [a]
sortOn f xs = map snd (sortBy (comparing fst) [(f x, x) | x <- xs])
isMinLowerBound :: LowerBound -> Bool
isMinLowerBound (LowerBound (Version [0] _) InclusiveBound) = True
isMinLowerBound _ = False
{-
isWildcardRange :: Version -> Version -> Bool
isWildcardRange (Version branch1 _) (Version branch2 _) = check branch1 branch2
where check (n:[]) (m:[]) | n+1 == m = True
check (n:ns) (m:ms) | n == m = check ns ms
check _ _ = False
withinInterval :: Version -> VersionInterval -> Bool
withinInterval v (lowerBound, upperBound) = withinLower lowerBound
&& withinUpper upperBound
where
withinLower (LowerBound v' ExclusiveBound) = v' < v
withinLower (LowerBound v' InclusiveBound) = v' <= v
withinUpper NoUpperBound = True
withinUpper (UpperBound v' ExclusiveBound) = v' > v
withinUpper (UpperBound v' InclusiveBound) = v' >= v
-}
intersectInterval :: VersionInterval -> VersionInterval -> Maybe VersionInterval
intersectInterval (l1, u1) (l2, u2)
| below u1 l2 || below u2 l1 = Nothing
| otherwise = Just (max l1 l2, min u1 u2)
below :: UpperBound -> LowerBound -> Bool
below NoUpperBound _ = False
below (UpperBound v1 InclusiveBound) (LowerBound v2 InclusiveBound) = v1 < v2
below (UpperBound v1 _) (LowerBound v2 _) = v1 <= v2
subInterval :: VersionInterval -> VersionInterval -> Bool
subInterval (l1, u1) (l2, u2) = l2 <= l1 && u1 <= u2
-- Constraints in disjunctive normal form
type Conjunct = Map PackageName VersionInterval
unitConjunct :: Conjunct
unitConjunct = Map.empty
intersectConjunct :: Conjunct -> Conjunct -> Maybe Conjunct
intersectConjunct m1 m2 =
Traversable.sequence $
Map.unionWith inters (fmap Just m1) (fmap Just m2)
where
inters mx my = do
x <- mx
y <- my
intersectInterval x y
subConjunct :: Conjunct -> Conjunct -> Bool
subConjunct m1 m2 =
Map.null (Map.difference m2 m1) &&
Foldable.and (Map.intersectionWith subInterval m1 m2)
type Disjunct = [Conjunct]
unitDisjunct :: Disjunct
unitDisjunct = [unitConjunct]
intersectDisjunct :: Disjunct -> Disjunct -> Disjunct
intersectDisjunct xs ys = catMaybes (liftM2 intersectConjunct xs ys)
-- eliminate any Conjunct list that is more restrictive than another
unionDisjunct :: Disjunct -> Disjunct -> Disjunct
unionDisjunct xs ys = xs' ++ ys'
where ys' = [y | y <- ys, not (or [subConjunct y x | x <- xs])]
xs' = [x | x <- xs, not (or [subConjunct x y | y <- ys'])]