-- SPDX-FileCopyrightText: 2026 Alexandra de Wit
--
-- SPDX-License-Identifier: MIT

{- | The npm wire JSON types and their decoders: the decode-only protocol boundary.

Every decoder is lenient in the ways the public registry requires. Unknown keys are
ignored, @license@ and a person arrive as a string or an object, @deprecated@ as a string
or a boolean, and an undecodable advisory @unpackedSize@ reads as absent rather than
failing the version. The shapes come from live captures of @registry.npmjs.org@, kept as
fixtures under @core\/test\/unit\/fixtures\/npm\/@.
-}
module Ecluse.Core.Registry.Npm.Wire (
    -- * Shared scalars
    Person (..),
    License (..),

    -- * The @dist@ object
    Dist (..),
    distFields,

    -- * Per-version manifest
    VersionManifest (..),
) where

import Data.Aeson (
    FromJSON (parseJSON),
    Value (Bool, Object, String),
    withObject,
    (.!=),
    (.:),
    (.:?),
 )

import Ecluse.Core.Json.Lenient (lenientOptional, typeMismatchOneOf)

{- | A person on a package: an author, maintainer, contributor, or a version's publisher.
A packed @"Name \<email\> (url)"@ string, npm's other form, stays verbatim in 'personName'.
-}
data Person = Person
    { Person -> Text
personName :: Text
    -- ^ The name, or the whole packed string as sent.
    , Person -> Maybe Text
personEmail :: Maybe Text
    , Person -> Maybe Text
personUrl :: Maybe Text
    }
    deriving stock (Person -> Person -> Bool
(Person -> Person -> Bool)
-> (Person -> Person -> Bool) -> Eq Person
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Person -> Person -> Bool
== :: Person -> Person -> Bool
$c/= :: Person -> Person -> Bool
/= :: Person -> Person -> Bool
Eq, Eq Person
Eq Person =>
(Person -> Person -> Ordering)
-> (Person -> Person -> Bool)
-> (Person -> Person -> Bool)
-> (Person -> Person -> Bool)
-> (Person -> Person -> Bool)
-> (Person -> Person -> Person)
-> (Person -> Person -> Person)
-> Ord Person
Person -> Person -> Bool
Person -> Person -> Ordering
Person -> Person -> Person
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: Person -> Person -> Ordering
compare :: Person -> Person -> Ordering
$c< :: Person -> Person -> Bool
< :: Person -> Person -> Bool
$c<= :: Person -> Person -> Bool
<= :: Person -> Person -> Bool
$c> :: Person -> Person -> Bool
> :: Person -> Person -> Bool
$c>= :: Person -> Person -> Bool
>= :: Person -> Person -> Bool
$cmax :: Person -> Person -> Person
max :: Person -> Person -> Person
$cmin :: Person -> Person -> Person
min :: Person -> Person -> Person
Ord, Int -> Person -> ShowS
[Person] -> ShowS
Person -> String
(Int -> Person -> ShowS)
-> (Person -> String) -> ([Person] -> ShowS) -> Show Person
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Person -> ShowS
showsPrec :: Int -> Person -> ShowS
$cshow :: Person -> String
show :: Person -> String
$cshowList :: [Person] -> ShowS
showList :: [Person] -> ShowS
Show)

instance FromJSON Person where
    parseJSON :: Value -> Parser Person
parseJSON = \case
        String Text
name -> Person -> Parser Person
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> Maybe Text -> Maybe Text -> Person
Person Text
name Maybe Text
forall a. Maybe a
Nothing Maybe Text
forall a. Maybe a
Nothing)
        Object Object
o ->
            Text -> Maybe Text -> Maybe Text -> Person
Person
                (Text -> Maybe Text -> Maybe Text -> Person)
-> Parser Text -> Parser (Maybe Text -> Maybe Text -> Person)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> 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
""
                Parser (Maybe Text -> Maybe Text -> Person)
-> Parser (Maybe Text) -> Parser (Maybe Text -> Person)
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 Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"email"
                Parser (Maybe Text -> Person)
-> Parser (Maybe Text) -> Parser Person
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 Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"url"
        Value
other -> String -> Value -> Parser Person
forall a. String -> Value -> Parser a
typeMismatchOneOf String
"Person (object or string)" Value
other

