foliage/app/Foliage/Meta.hs
Andrea Bedini 9a4d097cde Rework internals and add consistency check
The function preparePackageVersion is now responsible for doing
everything we need to do to be able to include the package in the index.

The function also returns a denormalised view of the package information
which can be taken as a proof that everything is consistent.
2023-03-03 08:15:19 +08:00

238 lines
7.7 KiB
Haskell

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Foliage.Meta
( PackageMeta (PackageMeta),
PackageMetaEntry (PackageMetaEntry),
packageMetaEntryDeprecated,
packageMetaEntryPreferred,
packageMetaEntryTimestamp,
readPackageMeta,
writePackageMeta,
packageVersionTimestamp,
packageVersionSource,
packageVersionRevisions,
packageVersionForce,
PackageVersionSpec (PackageVersionSpec),
readPackageVersionSpec,
writePackageVersionSpec,
RevisionSpec (RevisionSpec),
revisionTimestamp,
revisionNumber,
PackageVersionSource,
pattern TarballSource,
pattern GitHubSource,
GitHubRepo (..),
GitHubRev (..),
UTCTime,
latestRevisionNumber,
consolidateRanges,
)
where
import Control.Applicative ((<|>))
import Control.Monad (void)
import Data.List (sortOn)
import Data.Maybe (fromMaybe)
import Data.Ord (Down (Down))
import Data.Text (Text)
import Data.Text qualified as T
import Data.Time.LocalTime (utc, utcToZonedTime, zonedTimeToUTC)
import Development.Shake.Classes (Binary, Hashable, NFData)
import Distribution.Aeson ()
import Distribution.Parsec (simpleParsec)
import Distribution.Pretty (prettyShow)
import Distribution.Types.Orphans ()
import Distribution.Types.Version (Version)
import Distribution.Types.VersionRange (VersionRange, anyVersion, intersectVersionRanges, notThisVersion)
import Distribution.Version (isAnyVersion, isNoVersion, simplifyVersionRange)
import Foliage.Time (UTCTime)
import GHC.Generics (Generic)
import Network.URI (URI, parseURI)
import Network.URI.Orphans ()
import Toml (TomlCodec, (.=))
import Toml qualified
newtype PackageMeta = PackageMeta
{ packageMetaEntries :: [PackageMetaEntry]
}
deriving (Show, Eq, Generic)
deriving anyclass (Binary, Hashable, NFData)
data PackageMetaEntry = PackageMetaEntry
{ packageMetaEntryTimestamp :: UTCTime,
packageMetaEntryPreferred :: [VersionRange],
packageMetaEntryDeprecated :: [Version]
}
deriving (Show, Eq, Generic)
deriving anyclass (Binary, Hashable, NFData)
readPackageMeta :: FilePath -> IO PackageMeta
readPackageMeta = Toml.decodeFile packageMetaCodec
writePackageMeta :: FilePath -> PackageMeta -> IO ()
writePackageMeta fp a = void $ Toml.encodeToFile packageMetaCodec fp a
packageMetaCodec :: TomlCodec PackageMeta
packageMetaCodec =
PackageMeta
<$> Toml.list packageMetaEntryCodec "entries"
.= packageMetaEntries
packageMetaEntryCodec :: TomlCodec PackageMetaEntry
packageMetaEntryCodec =
PackageMetaEntry
<$> timeCodec "timestamp"
.= packageMetaEntryTimestamp
<*> Toml.arrayOf _VersionRange "preferred-versions"
.= packageMetaEntryPreferred
<*> Toml.arrayOf _Version "deprecated-versions"
.= packageMetaEntryDeprecated
_Version :: Toml.TomlBiMap Version Toml.AnyValue
_Version = Toml._TextBy showVersion parseVersion
where
showVersion = T.pack . prettyShow
parseVersion t = case simpleParsec (T.unpack t) of
Nothing -> Left $ T.pack $ "unable to parse version" ++ T.unpack t
Just v -> Right v
_VersionRange :: Toml.TomlBiMap VersionRange Toml.AnyValue
_VersionRange = Toml._TextBy showVersion parseVersion
where
showVersion = T.pack . prettyShow
parseVersion t = case simpleParsec (T.unpack t) of
Nothing -> Left $ T.pack $ "unable to parse version" ++ T.unpack t
Just v -> Right v
newtype GitHubRepo = GitHubRepo {unGitHubRepo :: Text}
deriving (Show, Eq, Binary, Hashable, NFData) via Text
newtype GitHubRev = GitHubRev {unGitHubRev :: Text}
deriving (Show, Eq, Binary, Hashable, NFData) via Text
data PackageVersionSource
= TarballSource
{ tarballSourceURI :: URI,
subdir :: Maybe String
}
| GitHubSource
{ githubRepo :: GitHubRepo,
githubRev :: GitHubRev,
subdir :: Maybe String
}
deriving (Show, Eq, Generic)
deriving anyclass (Binary, Hashable, NFData)
packageSourceCodec :: TomlCodec PackageVersionSource
packageSourceCodec =
Toml.dimatch matchTarballSource (uncurry TarballSource) tarballSourceCodec
<|> Toml.dimatch matchGitHubSource (\((repo, rev), mSubdir) -> GitHubSource repo rev mSubdir) githubSourceCodec
uri :: Toml.Key -> TomlCodec URI
uri = Toml.textBy to from
where
to = T.pack . show
from t = case parseURI (T.unpack t) of
Nothing -> Left $ "Invalid url: " <> t
Just uri' -> Right uri'
tarballSourceCodec :: TomlCodec (URI, Maybe String)
tarballSourceCodec =
Toml.pair
(uri "url")
(Toml.dioptional $ Toml.string "subdir")
matchTarballSource :: PackageVersionSource -> Maybe (URI, Maybe String)
matchTarballSource (TarballSource url mSubdir) = Just (url, mSubdir)
matchTarballSource _ = Nothing
gitHubRepo :: Toml.Key -> TomlCodec GitHubRepo
gitHubRepo = Toml.dimap unGitHubRepo GitHubRepo . Toml.text
gitHubRev :: Toml.Key -> TomlCodec GitHubRev
gitHubRev = Toml.dimap unGitHubRev GitHubRev . Toml.text
githubSourceCodec :: TomlCodec ((GitHubRepo, GitHubRev), Maybe String)
githubSourceCodec =
Toml.pair
(Toml.table (Toml.pair (gitHubRepo "repo") (gitHubRev "rev")) "github")
(Toml.dioptional $ Toml.string "subdir")
matchGitHubSource :: PackageVersionSource -> Maybe ((GitHubRepo, GitHubRev), Maybe String)
matchGitHubSource (GitHubSource repo rev mSubdir) = Just ((repo, rev), mSubdir)
matchGitHubSource _ = Nothing
data PackageVersionSpec = PackageVersionSpec
{ -- | timestamp
packageVersionTimestamp :: Maybe UTCTime,
-- | source parameters
packageVersionSource :: PackageVersionSource,
-- | revisions
packageVersionRevisions :: [RevisionSpec],
-- | force version
packageVersionForce :: Bool
}
deriving (Show, Eq, Generic)
deriving anyclass (Binary, Hashable, NFData)
sourceMetaCodec :: TomlCodec PackageVersionSpec
sourceMetaCodec =
PackageVersionSpec
<$> Toml.dioptional (timeCodec "timestamp")
.= packageVersionTimestamp
<*> packageSourceCodec
.= packageVersionSource
<*> Toml.list revisionMetaCodec "revisions"
.= packageVersionRevisions
<*> withDefault False (Toml.bool "force-version")
.= packageVersionForce
readPackageVersionSpec :: FilePath -> IO PackageVersionSpec
readPackageVersionSpec = Toml.decodeFile sourceMetaCodec
writePackageVersionSpec :: FilePath -> PackageVersionSpec -> IO ()
writePackageVersionSpec fp a = void $ Toml.encodeToFile sourceMetaCodec fp a
data RevisionSpec = RevisionSpec
{ revisionTimestamp :: UTCTime,
revisionNumber :: Int
}
deriving (Show, Eq, Generic)
deriving anyclass (Binary, Hashable, NFData)
revisionMetaCodec :: TomlCodec RevisionSpec
revisionMetaCodec =
RevisionSpec
<$> timeCodec "timestamp"
.= revisionTimestamp
<*> Toml.int "number"
.= revisionNumber
timeCodec :: Toml.Key -> TomlCodec UTCTime
timeCodec key = Toml.dimap (utcToZonedTime utc) zonedTimeToUTC $ Toml.zonedTime key
latestRevisionNumber :: PackageVersionSpec -> Maybe Int
latestRevisionNumber sm =
case sortOn (Down . revisionNumber) (packageVersionRevisions sm) of
[] -> Nothing
rev : _ -> Just (revisionNumber rev)
withDefault :: Eq a => a -> TomlCodec a -> TomlCodec a
withDefault d c = (fromMaybe d <$> Toml.dioptional c) .= f
where
f a = if a == d then Nothing else Just a
-- | copied from hackage-server
consolidateRanges :: PackageMetaEntry -> Maybe VersionRange
consolidateRanges PackageMetaEntry {packageMetaEntryPreferred, packageMetaEntryDeprecated} =
if isAnyVersion range || isNoVersion range then Nothing else Just range
where
range =
simplifyVersionRange $
foldr intersectVersionRanges anyVersion (map notThisVersion packageMetaEntryDeprecated ++ packageMetaEntryPreferred)