module Ecluse.Core.Registry.Npm.Project (
parsePackageInfoFromValue,
parseVersionList,
projectVersionEntry,
Projection (..),
projectName,
) where
import Data.Aeson (FromJSON (parseJSON), Object, Value, eitherDecodeStrict, withObject, (.!=), (.:?))
import Data.Aeson.Types (Parser, parseEither, parseMaybe)
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text qualified as T
import Data.Time (UTCTime)
import Ecluse.Core.Ecosystem (Ecosystem (Npm))
import Ecluse.Core.Package (
Artifact (..),
ArtifactKind (Tarball),
Availability (Available, Deprecated),
CodeExecSignal (NoCodeOnInstall, RunsCodeOnInstall),
Hash,
HashAlg (SHA1),
InvalidEntry (..),
InvalidEntryKind (InvalidDistTag, InvalidPublishTime, InvalidVersionManifest),
PackageDetails (..),
PackageInfo (..),
PackageName,
Person (..),
Scope,
Trust (TrustUnknown),
mkHash,
mkPackageName,
mkScope,
mkSriHashes,
)
import Ecluse.Core.Registry (ParseError (..), RegistryResponse (responseBody))
import Ecluse.Core.Registry.Npm.Wire (
Dist (..),
License (LicenseObject, LicenseSpdx),
VersionManifest (..),
)
import Ecluse.Core.Registry.Npm.Wire qualified as Wire
import Ecluse.Core.Registry.WireSupport (
NameAgreement (NameAgrees, NameDisagrees),
checkNameAgreement,
partitionLenient,
)
import Ecluse.Core.Text (lastPathSegment)
import Ecluse.Core.Version (Version, mkVersion, unVersion)
data WirePackument = WirePackument
{ WirePackument -> Text
wpName :: Text
, WirePackument -> Map Text Text
wpDistTags :: Map Text Text
, WirePackument -> Map Text VersionEntry
wpVersions :: Map Text VersionEntry
, WirePackument -> Map Text UTCTime
wpTime :: Map Text UTCTime
, WirePackument -> [InvalidEntry]
wpInvalidEntries :: [InvalidEntry]
}
instance FromJSON WirePackument where
parseJSON :: Value -> Parser WirePackument
parseJSON = String
-> (Object -> Parser WirePackument)
-> Value
-> Parser WirePackument
forall a. String -> (Object -> Parser a) -> Value -> Parser a
withObject String
"npm packument" ((Object -> Parser WirePackument) -> Value -> Parser WirePackument)
-> (Object -> Parser WirePackument)
-> Value
-> Parser WirePackument
forall a b. (a -> b) -> a -> b
$ \Object
o -> do
name <- Object
o Object -> Key -> Parser (Maybe Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"name" Parser (Maybe Text) -> Text -> Parser Text
forall a. Parser (Maybe a) -> a -> Parser a
.!= Text
""
(distTags, distTagDrops) <- lenientDistTags o
(versions, versionDrops) <- lenientVersionMap o
(time, timeDrops) <- lenientTimeMap (Map.keysSet versions) o
pure
WirePackument
{ wpName = name
, wpDistTags = distTags
, wpVersions = versions
, wpTime = time
,
wpInvalidEntries = versionDrops <> distTagDrops <> timeDrops
}
lenientVersionMap :: Object -> Parser (Map Text VersionEntry, [InvalidEntry])
lenientVersionMap :: Object -> Parser (Map Text VersionEntry, [InvalidEntry])
lenientVersionMap Object
o = do
raw <- Object
o Object -> Key -> Parser (Maybe (Map Text Value))
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"versions" 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
pure (partitionLenient InvalidVersionManifest (parseEither parseJSON) raw)
lenientDistTags :: Object -> Parser (Map Text Text, [InvalidEntry])
lenientDistTags :: Object -> Parser (Map Text Text, [InvalidEntry])
lenientDistTags Object
o = do
raw <- Object
o Object -> Key -> Parser (Maybe (Map Text Value))
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"dist-tags" 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
pure (partitionLenient InvalidDistTag (parseEither parseJSON) raw)
lenientTimeMap :: Set Text -> Object -> Parser (Map Text UTCTime, [InvalidEntry])
lenientTimeMap :: Set Text -> Object -> Parser (Map Text UTCTime, [InvalidEntry])
lenientTimeMap Set Text
versionKeys Object
o = do
raw <- Object
o Object -> Key -> Parser (Maybe (Map Text Value))
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"time" 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
let (kept, dropped) = partitionLenient InvalidPublishTime (parseEither parseJSON) raw
pure (kept, filter ((`Set.member` versionKeys) . invalidKey) dropped)
data VersionEntry = VersionEntry
{ VersionEntry -> VersionManifest
veManifest :: VersionManifest
, VersionEntry -> Maybe Person
vePublisher :: Maybe Wire.Person
}
instance FromJSON VersionEntry where
parseJSON :: Value -> Parser VersionEntry
parseJSON Value
v =
String
-> (Object -> Parser VersionEntry) -> Value -> Parser VersionEntry
forall a. String -> (Object -> Parser a) -> Value -> Parser a
withObject String
"npm version object" (\Object
o -> VersionManifest -> Maybe Person -> VersionEntry
VersionEntry (VersionManifest -> Maybe Person -> VersionEntry)
-> Parser VersionManifest -> Parser (Maybe Person -> VersionEntry)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Value -> Parser VersionManifest
forall a. FromJSON a => Value -> Parser a
parseJSON Value
v Parser (Maybe Person -> VersionEntry)
-> Parser (Maybe Person) -> Parser VersionEntry
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Object
o Object -> Key -> Parser (Maybe Person)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"_npmUser") Value
v
data Projection
=
Projected PackageInfo
|
NameMismatch Text
deriving stock (Projection -> Projection -> Bool
(Projection -> Projection -> Bool)
-> (Projection -> Projection -> Bool) -> Eq Projection
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Projection -> Projection -> Bool
== :: Projection -> Projection -> Bool
$c/= :: Projection -> Projection -> Bool
/= :: Projection -> Projection -> Bool
Eq, Int -> Projection -> ShowS
[Projection] -> ShowS
Projection -> String
(Int -> Projection -> ShowS)
-> (Projection -> String)
-> ([Projection] -> ShowS)
-> Show Projection
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Projection -> ShowS
showsPrec :: Int -> Projection -> ShowS
$cshow :: Projection -> String
show :: Projection -> String
$cshowList :: [Projection] -> ShowS
showList :: [Projection] -> ShowS
Show)
parsePackageInfoFromValue :: PackageName -> Value -> Either ParseError Projection
parsePackageInfoFromValue :: PackageName -> Value -> Either ParseError Projection
parsePackageInfoFromValue PackageName
requestedName Value
value =
Value -> Either ParseError WirePackument
decodePackumentValue Value
value Either ParseError WirePackument
-> (WirePackument -> Either ParseError Projection)
-> Either ParseError Projection
forall a b.
Either ParseError a
-> (a -> Either ParseError b) -> Either ParseError b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= PackageName -> WirePackument -> Either ParseError Projection
projectValidated PackageName
requestedName
projectValidated :: PackageName -> WirePackument -> Either ParseError Projection
projectValidated :: PackageName -> WirePackument -> Either ParseError Projection
projectValidated PackageName
requestedName WirePackument
pkmt = do
info <- WirePackument -> Either ParseError PackageInfo
projectPackageInfo WirePackument
pkmt
pure $ case checkNameAgreement requestedName (infoName info) of
NameAgreement
NameAgrees -> PackageInfo -> Projection
Projected PackageInfo
info
NameDisagrees Text
reported -> Text -> Projection
NameMismatch Text
reported
projectPackageInfo :: WirePackument -> Either ParseError PackageInfo
projectPackageInfo :: WirePackument -> Either ParseError PackageInfo
projectPackageInfo WirePackument
pkmt = do
name <- Text -> Either ParseError PackageName
projectName (WirePackument -> Text
wpName WirePackument
pkmt)
pure
PackageInfo
{ infoName = name
, infoVersions = projectVersions name pkmt
, infoDistTags = projectDistTags pkmt
, infoInvalidEntries = wpInvalidEntries pkmt
}
projectVersionEntry :: PackageName -> Version -> Maybe UTCTime -> Value -> Maybe PackageDetails
projectVersionEntry :: PackageName
-> Version -> Maybe UTCTime -> Value -> Maybe PackageDetails
projectVersionEntry PackageName
name Version
version Maybe UTCTime
publishedAt Value
value =
PackageName
-> Version -> Maybe UTCTime -> VersionEntry -> PackageDetails
projectDetails PackageName
name Version
version Maybe UTCTime
publishedAt (VersionEntry -> PackageDetails)
-> Maybe VersionEntry -> Maybe PackageDetails
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (Value -> Parser VersionEntry) -> Value -> Maybe VersionEntry
forall a b. (a -> Parser b) -> a -> Maybe b
parseMaybe Value -> Parser VersionEntry
forall a. FromJSON a => Value -> Parser a
parseJSON Value
value
parseVersionList :: RegistryResponse -> Either ParseError [Version]
parseVersionList :: RegistryResponse -> Either ParseError [Version]
parseVersionList RegistryResponse
resp = do
pkmt <- RegistryResponse -> Either ParseError WirePackument
decodePackument RegistryResponse
resp
pure (map (mkVersion Npm) (Map.keys (wpVersions pkmt)))
decodePackument :: RegistryResponse -> Either ParseError WirePackument
decodePackument :: RegistryResponse -> Either ParseError WirePackument
decodePackument =
(String -> ParseError)
-> Either String WirePackument -> Either ParseError WirePackument
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (Text -> ParseError
ParseError (Text -> ParseError) -> (String -> Text) -> String -> ParseError
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Text
forall a. ToText a => a -> Text
toText) (Either String WirePackument -> Either ParseError WirePackument)
-> (RegistryResponse -> Either String WirePackument)
-> RegistryResponse
-> Either ParseError WirePackument
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> Either String WirePackument
forall a. FromJSON a => ByteString -> Either String a
eitherDecodeStrict (ByteString -> Either String WirePackument)
-> (RegistryResponse -> ByteString)
-> RegistryResponse
-> Either String WirePackument
forall b c a. (b -> c) -> (a -> b) -> a -> c
. RegistryResponse -> ByteString
responseBody
decodePackumentValue :: Value -> Either ParseError WirePackument
decodePackumentValue :: Value -> Either ParseError WirePackument
decodePackumentValue =
(String -> ParseError)
-> Either String WirePackument -> Either ParseError WirePackument
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (Text -> ParseError
ParseError (Text -> ParseError) -> (String -> Text) -> String -> ParseError
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Text
forall a. ToText a => a -> Text
toText) (Either String WirePackument -> Either ParseError WirePackument)
-> (Value -> Either String WirePackument)
-> Value
-> Either ParseError WirePackument
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Value -> Parser WirePackument)
-> Value -> Either String WirePackument
forall a b. (a -> Parser b) -> a -> Either String b
parseEither Value -> Parser WirePackument
forall a. FromJSON a => Value -> Parser a
parseJSON
projectVersions :: PackageName -> WirePackument -> Map Text PackageDetails
projectVersions :: PackageName -> WirePackument -> Map Text PackageDetails
projectVersions PackageName
name WirePackument
pkmt =
(Text -> VersionEntry -> PackageDetails)
-> Map Text VersionEntry -> Map Text PackageDetails
forall k a b. (k -> a -> b) -> Map k a -> Map k b
Map.mapWithKey Text -> VersionEntry -> PackageDetails
projectAt (WirePackument -> Map Text VersionEntry
wpVersions WirePackument
pkmt)
where
projectAt :: Text -> VersionEntry -> PackageDetails
projectAt Text
rawVersion =
PackageName
-> Version -> Maybe UTCTime -> VersionEntry -> PackageDetails
projectDetails
PackageName
name
(Ecosystem -> Text -> Version
mkVersion Ecosystem
Npm Text
rawVersion)
(Text -> Map Text UTCTime -> Maybe UTCTime
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup Text
rawVersion (WirePackument -> Map Text UTCTime
wpTime WirePackument
pkmt))
projectDetails :: PackageName -> Version -> Maybe UTCTime -> VersionEntry -> PackageDetails
projectDetails :: PackageName
-> Version -> Maybe UTCTime -> VersionEntry -> PackageDetails
projectDetails PackageName
name Version
version Maybe UTCTime
publishedAt VersionEntry
entry =
PackageDetails
{ pkgName :: PackageName
pkgName = PackageName
name
, pkgVersion :: Version
pkgVersion = Version
version
, pkgPublishedAt :: Maybe UTCTime
pkgPublishedAt = Maybe UTCTime
publishedAt
, pkgInstallCode :: CodeExecSignal
pkgInstallCode = VersionManifest -> CodeExecSignal
installCode VersionManifest
vm
, pkgTrust :: Trust
pkgTrust = Trust
TrustUnknown
, pkgAvailability :: Availability
pkgAvailability = VersionManifest -> Availability
availability VersionManifest
vm
, pkgArtifacts :: NonEmpty Artifact
pkgArtifacts = Version -> Dist -> Artifact
projectArtifact Version
version (VersionManifest -> Dist
vmDist VersionManifest
vm) Artifact -> [Artifact] -> NonEmpty Artifact
forall a. a -> [a] -> NonEmpty a
:| []
, pkgLicenses :: [Text]
pkgLicenses = [Text] -> (License -> [Text]) -> Maybe License -> [Text]
forall b a. b -> (a -> b) -> Maybe a -> b
maybe [] (Text -> [Text]
OneItem [Text] -> [Text]
forall x. One x => OneItem x -> x
one (Text -> [Text]) -> (License -> Text) -> License -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. License -> Text
licenseText) (VersionManifest -> Maybe License
vmLicense VersionManifest
vm)
, pkgPublisher :: Maybe Person
pkgPublisher = Person -> Person
projectPerson (Person -> Person) -> Maybe Person -> Maybe Person
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> VersionEntry -> Maybe Person
vePublisher VersionEntry
entry
}
where
vm :: VersionManifest
vm = VersionEntry -> VersionManifest
veManifest VersionEntry
entry
licenseText :: License -> Text
licenseText :: License -> Text
licenseText = \case
LicenseSpdx Text
spdx -> Text
spdx
LicenseObject Text
name Maybe Text
_url -> Text
name
installCode :: VersionManifest -> CodeExecSignal
installCode :: VersionManifest -> CodeExecSignal
installCode VersionManifest
vm
| Bool -> Bool
not ([Text] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Text]
hooks) =
Text -> CodeExecSignal
RunsCodeOnInstall (Text
"declares install script(s): " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> [Text] -> Text
T.intercalate Text
", " [Text]
hooks)
| VersionManifest -> Maybe Bool
vmHasInstallScript VersionManifest
vm Maybe Bool -> Maybe Bool -> Bool
forall a. Eq a => a -> a -> Bool
== Bool -> Maybe Bool
forall a. a -> Maybe a
Just Bool
True =
Text -> CodeExecSignal
RunsCodeOnInstall Text
"declares an install script (hasInstallScript)"
| Bool
otherwise = CodeExecSignal
NoCodeOnInstall
where
hooks :: [Text]
hooks = (Text -> Bool) -> [Text] -> [Text]
forall a. (a -> Bool) -> [a] -> [a]
filter (Text -> Map Text Text -> Bool
forall k a. Ord k => k -> Map k a -> Bool
`Map.member` VersionManifest -> Map Text Text
vmScripts VersionManifest
vm) [Text]
installHooks
installHooks :: [Text]
installHooks :: [Text]
installHooks = [Text
"preinstall", Text
"install", Text
"postinstall"]
availability :: VersionManifest -> Availability
availability :: VersionManifest -> Availability
availability VersionManifest
vm = Availability
-> (Text -> Availability) -> Maybe Text -> Availability
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Availability
Available Text -> Availability
Deprecated (VersionManifest -> Maybe Text
vmDeprecated VersionManifest
vm)
projectArtifact :: Version -> Dist -> Artifact
projectArtifact :: Version -> Dist -> Artifact
projectArtifact Version
version Dist
dist =
Artifact
{ artFilename :: Text
artFilename = Text -> Version -> Text
tarballFilename (Dist -> Text
distTarball Dist
dist) Version
version
, artUrl :: Text
artUrl = Dist -> Text
distTarball Dist
dist
, artKind :: ArtifactKind
artKind = ArtifactKind
Tarball
, artHashes :: [Hash]
artHashes = [Hash]
sriHashes [Hash] -> [Hash] -> [Hash]
forall a. Semigroup a => a -> a -> a
<> Maybe Hash -> [Hash]
forall a. Maybe a -> [a]
maybeToList Maybe Hash
sha1Hash
, artSize :: Maybe Int
artSize = Dist -> Maybe Int
distUnpackedSize Dist
dist
, artInterpreter :: Maybe Text
artInterpreter = Maybe Text
forall a. Maybe a
Nothing
, artYanked :: Bool
artYanked = Bool
False
, artProvenance :: Maybe Text
artProvenance = Maybe Text
forall a. Maybe a
Nothing
}
where
toHash :: HashAlg -> Text -> Maybe Hash
toHash :: HashAlg -> Text -> Maybe Hash
toHash HashAlg
alg = Either Text Hash -> Maybe Hash
forall l r. Either l r -> Maybe r
rightToMaybe (Either Text Hash -> Maybe Hash)
-> (Text -> Either Text Hash) -> Text -> Maybe Hash
forall b c a. (b -> c) -> (a -> b) -> a -> c
. HashAlg -> Text -> Either Text Hash
mkHash HashAlg
alg
sriHashes :: [Hash]
sriHashes = [Hash] -> (Text -> [Hash]) -> Maybe Text -> [Hash]
forall b a. b -> (a -> b) -> Maybe a -> b
maybe [] ((Text -> [Hash])
-> (NonEmpty Hash -> [Hash])
-> Either Text (NonEmpty Hash)
-> [Hash]
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either ([Hash] -> Text -> [Hash]
forall a b. a -> b -> a
const []) NonEmpty Hash -> [Hash]
forall a. NonEmpty a -> [a]
forall (t :: * -> *) a. Foldable t => t a -> [a]
toList (Either Text (NonEmpty Hash) -> [Hash])
-> (Text -> Either Text (NonEmpty Hash)) -> Text -> [Hash]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Either Text (NonEmpty Hash)
mkSriHashes) (Dist -> Maybe Text
distIntegrity Dist
dist)
sha1Hash :: Maybe Hash
sha1Hash = Dist -> Maybe Text
distShasum Dist
dist Maybe Text -> (Text -> Maybe Hash) -> Maybe Hash
forall a b. Maybe a -> (a -> Maybe b) -> Maybe b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= HashAlg -> Text -> Maybe Hash
toHash HashAlg
SHA1
tarballFilename :: Text -> Version -> Text
tarballFilename :: Text -> Version -> Text
tarballFilename Text
url Version
version =
Text -> Maybe Text -> Text
forall a. a -> Maybe a -> a
fromMaybe (Version -> Text
unVersion Version
version Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
".tgz") (Text -> Maybe Text
lastPathSegment Text
url)
projectDistTags :: WirePackument -> Map Text Version
projectDistTags :: WirePackument -> Map Text Version
projectDistTags = (Text -> Version) -> Map Text Text -> Map Text Version
forall a b k. (a -> b) -> Map k a -> Map k b
Map.map (Ecosystem -> Text -> Version
mkVersion Ecosystem
Npm) (Map Text Text -> Map Text Version)
-> (WirePackument -> Map Text Text)
-> WirePackument
-> Map Text Version
forall b c a. (b -> c) -> (a -> b) -> a -> c
. WirePackument -> Map Text Text
wpDistTags
projectName :: Text -> Either ParseError PackageName
projectName :: Text -> Either ParseError PackageName
projectName Text
raw
| Text -> Bool
T.null Text
raw = ParseError -> Either ParseError PackageName
forall a b. a -> Either a b
Left (Text -> ParseError
ParseError Text
"empty package name")
| Bool
otherwise = case Text -> Maybe (Scope, Text)
scopeOf Text
raw of
Just (Scope
scope, Text
base) -> PackageName -> Either ParseError PackageName
forall a b. b -> Either a b
Right (Ecosystem -> Maybe Scope -> Text -> PackageName
mkPackageName Ecosystem
Npm (Scope -> Maybe Scope
forall a. a -> Maybe a
Just Scope
scope) Text
base)
Maybe (Scope, Text)
Nothing -> PackageName -> Either ParseError PackageName
forall a b. b -> Either a b
Right (Ecosystem -> Maybe Scope -> Text -> PackageName
mkPackageName Ecosystem
Npm Maybe Scope
forall a. Maybe a
Nothing Text
raw)
scopeOf :: Text -> Maybe (Scope, Text)
scopeOf :: Text -> Maybe (Scope, Text)
scopeOf Text
raw = do
afterAt <- Text -> Text -> Maybe Text
T.stripPrefix Text
"@" Text
raw
let (scopeText, rest) = T.break (== '/') afterAt
base = Int -> Text -> Text
T.drop Int
1 Text
rest
guard (not (T.null scopeText))
guard (not (T.null base))
pure (mkScope scopeText, base)
projectPerson :: Wire.Person -> Person
projectPerson :: Person -> Person
projectPerson Person
p =
Person
{ personName :: Text
personName = Person -> Text
Wire.personName Person
p
, personEmail :: Maybe Text
personEmail = Person -> Maybe Text
Wire.personEmail Person
p
, personUrl :: Maybe Text
personUrl = Person -> Maybe Text
Wire.personUrl Person
p
}