-- | A declared license, in npm's modern string form or its legacy object form.
data License
    = -- | An SPDX expression or identifier (@"MIT"@, @"(MIT OR Apache-2.0)"@).
      LicenseSpdx Text
    | -- | The legacy @{type, url?}@ object: a license name and a URL to its text.
      LicenseObject Text (Maybe Text)
    deriving stock (License -> License -> Bool
(License -> License -> Bool)
-> (License -> License -> Bool) -> Eq License
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: License -> License -> Bool
== :: License -> License -> Bool
$c/= :: License -> License -> Bool
/= :: License -> License -> Bool
Eq, Eq License
Eq License =>
(License -> License -> Ordering)
-> (License -> License -> Bool)
-> (License -> License -> Bool)
-> (License -> License -> Bool)
-> (License -> License -> Bool)
-> (License -> License -> License)
-> (License -> License -> License)
-> Ord License
License -> License -> Bool
License -> License -> Ordering
License -> License -> License
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: License -> License -> Ordering
compare :: License -> License -> Ordering
$c< :: License -> License -> Bool
< :: License -> License -> Bool
$c<= :: License -> License -> Bool
<= :: License -> License -> Bool
$c> :: License -> License -> Bool
> :: License -> License -> Bool
$c>= :: License -> License -> Bool
>= :: License -> License -> Bool
$cmax :: License -> License -> License
max :: License -> License -> License
$cmin :: License -> License -> License
min :: License -> License -> License
Ord, Int -> License -> ShowS
[License] -> ShowS
License -> String
(Int -> License -> ShowS)
-> (License -> String) -> ([License] -> ShowS) -> Show License
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> License -> ShowS
showsPrec :: Int -> License -> ShowS
$cshow :: License -> String
show :: License -> String
$cshowList :: [License] -> ShowS
showList :: [License] -> ShowS
Show)

instance FromJSON License where
    parseJSON :: Value -> Parser License
parseJSON = \case
        String Text
spdx -> License -> Parser License
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> License
LicenseSpdx Text
spdx)
        Object Object
o ->
            Text -> Maybe Text -> License
LicenseObject
                (Text -> Maybe Text -> License)
