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

{- | What one OSV compile pass needs to know about the ecosystem it compiles.

osv.dev and Écluse do not always agree on the spelling, so a pass that carried one name would
either fetch a directory that does not exist or write an artifact the proxy's sync refuses. The
pass also needs the version grammar that orders the advisory bounds it ingests, and the fan-out
an ordinary advisory of the feed stays under.
-}
module Ecluse.Core.Osv.Ecosystem (
    OsvEcosystem (..),
    osvEcosystemFor,
    osvEcosystemNamed,
) where

import Ecluse.Core.Ecosystem (Ecosystem (Npm, PyPI, RubyGems), ecosystemName, parseEcosystem)

-- | The two spellings, the version grammar, and the fan-out bound one compile pass needs.
data OsvEcosystem = OsvEcosystem
    { OsvEcosystem -> Text
osvExportDirectory :: Text
    {- ^ osv.dev's own spelling: the directory its export archive sits under, and the value an
    advisory's affected package carries, which is what the row filter matches.
    -}
    , OsvEcosystem -> Text
osvWireName :: Text
    {- ^ Écluse's spelling ('ecosystemName'): it names the published artifact and stamps the
    @meta@ row the proxy's sync checks.
    -}
    , OsvEcosystem -> Maybe Ecosystem
osvEcosystemTag :: Maybe Ecosystem
    {- ^ The ecosystem whose version grammar orders this pass's advisory bounds. 'Nothing' for a
    name this build does not serve, and then the pass tallies nothing.
    -}
    , OsvEcosystem -> Int
osvMaxAdvisoryFanOut :: Int
    {- ^ Ranges one advisory of this feed may expand into before the ingest flags it as
    anomalous. The ingest keeps the advisory either way, so the number only sizes the alarm.
    -}
    }
    deriving stock (OsvEcosystem -> OsvEcosystem -> Bool
(OsvEcosystem -> OsvEcosystem -> Bool)
-> (OsvEcosystem -> OsvEcosystem -> Bool) -> Eq OsvEcosystem
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: OsvEcosystem -> OsvEcosystem -> Bool
== :: OsvEcosystem -> OsvEcosystem -> Bool
$c/= :: OsvEcosystem -> OsvEcosystem -> Bool
/= :: OsvEcosystem -> OsvEcosystem -> Bool
Eq, Int -> OsvEcosystem -> ShowS
[OsvEcosystem] -> ShowS
OsvEcosystem -> String
(Int -> OsvEcosystem -> ShowS)
-> (OsvEcosystem -> String)
-> ([OsvEcosystem] -> ShowS)
-> Show OsvEcosystem
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> OsvEcosystem -> ShowS
showsPrec :: Int -> OsvEcosystem -> ShowS
$cshow :: OsvEcosystem -> String
show :: OsvEcosystem -> String
$cshowList :: [OsvEcosystem] -> ShowS
showList :: [OsvEcosystem] -> ShowS
Show)

-- One advisory of the npm export names a few hundred ranges, and a feed no one has measured
-- borrows this bound.
npmAdvisoryFanOut :: Int
npmAdvisoryFanOut :: Int
npmAdvisoryFanOut = Int
256

-- The largest advisory of today's PyPI export names 2459 ranges, so no ordinary one trips this.
pypiAdvisoryFanOut :: Int
pypiAdvisoryFanOut :: Int
pypiAdvisoryFanOut = Int
4096

{- | An ecosystem's pair of spellings. npm agrees with osv.dev, PyPI and RubyGems do not.

>>> osvEcosystemFor PyPI
OsvEcosystem {osvExportDirectory = "PyPI", osvWireName = "pypi", osvEcosystemTag = Just PyPI, osvMaxAdvisoryFanOut = 4096}
-}
osvEcosystemFor :: Ecosystem -> OsvEcosystem
osvEcosystemFor :: Ecosystem -> OsvEcosystem
osvEcosystemFor Ecosystem
eco =
    OsvEcosystem
        { osvExportDirectory :: Text
osvExportDirectory = Text
exportDirectory
        , osvWireName :: Text
osvWireName = Ecosystem -> Text
ecosystemName Ecosystem
eco
        , osvEcosystemTag :: Maybe Ecosystem
osvEcosystemTag = Ecosystem -> Maybe Ecosystem
forall a. a -> Maybe a
Just Ecosystem
eco
        , osvMaxAdvisoryFanOut :: Int
osvMaxAdvisoryFanOut = Int
fanOut
        }
  where
    exportDirectory :: Text
exportDirectory = case Ecosystem
eco of
        Ecosystem
Npm -> Text
"npm"
        Ecosystem
PyPI -> Text
"PyPI"
        Ecosystem
RubyGems -> Text
"RubyGems"

    fanOut :: Int
fanOut = case Ecosystem
eco of
        Ecosystem
Npm -> Int
npmAdvisoryFanOut
        Ecosystem
PyPI -> Int
pypiAdvisoryFanOut
        Ecosystem
RubyGems -> Int
npmAdvisoryFanOut

{- | The pair for a name a one-shot compile was given: a name this build serves resolves through
'osvEcosystemFor', and any other spells itself on both halves.

>>> osvEcosystemNamed "pypi"
OsvEcosystem {osvExportDirectory = "PyPI", osvWireName = "pypi", osvEcosystemTag = Just PyPI, osvMaxAdvisoryFanOut = 4096}
-}
osvEcosystemNamed :: Text -> OsvEcosystem
osvEcosystemNamed :: Text -> OsvEcosystem
osvEcosystemNamed Text
name = OsvEcosystem
-> (Ecosystem -> OsvEcosystem) -> Maybe Ecosystem -> OsvEcosystem
forall b a. b -> (a -> b) -> Maybe a -> b
maybe OsvEcosystem
unserved Ecosystem -> OsvEcosystem
osvEcosystemFor (Text -> Maybe Ecosystem
parseEcosystem Text
name)
  where
    unserved :: OsvEcosystem
unserved =
        OsvEcosystem
            { osvExportDirectory :: Text
osvExportDirectory = Text
name
            , osvWireName :: Text
osvWireName = Text
name
            , osvEcosystemTag :: Maybe Ecosystem
osvEcosystemTag = Maybe Ecosystem
forall a. Maybe a
Nothing
            , osvMaxAdvisoryFanOut :: Int
osvMaxAdvisoryFanOut = Int
npmAdvisoryFanOut
            }