packages feed

hix-0.9.0: lib/Hix/Managed/Handlers/HackageClient/Prod.hs

{-# language MonadComprehensions #-}

module Hix.Managed.Handlers.HackageClient.Prod where

import Data.Aeson (Value, eitherDecodeStrict')
import Exon (exon)
import Network.HTTP.Client (
  Manager,
  Request (..),
  RequestBody (RequestBodyLBS),
  Response (..),
  applyBasicAuth,
  defaultRequest,
  httpLbs,
  setQueryString,
  )
import Network.HTTP.Client.MultipartFormData (formDataBody, partBS)
import Network.HTTP.Types (
  Status (statusCode, statusMessage),
  hAccept,
  hContentType,
  statusIsClientError,
  statusIsServerError,
  statusIsSuccessful,
  )

import qualified Hix.Color as Color
import Hix.Data.Monad (M)
import Hix.Hackage (hackagePostQuery, hackagePut)
import Hix.Http (httpManager)
import qualified Hix.Log as Log
import Hix.Managed.Cabal.Data.Config (CabalConfig, HackagePurpose, hackagesFor)
import Hix.Managed.Cabal.Data.HackageLocation (
  HackageHost (..),
  HackageLocation (..),
  HackagePassword (HackagePassword),
  HackageTls (..),
  HackageUser (..),
  hackageTlsBool,
  )
import qualified Hix.Managed.Cabal.Data.HackageRepo as HackageRepo
import Hix.Managed.Cabal.Data.HackageRepo (HackageDescription, HackageRepo (HackageRepo))
import Hix.Managed.Cabal.HackageLocation (hackagePort)
import Hix.Managed.Handlers.HackageClient (
  HackageClient (..),
  HackageError (..),
  HackageRequest (..),
  HackageResponse (..),
  )
import Hix.Maybe (fromMaybeA)
import Hix.Monad (appContextDebug)
import Hix.Network (Port)

data HackageResources =
  HackageResources {
    manager :: Manager,
    description :: HackageDescription,
    location :: HackageLocation
  }

parseResult ::
  LByteString ->
  HackageResponse a ->
  Either HackageError a
parseResult body = \case
  HackageResponseJson ->
    first wrapError (eitherDecodeStrict' (toStrict body))
  HackageResponseHtml ->
    Right (decodeUtf8 body)
  HackageNoResponse ->
    unit
  where
    wrapError err = HackageFatal [exon|Hackage response parse error: #{toText err}|]

baseRequest :: HackageLocation -> HackageRequest a -> Request
baseRequest location@HackageLocation {host = HackageHost host, tls} HackageRequest {..} =
  defaultRequest {
    host = encodeUtf8 host,
    port = fromIntegral (hackagePort location),
    secure = hackageTlsBool tls,
    method = encodeUtf8 method,
    path = encodeUtf8 path,
    requestHeaders = acceptHeader : maybeToList contentHeader
  }
  where
    acceptHeader
      | HackageResponseJson <- accept = (hAccept, "application/json")
      | otherwise = (hAccept, "text/html")

    contentHeader
      | Just (Right _) <- body = Just (hContentType, "application/json")
      | Just _ <- query = Just (hContentType, "application/x-www-form-urlencoded")
      | otherwise = Nothing

nativeRequest :: HackageLocation -> HackageRequest a -> M Request
nativeRequest location request@HackageRequest {..} = do
  addBody body (addAuth location.auth (addQuery query (baseRequest location request)))
  where
    addBody = maybe pure \case
      Right bs -> \ r -> pure r { requestBody = RequestBodyLBS bs }
      Left fields -> formDataBody [partBS key (encodeUtf8 value) | (key, value) <- toList fields]

    addAuth =
      maybe id \ (HackageUser user, HackagePassword password) ->
        applyBasicAuth (encodeUtf8 user) (encodeUtf8 password)

    addQuery = maybe id \ q -> setQueryString (second Just <$> toList q)

makeNativeRequest ::
  ∀ a .
  HackageResources ->
  HackageRequest a ->
  M (Either HackageError a)
makeNativeRequest HackageResources {manager, location} request = do
  appContextDebug [exon|sending request to Hackage at #{Color.url location.host} (#{request.path})|] do
    nrequest <- nativeRequest location request
    response <- liftIO (httpLbs nrequest manager)
    let
      status = responseStatus response

      errorStatus category = requestFailed [exon|#{category} (#{decodeUtf8 (statusMessage status)})|]

      result = if
        | statusIsSuccessful status -> parseResult (responseBody response) request.accept
        | statusCode status == 404 -> Left HackageNotFound
        | statusIsClientError status -> errorStatus "Client error"
        | statusIsServerError status -> errorStatus "Server error"
        | otherwise -> errorStatus "Weird error"

    checkResult result
    pure result
  where
    requestFailed = Left . HackageFatal

    checkResult = \case
      Left (HackageFatal msg) ->
        Log.error [exon|Hackage request for #{Color.path request.path} at #{Color.green location.host} failed: #{msg}|]
      _ -> unit

handlersProd :: HackageResources -> HackageClient
handlersProd resources =
  HackageClient {
    description = resources.description,
    request = makeNativeRequest resources
  }

handlersProdFor ::
  Maybe Manager ->
  HackagePurpose ->
  CabalConfig ->
  M (NonEmpty HackageClient)
handlersProdFor sharedManager purpose conf = do
  manager <- fromMaybeA httpManager sharedManager
  matching <- hackagesFor purpose conf
  pure (repoHandlers manager <$> matching)
  where
    repoHandlers manager HackageRepo {description, location} =
      handlersProd (HackageResources {manager, description, location})

mockUser :: NonEmpty (Text, Text)
mockUser =
  [
    ("username", "test"),
    ("password", "test"),
    ("repeat-password", "test")
  ]

handlersMock :: Manager -> Port -> M HackageClient
handlersMock manager port = do
  _ <- hackagePostQuery Right adminClient "users/" mockUser HackageNoResponse
  _ <- hackagePut @Value Right adminClient "packages/uploaders/user/test"
  pure (handlersProd userRes)
  where
    res = HackageResources {
      manager,
      description = "mock Hackage",
      location = HackageLocation {
        host = "localhost",
        tls = TlsOff,
        port = Just (fromIntegral port),
        auth = Just ("admin", "admin")
      }
    }
    userRes = res {location = res.location {auth = Just ("test", "test")}}
    adminClient = handlersProd res