11{-# LANGUAGE RankNTypes #-}
2+ {-# LANGUAGE OverloadedRecordDot #-}
23{-# LANGUAGE RecordWildCards #-}
34{-# LANGUAGE ScopedTypeVariables #-}
5+ {-# LANGUAGE NamedFieldPuns #-}
46
57module Distribution.Server.Features.PackageInfoJSON (
68 PackageInfoJSONFeature (.. )
@@ -15,48 +17,102 @@ import Prelude ()
1517import Distribution.Server.Prelude
1618
1719import qualified Data.Aeson as Aeson
20+ import Data.Aeson ((.=) )
21+ import qualified Data.Aeson.Key as Key
1822import qualified Data.ByteString.Lazy.Char8 as BS (toStrict )
23+ import qualified Data.Map.Strict as Map
1924import qualified Data.Text as T
2025import qualified Data.Vector as Vector
2126
2227import Distribution.License (licenseToSPDX )
2328import Distribution.Package (PackageIdentifier (.. ),
24- PackageName , packageName ,
2529 packageVersion )
2630import qualified Distribution.Parsec as Parsec
2731import qualified Distribution.PackageDescription.Parsec as PkgDescr
32+ import Distribution.Text (display )
2833import qualified Distribution.Types.GenericPackageDescription as PkgDescr
2934import qualified Distribution.Types.PackageDescription as PkgDescr
30- import Distribution.Version (nullVersion )
35+ import qualified Distribution.Pretty as Pretty
36+ import Distribution.SPDX.License (License )
37+ import Distribution.Version (nullVersion , Version )
3138
32- import Distribution.Server.Framework ((</>) )
33- import qualified Distribution.Server.Framework as Framework
34- import Distribution.Server.Features.Core (CoreFeature (.. ),
35- CoreResource (.. ),
36- isPackageChangeAny )
39+ import qualified Distribution.Server.Framework as Framework
40+ import Distribution.Server.Features.Core (CoreFeature (.. ),
41+ CoreResource (.. ))
3742import qualified Distribution.Server.Features.PreferredVersions as Preferred
38- import Distribution.Server.Packages.Types (CabalFileText (.. ), pkgMetadataRevisions )
39- import Distribution.Server.Framework.BackupRestore (RestoreBackup (.. ))
40-
41- import Distribution.Server.Features.PackageInfoJSON.State (PackageBasicDescription (.. ),
42- PackageVersions (.. ),
43- PackageInfoState (.. ),
44- GetPackageInfo (.. ),
45- ReplacePackageInfo (.. ),
46- GetDescriptionFor (.. ),
47- SetDescriptionFor (.. ),
48- GetVersionsFor (.. ),
49- SetVersionsFor (.. ),
50- initialPackageInfoState
51- )
43+ import Distribution.Server.Packages.Types (CabalFileText (.. ), pkgMetadataRevisions )
44+
5245import Distribution.Utils.ShortText (fromShortText )
5346import Data.Foldable (toList )
5447import Data.Traversable (for )
5548import qualified Data.List as List
5649import Data.Time (UTCTime )
57- import Distribution.Server.Users.Types (UserName , UserInfo (.. ))
50+ import Distribution.Server.Users.Types (UserName ( .. ) , UserInfo (.. ))
5851import Distribution.Server.Features.Users (UserFeature (lookupUserInfo ))
5952
53+ data PackageBasicDescription = PackageBasicDescription
54+ { pbd_license :: ! License
55+ , pbd_copyright :: ! T. Text
56+ , pbd_synopsis :: ! T. Text
57+ , pbd_description :: ! T. Text
58+ , pbd_author :: ! T. Text
59+ , pbd_homepage :: ! T. Text
60+ , pbd_metadata_revision :: ! Int
61+ , pbd_uploaded_at :: ! UTCTime
62+ } deriving (Eq , Show )
63+
64+
65+
66+ -- | Data type used in the `/package/:packagename` JSON endpoint
67+ data PackageBasicDescriptionDTO = PackageBasicDescriptionDTO
68+ { license :: ! License
69+ , copyright :: ! T. Text
70+ , synopsis :: ! T. Text
71+ , description :: ! T. Text
72+ , author :: ! T. Text
73+ , homepage :: ! T. Text
74+ , metadata_revision :: ! Int
75+ , uploaded_at :: ! UTCTime
76+ , uploader :: ! UserName
77+ } deriving (Eq , Show )
78+
79+ instance Aeson. ToJSON PackageBasicDescriptionDTO where
80+ toJSON PackageBasicDescriptionDTO {.. } =
81+ Aeson. object
82+ [ Key. fromString " license" .= Pretty. prettyShow license
83+ , Key. fromString " copyright" .= copyright
84+ , Key. fromString " synopsis" .= synopsis
85+ , Key. fromString " description" .= description
86+ , Key. fromString " author" .= author
87+ , Key. fromString " homepage" .= homepage
88+ , Key. fromString " metadata_revision" .= metadata_revision
89+ , Key. fromString " uploaded_at" .= uploaded_at
90+ , Key. fromString " uploader" .= uploader
91+ ]
92+
93+
94+ -- | An index of versions for one Hackage package
95+ -- and their preferred/deprecated status
96+ newtype PackageVersions = PackageVersions {
97+ unPackageVersions :: [(Version , Preferred. VersionStatus )]
98+ } deriving (Eq , Show )
99+
100+ -- | This encoding of @PackageVersions@ is used in the
101+ -- `/package/$package` endpoint (when the URI doesn't specify)
102+ -- a version. Any change here is an API change.
103+ instance Aeson. ToJSON PackageVersions where
104+ toJSON (PackageVersions p) =
105+ Aeson. toJSON
106+ $ Map. mapKeys display
107+ $ fmap encodeStatus
108+ $ Map. fromList p
109+ where
110+ encodeStatus = \ case
111+ Preferred. NormalVersion -> " normal"
112+ Preferred. DeprecatedVersion -> " deprecated"
113+ Preferred. UnpreferredVersion -> " unpreferred"
114+
115+
60116
61117data PackageInfoJSONFeature = PackageInfoJSONFeature {
62118 packageInfoJSONFeatureInterface :: Framework. HackageFeature
@@ -76,14 +132,10 @@ data PackageInfoJSONResource = PackageInfoJSONResource {
76132-- | Initializing our feature involves adding JSON variants to the
77133-- endpoints that serve basic information about a package-version,
78134-- and a packages version deprecation status.
79- -- Additionally we set up caching for these endpoints,
80- -- and attach a package change hook that invalidates the cache
81- -- line for a package when it changes
82135initPackageInfoJSONFeature
83136 :: Framework. ServerEnv
84137 -> IO (CoreFeature -> Preferred. VersionsFeature -> UserFeature -> IO PackageInfoJSONFeature )
85- initPackageInfoJSONFeature env = do
86- packageInfoState <- packageInfoStateComponent False (Framework. serverStateDir env)
138+ initPackageInfoJSONFeature _env = do
87139 return $ \ core preferred userFeature -> do
88140
89141 let coreR = coreResource core
@@ -97,53 +149,38 @@ initPackageInfoJSONFeature env = do
97149 Framework. resourceDesc = [(Framework. GET , info)]
98150 , Framework. resourceGet =
99151 [(" json" , servePackageBasicDescription coreR userFeature
100- preferred packageInfoState )]
152+ preferred)]
101153 }
102154 , (Framework. extendResource (coreCabalFileRev coreR)) {
103155 Framework. resourceDesc = [(Framework. GET , vInfo)]
104156 , Framework. resourceGet =
105157 [(" json" , servePackageBasicDescription coreR userFeature
106- preferred packageInfoState )]
158+ preferred)]
107159 }
108160 ]
109161
110- -- When a package is modified in any way, delet all its
111- -- PackageInfoState cache lines.
112- -- They will be recalculated next time the endpoint
113- -- is hit
114- postInit = Framework. registerHookJust
115- (packageChangeHook core)
116- isPackageChangeAny $ \ (pkgid, _) -> do
117-
118- Framework. updateState packageInfoState $
119- SetDescriptionFor (pkgid, Nothing ) Nothing
120- Framework. updateState packageInfoState $
121- SetVersionsFor (packageName pkgid) Nothing
122-
123162 return $ PackageInfoJSONFeature {
124163 packageInfoJSONFeatureInterface =
125164 (Framework. emptyHackageFeature " package-info-json" )
126165 { Framework. featureDesc = " Provide JSON endpoints for basic package descriptions"
127166 , Framework. featureResources = jsonResources
128167 , Framework. featureCaches = []
129- , Framework. featurePostInit = postInit
130- , Framework. featureState =
131- [Framework. abstractAcidStateComponent packageInfoState]
168+ , Framework. featurePostInit = pure ()
169+ , Framework. featureState = []
132170 }
133171 }
134172
135173
136174-- | Pure function for extracting basic package info from a Cabal file
137175getBasicDescription
138- :: UserName
139- -> UTCTime
176+ :: UTCTime
140177 -- ^ Time of upload
141178 -> CabalFileText
142179 -> Int
143180 -- ^ Metadata revision. This will be added to the resulting
144181 -- @PackageBasicDescription@
145182 -> Either String PackageBasicDescription
146- getBasicDescription uploader uploadedAt (CabalFileText cf) metadataRev =
183+ getBasicDescription uploadedAt (CabalFileText cf) metadataRev =
147184 let parseResult = PkgDescr. parseGenericPackageDescription (BS. toStrict cf)
148185 in case PkgDescr. runParseResult parseResult of
149186 (_, Right pkg) -> let
@@ -157,14 +194,26 @@ getBasicDescription uploader uploadedAt (CabalFileText cf) metadataRev =
157194 pbd_homepage = T. pack . fromShortText $ PkgDescr. homepage pkgd
158195 pbd_metadata_revision = metadataRev
159196 pbd_uploaded_at = uploadedAt
160- pbd_uploader = uploader
161197 in
162198 return $ PackageBasicDescription {.. }
163199 (_, Left (_, perrs)) ->
164200 let errs = List. intersperse ' \n ' $ mconcat $ for (toList perrs) $ \ err -> Parsec. showPError " " err
165201 in Left $ " Could not parse cabal file: "
166202 <> errs
167203
204+ basicDescriptionToDTO :: UserName -> PackageBasicDescription -> PackageBasicDescriptionDTO
205+ basicDescriptionToDTO uploader d =
206+ PackageBasicDescriptionDTO
207+ { license = d. pbd_license
208+ , copyright = d. pbd_copyright
209+ , synopsis = d. pbd_synopsis
210+ , description = d. pbd_description
211+ , author = d. pbd_author
212+ , homepage = d. pbd_homepage
213+ , metadata_revision = d. pbd_metadata_revision
214+ , uploaded_at = d. pbd_uploaded_at
215+ , uploader
216+ }
168217
169218-- | Get a JSON @PackageBasicDescription@ for a particular
170219-- package/version/metadata-revision
@@ -174,48 +223,38 @@ servePackageBasicDescription
174223 :: CoreResource
175224 -> UserFeature
176225 -> Preferred. VersionsFeature
177- -> Framework. StateComponent Framework. AcidState PackageInfoState
178226 -> Framework. DynamicPath
179227 -- ^ URI specifying a package and version `e.g. lens or lens-4.11`
180228 -> Framework. ServerPartE Framework. Response
181- servePackageBasicDescription resource userFeature preferred packageInfoState dpath = do
229+ servePackageBasicDescription resource userFeature preferred dpath = do
182230
183231 let metadataRev :: Maybe Int = lookup " revision" dpath >>= Framework. fromReqURI
184232
185233 pkgid@ (PackageIdentifier name version) <- packageInPath resource dpath
186234 guardValidPackageName resource name
187235
188236 if version /= nullVersion
189- then lookupOrInsertDescr pkgid metadataRev
190- else lookupOrInsertVersions name
237+ then fetchDescr pkgid metadataRev
238+ else Framework. toResponse . Aeson. toJSON <$> getVersionListing name
191239
192240 where
193241
194- lookupOrInsertDescr
242+ fetchDescr
195243 :: PackageIdentifier
196244 -> Maybe Int
197245 -> Framework. ServerPartE Framework. Response
198- lookupOrInsertDescr pkgid metadataRev = do
199- cachedDescr <- Framework. queryState packageInfoState $
200- GetDescriptionFor (pkgid, metadataRev)
201- descr :: PackageBasicDescription <- case cachedDescr of
202- Just d -> return d
203- Nothing -> do
204- d <- getPackageDescr pkgid metadataRev
205- Framework. updateState packageInfoState $
206- SetDescriptionFor (pkgid, metadataRev) (Just d)
207- return d
208- return $ Framework. toResponse $ Aeson. toJSON descr
209-
210- getPackageDescr pkgid metadataRev = do
246+ fetchDescr pkgid metadataRev = do
211247 guardValidPackageId resource pkgid
212248 pkg <- lookupPackageId resource pkgid
213249
214250 let metadataRevs = fst <$> pkgMetadataRevisions pkg
215251 uploadInfos = snd <$> pkgMetadataRevisions pkg
216252 nMetadata = Vector. length metadataRevs
217253 metadataInd = fromMaybe (nMetadata - 1 ) metadataRev
254+ descr <- getPackageDescr metadataInd nMetadata metadataRevs uploadInfos
255+ return $ Framework. toResponse $ Aeson. toJSON descr
218256
257+ getPackageDescr metadataInd nMetadata metadataRevs uploadInfos = do
219258 when (metadataInd < 0 || metadataInd >= nMetadata)
220259 (Framework. errNotFound " Revision not found"
221260 [Framework. MText
@@ -227,25 +266,12 @@ servePackageBasicDescription resource userFeature preferred packageInfoState dpa
227266 uploadedAt = fst $ uploadInfos Vector. ! metadataInd
228267 uploaderId = snd $ uploadInfos Vector. ! metadataInd
229268 uploader <- userName <$> lookupUserInfo userFeature uploaderId
230- let pkgDescr = getBasicDescription uploader uploadedAt cabalFile metadataInd
269+ let pkgDescr = getBasicDescription uploadedAt cabalFile metadataInd
231270 case pkgDescr of
232271 Left e -> Framework. errInternalError [Framework. MText e]
233- Right d -> return d
234-
235- lookupOrInsertVersions
236- :: PackageName
237- -> Framework. ServerPartE Framework. Response
238- lookupOrInsertVersions pkgname = do
239- cachedVersions <- Framework. queryState packageInfoState $
240- GetVersionsFor pkgname
241- vers :: PackageVersions <- case cachedVersions of
242- Just vs -> return vs
243- Nothing -> do
244- vs <- getVersionListing pkgname
245- Framework. updateState packageInfoState $
246- SetVersionsFor pkgname (Just vs)
247- return vs
248- return $ Framework. toResponse $ Aeson. toJSON vers
272+ Right d -> do
273+ let packageInfoDTO = basicDescriptionToDTO uploader d
274+ return packageInfoDTO
249275
250276 getVersionListing name = do
251277 pkgs <- lookupPackageName resource name
@@ -254,34 +280,3 @@ servePackageBasicDescription resource userFeature preferred packageInfoState dpa
254280 . PackageVersions
255281 . Preferred. classifyVersions prefInfo
256282 $ fmap packageVersion pkgs
257-
258-
259- -- | Our backup doesn't produce any entries, and backup restore
260- -- returns an empty state. Our responses are cheap enough to
261- -- compute that we would rather regenerate them by need than
262- -- deal with the complexity persisting backups in
263- -- yet-another-format
264- packageInfoStateComponent
265- :: Bool
266- -> FilePath
267- -> IO (Framework. StateComponent Framework. AcidState PackageInfoState )
268- packageInfoStateComponent freshDB stateDir = do
269- st <- Framework. openLocalStateFrom
270- (stateDir </> " db" </> " PackageInfoJSON" )
271- (initialPackageInfoState freshDB)
272- return Framework. StateComponent {
273- stateDesc = " Preferred package versions"
274- , stateHandle = st
275- , getState = Framework. query st GetPackageInfo
276- , putState = Framework. update st . ReplacePackageInfo
277- , resetState = packageInfoStateComponent True
278- , backupState = \ _ -> return []
279- , restoreState = nullRestore (initialPackageInfoState True )
280- }
281- where
282-
283- nullRestore :: PackageInfoState -> RestoreBackup PackageInfoState
284- nullRestore st = RestoreBackup {
285- restoreEntry = \ _ -> nullRestore <$> pure (initialPackageInfoState True )
286- , restoreFinalize = return st
287- }
0 commit comments