-> Parser Text -> Parser (Maybe Text -> License)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Object
o Object -> Key -> Parser (Maybe Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"type" Parser (Maybe Text) -> Text -> Parser Text
forall a. Parser (Maybe a) -> a -> Parser a
.!= Text
""
                Parser (Maybe Text -> License)
-> Parser (Maybe Text) -> Parser License
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 Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"url"
        Value
other -> String -> Value -> Parser License
forall a. String -> Value -> Parser a
typeMismatchOneOf String
"License (object or string)" Value
other

{- | The artifact descriptor every version manifest carries. A client fails the install
when the bytes mismatch, so a mirror or a URL rewrite preserves the digests byte for byte.
-}
data Dist = Dist
    { Dist -> Text
distTarball :: Text
    -- ^ Absolute URL of the @.tgz@ artifact. Always present.
    , Dist -> Maybe Text
distShasum :: Maybe Text
    -- ^ The tarball's SHA-1, hex-encoded (legacy integrity).
    , Dist -> Maybe Text
distIntegrity :: Maybe Text
    -- ^ The Subresource-Integrity string (@"sha512-..."@), preferred over the shasum.
    , Dist -> Maybe Int
distUnpackedSize :: Maybe Int
    {- ^ Unpacked size in bytes. Advisory, so a fractional, huge, or wrong-typed value
    reads as 'Nothing' instead of denying the whole packument.
    -}
    }
    deriving stock (Dist -> Dist -> Bool
(Dist -> Dist -> Bool) -> (Dist -> Dist -> Bool) -> Eq Dist
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Dist -> Dist -> Bool
== :: Dist -> Dist -> Bool
$c/= :: Dist -> Dist -> Bool
/= :: Dist -> Dist -> Bool
Eq, Eq Dist
Eq Dist =>
(Dist -> Dist -> Ordering)
-> (Dist -> Dist -> Bool)
-> (Dist -> Dist -> Bool)
-> (Dist -> Dist -> Bool)
-> (Dist -> Dist -> Bool)
-> (Dist -> Dist -> Dist)
-> (Dist -> Dist -> Dist)
-> Ord Dist
Dist -> Dist -> Bool
Dist -> Dist -> Ordering
Dist -> Dist -> Dist
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: Dist -> Dist -> Ordering
compare :: Dist -> Dist -> Ordering
$c< :: Dist -> Dist -> Bool
< :: Dist -> Dist -> Bool
$c<= :: Dist -> Dist -> Bool
<= :: Dist -> Dist -> Bool
$c> :: Dist -> Dist -> Bool
> :: Dist -> Dist -> Bool
$c>= :: Dist -> Dist -> Bool
>= :: Dist -> Dist -> Bool
$cmax :: Dist -> Dist -> Dist
max :: Dist -> Dist -> Dist
$cmin :: Dist -> Dist -> Dist
min :: Dist -> Dist -> Dist
Ord, Int -> Dist -> ShowS
[Dist] -> ShowS
Dist -> String
(Int -> Dist -> ShowS)
-> (Dist -> String) -> ([Dist] -> ShowS) -> Show Dist
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Dist -> ShowS
showsPrec :: Int -> Dist -> ShowS
$cshow :: Dist -> String
show :: Dist -> String
$cshowList :: [Dist] -> ShowS
showList :: [Dist] -> ShowS
Show)

-- | The @dist@ members the 'Dist' parser reads.
distFields :: [Text]
distFields :: [Text]
distFields = [Text
"tarball", Text
"shasum", Text
"integrity", Text
"unpackedSize"]

instance FromJSON Dist where
    parseJSON :: Value -> Parser Dist
parseJSON = String -> (Object -> Parser Dist) -> Value -> Parser Dist
forall a. String -> (Object -> Parser a) -> Value -> Parser a
withObject String
"Dist" ((Object -> Parser Dist) -> Value -> Parser Dist)
-> (Object -> Parser Dist) -> Value -> Parser Dist
forall a b. (a -> b) -> a -> b
$ \Object
o ->
        Text -> Maybe Text -> Maybe Text -> Maybe Int -> Dist
Dist
            (Text -> Maybe Text -> Maybe Text -> Maybe Int -> Dist)
-> Parser Text
-> Parser (Maybe Text -> Maybe Text -> Maybe Int -> Dist)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Object
o Object -> Key -> Parser Text
forall a. FromJSON a => Object -> Key -> Parser a
.: Key
"tarball"
            Parser (Maybe Text -> Maybe Text -> Maybe Int -> Dist)
-> Parser (Maybe Text) -> Parser (Maybe Text -> Maybe Int -> Dist)
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 Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"shasum"
            Parser (Maybe Text -> Maybe Int -> Dist)
-> Parser (Maybe Text) -> Parser (Maybe Int -> Dist)
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 Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"integrity"
            Parser (Maybe Int -> Dist) -> Parser (Maybe Int) -> Parser Dist
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Object -> Key -> Parser (Maybe Int)
forall a.
(FromJSON a, NFData a) =>
Object -> Key -> Parser (Maybe a)
lenientOptional Object
o Key
"unpackedSize"

{- | One version's manifest, decoding all three wire forms alike. The dependency maps and
the maintainer list stay unparsed: nothing reads them, and the cost falls on every version.
-}
data VersionManifest = VersionManifest
    { VersionManifest -> Text
vmName :: Text
    -- ^ The package name as sent, possibly scoped (@"\@scope\/name"@).
    , VersionManifest -> Text
vmVersion :: Text
    -- ^ The exact version string, kept opaque at this layer.
    , VersionManifest -> Dist
vmDist :: Dist
    , VersionManifest -> Maybe Text
vmDeprecated :: Maybe Text
    {- ^ The deprecation message. A boolean @true@ reads as @""@, and @false@, @null@,
    absence, or any other shape as 'Nothing'.
    -}
    , VersionManifest -> Maybe Bool
vmHasInstallScript :: Maybe Bool
    -- ^ Abbreviated form only. 'Nothing' in the full form, where 'vmScripts' carries it.
    , VersionManifest -> Map Text Text
vmScripts :: Map Text Text
    -- ^ The @scripts@ map, lifecycle name to command, empty when absent.
    , VersionManifest -> Maybe License
vmLicense :: Maybe License
    }
    deriving stock (VersionManifest -> VersionManifest -> Bool
(VersionManifest -> VersionManifest -> Bool)
-> (VersionManifest -> VersionManifest -> Bool)
-> Eq VersionManifest
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: VersionManifest -> VersionManifest -> Bool
== :: VersionManifest -> VersionManifest -> Bool
$c/= :: VersionManifest -> VersionManifest -> Bool
/= :: VersionManifest -> VersionManifest -> Bool
Eq, Int -> VersionManifest -> ShowS
[VersionManifest] -> ShowS
VersionManifest -> String
(Int -> VersionManifest -> ShowS)
-> (VersionManifest -> String)
-> ([VersionManifest] -> ShowS)
-> Show VersionManifest
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> VersionManifest -> ShowS
showsPrec :: Int -> VersionManifest -> ShowS
$cshow :: VersionManifest -> String
show :: VersionManifest -> String
$cshowList :: [VersionManifest] -> ShowS
showList :: [VersionManifest] -> ShowS
Show)

instance FromJSON VersionManifest where
    parseJSON :: Value -> Parser VersionManifest
parseJSON = String
-> (Object -> Parser VersionManifest)
-> Value
-> Parser VersionManifest
forall a. String -> (Object -> Parser a) -> Value -> Parser a
withObject String
"VersionManifest" ((Object -> Parser VersionManifest)
 -> Value -> Parser VersionManifest)
-> (Object -> Parser VersionManifest)
-> Value
-> Parser VersionManifest
forall a b. (a -> b) -> a -> b
$ \Object
o ->
        Text
-> Text
-> Dist
-> Maybe Text
-> Maybe Bool
-> Map Text Text
-> Maybe License
-> VersionManifest
VersionManifest
            (Text
 -> Text
 -> Dist
 -> Maybe Text
 -> Maybe Bool
 -> Map Text Text
 -> Maybe License
 -> VersionManifest)
-> Parser Text
-> Parser
     (Text
      -> Dist
      -> Maybe Text
      -> Maybe Bool
      -> Map Text Text
      -> Maybe License
      -> VersionManifest)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Object
o Object -> Key -> Parser Text
forall a. FromJSON a => Object -> Key -> Parser a
.: Key
"name"
            Parser
  (Text
   -> Dist
   -> Maybe Text
   -> Maybe Bool
   -> Map Text Text
   -> Maybe License
   -> VersionManifest)
-> Parser Text
-> Parser
     (Dist
      -> Maybe Text
      -> Maybe Bool
      -> Map Text Text
      -> Maybe License
      -> VersionManifest)
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 Text
forall a. FromJSON a => Object -> Key -> Parser a
.: Key
"version"
            Parser
  (Dist
   -> Maybe Text
   -> Maybe Bool
   -> Map Text Text
   -> Maybe License
   -> VersionManifest)
-> Parser Dist
-> Parser
     (Maybe Text
      -> Maybe Bool -> Map Text Text -> Maybe License -> VersionManifest)
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 Dist
forall a. FromJSON a => Object -> Key -> Parser a
.: Key
"dist"
            Parser
  (Maybe Text
   -> Maybe Bool -> Map Text Text -> Maybe License -> VersionManifest)
-> Parser (Maybe Text)
-> Parser
     (Maybe Bool -> Map Text Text -> Maybe License -> VersionManifest)
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> (Maybe Value -> Maybe Text
deprecatedNotice (Maybe Value -> Maybe Text)
-> Parser (Maybe Value) -> Parser (Maybe Text)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Object
o Object -> Key -> Parser (Maybe Value)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"deprecated")
            Parser
  (Maybe Bool -> Map Text Text -> Maybe License -> VersionManifest)
-> Parser (Maybe Bool)
-> Parser (Map Text Text -> Maybe License -> VersionManifest)
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 Bool)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"hasInstallScript"
            Parser (Map Text Text -> Maybe License -> VersionManifest)
-> Parser (Map Text Text)
-> Parser (Maybe License -> VersionManifest)
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 (Map Text Text))
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"scripts" Parser (Maybe (Map Text Text))
-> Map Text Text -> Parser (Map Text Text)
forall a. Parser (Maybe a) -> a -> Parser a
.!= Map Text Text
forall a. Monoid a => a
mempty
            Parser (Maybe License -> VersionManifest)
-> Parser (Maybe License) -> Parser VersionManifest
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 License)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"license"

deprecatedNotice :: Maybe Value -> Maybe Text
deprecatedNotice :: Maybe Value -> Maybe Text
deprecatedNotice = \case
    Just (String Text
message) -> Text -> Maybe Text
forall a. a -> Maybe a
Just Text
message
    Just (Bool Bool
True) -> Text -> Maybe Text
forall a. a -> Maybe a
Just Text
""
    Maybe Value
_ -> Maybe Text
forall a. Maybe a
Nothing