{- | The registry-protocol handle: the sole interface between the proxy core and
any specific registry's wire protocol.

This is the __ecosystem (protocol) axis__ -- fetch, publish, and parse -- and
nothing more (see @docs\/architecture\/registry-model.md@ → "Registry
Abstraction"). It is a __record of functions__ (the Handle pattern): a backend's
smart constructor returns a 'RegistryClient' whose closures capture that
backend's private state (an HTTP manager). The proxy core operates only on
'Ecluse.Core.Package.PackageInfo' (the packument-level view) and
'Ecluse.Core.Package.PackageDetails' (the per-version snapshot the rules engine
evaluates); an adapter projects its wire format into those, and nothing above
the registry layer sees registry-specific structures.

Two design points are load-bearing:

* __The effectful fields return 'IO', not @App@.__ An adapter closes over its
  own state (HTTP manager, credentials) and never imports the proxy's
  @Env@\/@App@, so backends stay decoupled from the core (no import cycle) -- see
  @docs\/architecture\/technology-stack.md@ → "Key Decisions". The @parse*@
  fields are __pure__ ('Either'): parsing a fetched response is a total,
  side-effect-free projection (/parse, don't validate/).

* __'RegistryClient' deliberately carries no authentication.__ Protocol and auth
  are orthogonal axes: every managed npm registry (AWS CodeArtifact, GCP Artifact
  Registry, a self-hosted Verdaccio) speaks the same npm protocol and differs
  only in how a bearer token is minted, which lives behind the separate
  "Ecluse.Core.Credential" handle. So one npm 'RegistryClient' is reused across every
  cloud rather than near-duplicated per provider.

The abstraction is the sole interface, so a new ecosystem backend (PyPI,
RubyGems, …) is an additive constructor behind this record rather than a
structural change.
-}
module Ecluse.Core.Registry (
    -- * Protocol handle
    RegistryClient (..),

    -- * Fetch payload
    RegistryResponse (..),

    -- * Errors
    ParseError (..),
    PublishError (..),
    PublishFault (..),
    UrlFormationError (..),
    PublishRelayResponse (..),
) where

import Ecluse.Core.Package (PackageDetails, PackageInfo, PackageName)
import Ecluse.Core.Queue (MirrorArtifact)
import Ecluse.Core.Version (Version)

{- | A raw response fetched from a registry -- the unparsed bytes of a metadata
document or an artifact, as returned by 'fetchMetadata' \/ 'fetchArtifact'. It is
kept opaque-of-bytes here so the protocol\/data plane (fetch) is separate from
parsing: a @parse*@ field turns a 'RegistryResponse' into a domain type.
-}
newtype RegistryResponse = RegistryResponse
    { RegistryResponse -> ByteString
responseBody :: ByteString
    -- ^ The raw response body (a metadata JSON document, or artifact bytes).
    }
    deriving stock (RegistryResponse -> RegistryResponse -> Bool
(RegistryResponse -> RegistryResponse -> Bool)
-> (RegistryResponse -> RegistryResponse -> Bool)
-> Eq RegistryResponse
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: RegistryResponse -> RegistryResponse -> Bool
== :: RegistryResponse -> RegistryResponse -> Bool
$c/= :: RegistryResponse -> RegistryResponse -> Bool
/= :: RegistryResponse -> RegistryResponse -> Bool
Eq, Int -> RegistryResponse -> ShowS
[RegistryResponse] -> ShowS
RegistryResponse -> String
(Int -> RegistryResponse -> ShowS)
-> (RegistryResponse -> String)
-> ([RegistryResponse] -> ShowS)
-> Show RegistryResponse
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> RegistryResponse -> ShowS
showsPrec :: Int -> RegistryResponse -> ShowS
$cshow :: RegistryResponse -> String
show :: RegistryResponse -> String
$cshowList :: [RegistryResponse] -> ShowS
showList :: [RegistryResponse] -> ShowS
Show)

{- | Why parsing a 'RegistryResponse' into a domain type failed. Parsing is the
boundary that turns untrusted wire data into the proxy's precise types, so a
failure is reported (not thrown): the caller decides how to respond.
-}
newtype ParseError = ParseError
    { ParseError -> Text
parseErrorMessage :: Text
    -- ^ A human-readable description of what could not be parsed.
    }
    deriving stock (ParseError -> ParseError -> Bool
(ParseError -> ParseError -> Bool)
-> (ParseError -> ParseError -> Bool) -> Eq ParseError
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ParseError -> ParseError -> Bool
== :: ParseError -> ParseError -> Bool
$c/= :: ParseError -> ParseError -> Bool
/= :: ParseError -> ParseError -> Bool
Eq, Int -> ParseError -> ShowS
[ParseError] -> ShowS
ParseError -> String
(Int -> ParseError -> ShowS)
-> (ParseError -> String)
-> ([ParseError] -> ShowS)
-> Show ParseError
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ParseError -> ShowS
showsPrec :: Int -> ParseError -> ShowS
$cshow :: ParseError -> String
show :: ParseError -> String
$cshowList :: [ParseError] -> ShowS
showList :: [ParseError] -> ShowS
Show)

{- | Why publishing an artifact to a registry failed -- a genuine write fault
reported by 'publishArtifact' (an 'Ecluse.Core.Queue' job is then left un-acked and
retried; see @docs\/architecture\/cloud-backends.md@).

This is the __write-path__ fault and nothing more: forming the request URL is a
separate concern (a 'UrlFormationError'), so a read-path fetch can no longer
surface a failure mislabelled as a publish.
-}
newtype PublishError = PublishError
    { PublishError -> Text
publishErrorMessage :: Text
    -- ^ A human-readable description of why the publish failed.
    }
    deriving stock (PublishError -> PublishError -> Bool
(PublishError -> PublishError -> Bool)
-> (PublishError -> PublishError -> Bool) -> Eq PublishError
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: PublishError -> PublishError -> Bool
== :: PublishError -> PublishError -> Bool
$c/= :: PublishError -> PublishError -> Bool
/= :: PublishError -> PublishError -> Bool
Eq, Int -> PublishError -> ShowS
[PublishError] -> ShowS
PublishError -> String
(Int -> PublishError -> ShowS)
-> (PublishError -> String)
-> ([PublishError] -> ShowS)
-> Show PublishError
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> PublishError -> ShowS
showsPrec :: Int -> PublishError -> ShowS
$cshow :: PublishError -> String
show :: PublishError -> String
$cshowList :: [PublishError] -> ShowS
showList :: [PublishError] -> ShowS
Show)

{- | Why an upstream request URL could not be formed from configuration and an
already-parsed 'Ecluse.Core.Package.PackageName'.

This is a __protocol-independent__ fault shared by every request an adapter
builds -- metadata fetch, artifact fetch, and publish alike -- so a read-path
failure is reported as what it is rather than borrowing the write-path's
'PublishError'. It is distinct from "Ecluse.Core.Security"'s @UrlError@: that is the
pure SSRF\/identifier guard (which also rejects unsafe name components), whereas
this is the effectful adapter's report that the configured base URL is unusable.
-}
data UrlFormationError
    = -- | The configured base URL is empty, so no request URL can be formed.
      EmptyBaseUrl
    | {- | The formed URL string could not be parsed into a request. Carries the
      offending URL.
      -}
      UnparseableUrl Text
    deriving stock (UrlFormationError -> UrlFormationError -> Bool
(UrlFormationError -> UrlFormationError -> Bool)
-> (UrlFormationError -> UrlFormationError -> Bool)
-> Eq UrlFormationError
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: UrlFormationError -> UrlFormationError -> Bool
== :: UrlFormationError -> UrlFormationError -> Bool
$c/= :: UrlFormationError -> UrlFormationError -> Bool
/= :: UrlFormationError -> UrlFormationError -> Bool
Eq, Int -> UrlFormationError -> ShowS
[UrlFormationError] -> ShowS
UrlFormationError -> String
(Int -> UrlFormationError -> ShowS)
-> (UrlFormationError -> String)
-> ([UrlFormationError] -> ShowS)
-> Show UrlFormationError
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> UrlFormationError -> ShowS
showsPrec :: Int -> UrlFormationError -> ShowS
$cshow :: UrlFormationError -> String
show :: UrlFormationError -> String
$cshowList :: [UrlFormationError] -> ShowS
showList :: [UrlFormationError] -> ShowS
Show)

{- | A 'UrlFormationError' is throwable. The __read__ path (metadata\/artifact
fetch) treats an unformable URL as a configuration fault and raises it as this
typed exception -- catchable by type, never laundered into a stringly-typed
@stringException@. The __write__ path does not throw it: it surfaces it as a
'PublishFault' value instead (see below), because the mirror worker must decide
retry vs. drop on it.
-}
instance Exception UrlFormationError

{- | The response from the publication target after relaying a publish document.
Kept in memory (no streaming) -- the relayed body is small (typically a JSON
envelope and a tarball under the target's size limit), and buffering it whole lets the
proxy catch and log an exception before starting a chunked response it would otherwise
abandon mid-stream.
-}
data PublishRelayResponse = PublishRelayResponse
    { PublishRelayResponse -> Int
relayStatus :: Int
    -- ^ The HTTP status code the publication target returned.
    , PublishRelayResponse -> LByteString
relayBody :: LByteString
    -- ^ The publication target's response body, relayed to the client unchanged.
    }
    deriving stock (PublishRelayResponse -> PublishRelayResponse -> Bool
(PublishRelayResponse -> PublishRelayResponse -> Bool)
-> (PublishRelayResponse -> PublishRelayResponse -> Bool)
-> Eq PublishRelayResponse
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: PublishRelayResponse -> PublishRelayResponse -> Bool
== :: PublishRelayResponse -> PublishRelayResponse -> Bool
$c/= :: PublishRelayResponse -> PublishRelayResponse -> Bool
/= :: PublishRelayResponse -> PublishRelayResponse -> Bool
Eq, Int -> PublishRelayResponse -> ShowS
[PublishRelayResponse] -> ShowS
PublishRelayResponse -> String
(Int -> PublishRelayResponse -> ShowS)
-> (PublishRelayResponse -> String)
-> ([PublishRelayResponse] -> ShowS)
-> Show PublishRelayResponse
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> PublishRelayResponse -> ShowS
showsPrec :: Int -> PublishRelayResponse -> ShowS
$cshow :: PublishRelayResponse -> String
show :: PublishRelayResponse -> String
$cshowList :: [PublishRelayResponse] -> ShowS
showList :: [PublishRelayResponse] -> ShowS
Show)

{- | Why a publish could not complete, surfaced as a __value__ rather than thrown
so the mirror worker decides retry vs. drop by an exhaustive pattern match rather
than by catching (and re-classifying) an exception. The two cases differ in
exactly that -- retryability -- which is the whole reason this is a value: one is
worth redelivering and the other never is.
-}
data PublishFault
    = {- | The request URL could not be formed (e.g. an empty base URL) -- a
      configuration fault carried as its 'UrlFormationError'. __Not retryable__:
      redelivering the job cannot change a misconfigured base URL, so the worker
      drops (and alerts) rather than re-enqueueing forever.
      -}
      PublishUrlUnformable UrlFormationError
    | {- | The registry rejected the write (a non-2xx, non-@409@ status), carried
      as a 'PublishError'. __Retryable__: the job is left un-acked and redelivered.
      -}
      PublishRejected PublishError
    deriving stock (PublishFault -> PublishFault -> Bool
(PublishFault -> PublishFault -> Bool)
-> (PublishFault -> PublishFault -> Bool) -> Eq PublishFault
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: PublishFault -> PublishFault -> Bool
== :: PublishFault -> PublishFault -> Bool
$c/= :: PublishFault -> PublishFault -> Bool
/= :: PublishFault -> PublishFault -> Bool
Eq, Int -> PublishFault -> ShowS
[PublishFault] -> ShowS
PublishFault -> String
(Int -> PublishFault -> ShowS)
-> (PublishFault -> String)
-> ([PublishFault] -> ShowS)
-> Show PublishFault
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> PublishFault -> ShowS
showsPrec :: Int -> PublishFault -> ShowS
$cshow :: PublishFault -> String
show :: PublishFault -> String
$cshowList :: [PublishFault] -> ShowS
showList :: [PublishFault] -> ShowS
Show)

{- | The registry-protocol handle -- a record of functions over a backend whose
private state the closures capture. The effectful fields return 'IO' (decoupled
from the core); the @parse*@ fields are pure. See the module header.
-}
data RegistryClient = RegistryClient
    { RegistryClient -> PackageName -> IO RegistryResponse
fetchMetadata :: PackageName -> IO RegistryResponse
    -- ^ Fetch a package's metadata document (its packument) from the registry.
    , RegistryClient -> PackageName -> Version -> IO RegistryResponse
fetchArtifact :: PackageName -> Version -> IO RegistryResponse
    -- ^ Fetch the artifact bytes for one version.
    , RegistryClient
-> PackageName
-> Version
-> MirrorArtifact
-> ByteString
-> IO (Either PublishFault ())
publishArtifact :: PackageName -> Version -> MirrorArtifact -> ByteString -> IO (Either PublishFault ())
    {- ^ Publish one version's artifact to the registry, given its metadata
    ('MirrorArtifact': filename, integrity hashes, declared size) and the raw
    tarball bytes. The adapter is responsible for assembling the
    ecosystem-specific publish document from these inputs. Idempotent at the
    protocol level (versions are immutable), so a redelivered mirror job's
    re-publish is safe. A failure is reported as a 'PublishFault' __value__ --
    'PublishRejected' (retry) or 'PublishUrlUnformable' (drop) -- never thrown,
    so the worker's retry-vs-drop decision is total at the call site.
    -}
    , RegistryClient
-> PackageName -> RegistryResponse -> Either ParseError PackageInfo
parsePackageInfo :: PackageName -> RegistryResponse -> Either ParseError PackageInfo
    {- ^ Project a fetched metadata response into the packument-level
    'PackageInfo' for the requested package. The 'PackageName' is the identity the
    request is for -- the proxy always knows it from the route -- supplied so the
    projection has the requested identity available alongside the upstream
    document's self-reported @name@.
    -}
    , RegistryClient
-> RegistryResponse -> Version -> Either ParseError PackageDetails
parseVersionDetails :: RegistryResponse -> Version -> Either ParseError PackageDetails
    {- ^ Project a fetched metadata response into the per-version
    'PackageDetails' for a specific version.
    -}
    , RegistryClient -> RegistryResponse -> Either ParseError [Version]
parseVersionList :: RegistryResponse -> Either ParseError [Version]
    -- ^ Extract the list of available versions from a fetched metadata response.
    }