{- | The PyPI registry __wire__ JSON, decoded into a typed model.

This is a __placeholder boundary__, the PyPI counterpart to
"Ecluse.Core.Registry.Npm.Wire". Écluse serves only npm, so the PyPI wire model covers
just the one shape another part of the system reads from a PyPI project: the list
of published version strings, taken from the @releases@ object of the
@\/pypi\/{project}\/json@ response. The per-release metadata (file URLs, digests,
@requires-python@) is left as an opaque 'Value' rather than modelled, so a full
PyPI adapter can grow this module the way the npm wire layer models the packument
without disturbing callers that only want the version listing.

Like the npm wire layer, the decoder is __lenient__: a document with no @releases@
object yields an empty listing rather than a decode failure, so a partial or
unexpected body still parses to "no versions" instead of throwing.
-}
module Ecluse.Core.Registry.Pypi.Wire (
    ProjectJson (..),
    projectVersions,
) where

import Data.Aeson (FromJSON (parseJSON), Value, withObject, (.!=), (.:?))
import Data.Map.Strict qualified as Map

{- | A PyPI project's @\/pypi\/{project}\/json@ document, modelled only as far as
its @releases@ map: each key is a published version string, each value the
(unmodelled) array of release files for that version.
-}
newtype ProjectJson = ProjectJson
    { ProjectJson -> Map Text Value
pjReleases :: Map Text Value
    -- ^ The @releases@ object: a published version string to its (opaque) file list.
    }
    deriving stock (ProjectJson -> ProjectJson -> Bool
(ProjectJson -> ProjectJson -> Bool)
-> (ProjectJson -> ProjectJson -> Bool) -> Eq ProjectJson
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ProjectJson -> ProjectJson -> Bool
== :: ProjectJson -> ProjectJson -> Bool
$c/= :: ProjectJson -> ProjectJson -> Bool
/= :: ProjectJson -> ProjectJson -> Bool
Eq, Int -> ProjectJson -> ShowS
[ProjectJson] -> ShowS
ProjectJson -> String
(Int -> ProjectJson -> ShowS)
-> (ProjectJson -> String)
-> ([ProjectJson] -> ShowS)
-> Show ProjectJson
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ProjectJson -> ShowS
showsPrec :: Int -> ProjectJson -> ShowS
$cshow :: ProjectJson -> String
show :: ProjectJson -> String
$cshowList :: [ProjectJson] -> ShowS
showList :: [ProjectJson] -> ShowS
Show)

instance FromJSON ProjectJson where
    parseJSON :: Value -> Parser ProjectJson
parseJSON = String
-> (Object -> Parser ProjectJson) -> Value -> Parser ProjectJson
forall a. String -> (Object -> Parser a) -> Value -> Parser a
withObject String
"PyPI project JSON" ((Object -> Parser ProjectJson) -> Value -> Parser ProjectJson)
-> (Object -> Parser ProjectJson) -> Value -> Parser ProjectJson
forall a b. (a -> b) -> a -> b
$ \Object
o ->
        Map Text Value -> ProjectJson
ProjectJson (Map Text Value -> ProjectJson)
-> Parser (Map Text Value) -> Parser ProjectJson
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Object
o Object -> Key -> Parser (Maybe (Map Text Value))
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"releases" Parser (Maybe (Map Text Value))
-> Map Text Value -> Parser (Map Text Value)
forall a. Parser (Maybe a) -> a -> Parser a
.!= Map Text Value
forall a. Monoid a => a
mempty

{- | The published version strings of a PyPI project: the keys of its @releases@
map, exactly as PyPI lists them.
-}
projectVersions :: ProjectJson -> [Text]
projectVersions :: ProjectJson -> [Text]
projectVersions = Map Text Value -> [Text]
forall k a. Map k a -> [k]
Map.keys (Map Text Value -> [Text])
-> (ProjectJson -> Map Text Value) -> ProjectJson -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ProjectJson -> Map Text Value
pjReleases