forked from haskell/hackage-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPackageInfoJSON.hs
More file actions
282 lines (242 loc) · 11 KB
/
PackageInfoJSON.hs
File metadata and controls
282 lines (242 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NamedFieldPuns #-}
module Distribution.Server.Features.PackageInfoJSON (
PackageInfoJSONFeature(..)
, PackageInfoJSONResource(..)
, initPackageInfoJSONFeature
, PackageBasicDescription(..)
, PackageVersions(..)
) where
import Prelude ()
import Distribution.Server.Prelude
import qualified Data.Aeson as Aeson
import Data.Aeson ((.=))
import qualified Data.Aeson.Key as Key
import qualified Data.ByteString.Lazy.Char8 as BS (toStrict)
import qualified Data.Map.Strict as Map
import qualified Data.Text as T
import qualified Data.Vector as Vector
import Distribution.License (licenseToSPDX)
import Distribution.Package (PackageIdentifier(..),
packageVersion)
import qualified Distribution.Parsec as Parsec
import qualified Distribution.PackageDescription.Parsec as PkgDescr
import Distribution.Text (display)
import qualified Distribution.Types.GenericPackageDescription as PkgDescr
import qualified Distribution.Types.PackageDescription as PkgDescr
import qualified Distribution.Pretty as Pretty
import Distribution.SPDX.License (License)
import Distribution.Version (nullVersion, Version)
import qualified Distribution.Server.Framework as Framework
import Distribution.Server.Features.Core (CoreFeature(..),
CoreResource(..))
import qualified Distribution.Server.Features.PreferredVersions as Preferred
import Distribution.Server.Packages.Types (CabalFileText(..), pkgMetadataRevisions)
import Distribution.Utils.ShortText (fromShortText)
import Data.Foldable (toList)
import Data.Traversable (for)
import qualified Data.List as List
import Data.Time (UTCTime)
import Distribution.Server.Users.Types (UserName (..), UserInfo(..))
import Distribution.Server.Features.Users (UserFeature(lookupUserInfo))
data PackageBasicDescription = PackageBasicDescription
{ pbd_license :: !License
, pbd_copyright :: !T.Text
, pbd_synopsis :: !T.Text
, pbd_description :: !T.Text
, pbd_author :: !T.Text
, pbd_homepage :: !T.Text
, pbd_metadata_revision :: !Int
, pbd_uploaded_at :: !UTCTime
} deriving (Eq, Show)
-- | Data type used in the `/package/:packagename` JSON endpoint
data PackageBasicDescriptionDTO = PackageBasicDescriptionDTO
{ license :: !License
, copyright :: !T.Text
, synopsis :: !T.Text
, description :: !T.Text
, author :: !T.Text
, homepage :: !T.Text
, metadata_revision :: !Int
, uploaded_at :: !UTCTime
, uploader :: !UserName
} deriving (Eq, Show)
instance Aeson.ToJSON PackageBasicDescriptionDTO where
toJSON PackageBasicDescriptionDTO {..} =
Aeson.object
[ Key.fromString "license" .= Pretty.prettyShow license
, Key.fromString "copyright" .= copyright
, Key.fromString "synopsis" .= synopsis
, Key.fromString "description" .= description
, Key.fromString "author" .= author
, Key.fromString "homepage" .= homepage
, Key.fromString "metadata_revision" .= metadata_revision
, Key.fromString "uploaded_at" .= uploaded_at
, Key.fromString "uploader" .= uploader
]
-- | An index of versions for one Hackage package
-- and their preferred/deprecated status
newtype PackageVersions = PackageVersions {
unPackageVersions :: [(Version, Preferred.VersionStatus)]
} deriving (Eq, Show)
-- | This encoding of @PackageVersions@ is used in the
-- `/package/$package` endpoint (when the URI doesn't specify)
-- a version. Any change here is an API change.
instance Aeson.ToJSON PackageVersions where
toJSON (PackageVersions p) =
Aeson.toJSON
$ Map.mapKeys display
$ fmap encodeStatus
$ Map.fromList p
where
encodeStatus = \case
Preferred.NormalVersion -> "normal"
Preferred.DeprecatedVersion -> "deprecated"
Preferred.UnpreferredVersion -> "unpreferred"
data PackageInfoJSONFeature = PackageInfoJSONFeature {
packageInfoJSONFeatureInterface :: Framework.HackageFeature
}
instance Framework.IsHackageFeature PackageInfoJSONFeature where
getFeatureInterface = packageInfoJSONFeatureInterface
data PackageInfoJSONResource = PackageInfoJSONResource {
packageJSONResource :: Framework.Resource,
packageVersionJSONResource :: Framework.Resource
}
-- | Initializing our feature involves adding JSON variants to the
-- endpoints that serve basic information about a package-version,
-- and a packages version deprecation status.
initPackageInfoJSONFeature
:: Framework.ServerEnv
-> IO (CoreFeature -> Preferred.VersionsFeature -> UserFeature -> IO PackageInfoJSONFeature)
initPackageInfoJSONFeature _env = do
return $ \core preferred userFeature -> do
let coreR = coreResource core
info = "Get basic package information: \
\The response contains a JSON object where the keys are version numbers as strings, \
\and the values are whether the version is preferred or not"
vInfo = "Get basic package information at a specific metadata revision"
jsonResources = [
(Framework.extendResource (corePackagePage coreR)) {
Framework.resourceDesc = [(Framework.GET, info)]
, Framework.resourceGet =
[("json", servePackageBasicDescription coreR userFeature
preferred)]
}
, (Framework.extendResource (coreCabalFileRev coreR)) {
Framework.resourceDesc = [(Framework.GET, vInfo)]
, Framework.resourceGet =
[("json", servePackageBasicDescription coreR userFeature
preferred)]
}
]
return $ PackageInfoJSONFeature {
packageInfoJSONFeatureInterface =
(Framework.emptyHackageFeature "package-info-json")
{ Framework.featureDesc = "Provide JSON endpoints for basic package descriptions"
, Framework.featureResources = jsonResources
, Framework.featureCaches = []
, Framework.featurePostInit = pure ()
, Framework.featureState = []
}
}
-- | Pure function for extracting basic package info from a Cabal file
getBasicDescription
:: UTCTime
-- ^ Time of upload
-> CabalFileText
-> Int
-- ^ Metadata revision. This will be added to the resulting
-- @PackageBasicDescription@
-> Either String PackageBasicDescription
getBasicDescription uploadedAt (CabalFileText cf) metadataRev =
let parseResult = PkgDescr.parseGenericPackageDescription (BS.toStrict cf)
in case PkgDescr.runParseResult parseResult of
(_, Right pkg) -> let
pkgd = PkgDescr.packageDescription pkg
pbd_author = T.pack . fromShortText $ PkgDescr.author pkgd
pbd_copyright = T.pack . fromShortText $ PkgDescr.copyright pkgd
pbd_synopsis = T.pack . fromShortText $ PkgDescr.synopsis pkgd
pbd_description = T.pack . fromShortText $ PkgDescr.description pkgd
pbd_license = either id licenseToSPDX $
PkgDescr.licenseRaw pkgd
pbd_homepage = T.pack . fromShortText $ PkgDescr.homepage pkgd
pbd_metadata_revision = metadataRev
pbd_uploaded_at = uploadedAt
in
return $ PackageBasicDescription {..}
(_, Left (_, perrs)) ->
let errs = List.intersperse '\n' $ mconcat $ for (toList perrs) $ \err -> Parsec.showPError "" err
in Left $ "Could not parse cabal file: "
<> errs
basicDescriptionToDTO :: UserName -> PackageBasicDescription -> PackageBasicDescriptionDTO
basicDescriptionToDTO uploader d =
PackageBasicDescriptionDTO
{ license = d.pbd_license
, copyright = d.pbd_copyright
, synopsis = d.pbd_synopsis
, description = d.pbd_description
, author = d.pbd_author
, homepage = d.pbd_homepage
, metadata_revision = d.pbd_metadata_revision
, uploaded_at = d.pbd_uploaded_at
, uploader
}
-- | Get a JSON @PackageBasicDescription@ for a particular
-- package/version/metadata-revision
-- OR
-- A listing of versions and their deprecation states
servePackageBasicDescription
:: CoreResource
-> UserFeature
-> Preferred.VersionsFeature
-> Framework.DynamicPath
-- ^ URI specifying a package and version `e.g. lens or lens-4.11`
-> Framework.ServerPartE Framework.Response
servePackageBasicDescription resource userFeature preferred dpath = do
let metadataRev :: Maybe Int = lookup "revision" dpath >>= Framework.fromReqURI
pkgid@(PackageIdentifier name version) <- packageInPath resource dpath
guardValidPackageName resource name
if version /= nullVersion
then fetchDescr pkgid metadataRev
else Framework.toResponse . Aeson.toJSON <$> getVersionListing name
where
fetchDescr
:: PackageIdentifier
-> Maybe Int
-> Framework.ServerPartE Framework.Response
fetchDescr pkgid metadataRev = do
guardValidPackageId resource pkgid
pkg <- lookupPackageId resource pkgid
let metadataRevs = fst <$> pkgMetadataRevisions pkg
uploadInfos = snd <$> pkgMetadataRevisions pkg
nMetadata = Vector.length metadataRevs
metadataInd = fromMaybe (nMetadata - 1) metadataRev
descr <- getPackageDescr metadataInd nMetadata metadataRevs uploadInfos
return $ Framework.toResponse $ Aeson.toJSON descr
getPackageDescr metadataInd nMetadata metadataRevs uploadInfos = do
when (metadataInd < 0 || metadataInd >= nMetadata)
(Framework.errNotFound "Revision not found"
[Framework.MText
$ "There are " <> show nMetadata <> " metadata revisions. Index "
<> show metadataInd <> " is out of bounds."]
)
let cabalFile = metadataRevs Vector.! metadataInd
uploadedAt = fst $ uploadInfos Vector.! metadataInd
uploaderId = snd $ uploadInfos Vector.! metadataInd
uploader <- userName <$> lookupUserInfo userFeature uploaderId
let pkgDescr = getBasicDescription uploadedAt cabalFile metadataInd
case pkgDescr of
Left e -> Framework.errInternalError [Framework.MText e]
Right d -> do
let packageInfoDTO = basicDescriptionToDTO uploader d
return packageInfoDTO
getVersionListing name = do
pkgs <- lookupPackageName resource name
prefInfo <- Preferred.queryGetPreferredInfo preferred name
return
. PackageVersions
. Preferred.classifyVersions prefInfo
$ fmap packageVersion pkgs