packages feed

headroom-0.4.2.0: src/Headroom/IO/Network.hs

{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TypeApplications  #-}
{-# LANGUAGE TypeFamilies      #-}

{-|
Module      : Headroom.IO.Network
Description : Network related IO operations
Copyright   : (c) 2019-2021 Vaclav Svejcar
License     : BSD-3-Clause
Maintainer  : vaclav.svejcar@gmail.com
Stability   : experimental
Portability : POSIX

Module providing support to perform selected network IO operations, such as
downloading file content, etc.
-}

module Headroom.IO.Network
  ( -- * Type Aliases
    DownloadContentFn
    -- * Polymorphic Record
  , Network(..)
  , mkNetwork
    -- * Network IO operations
  , downloadContent
  )
where

import           Data.String.Interpolate             ( i )
import           Headroom.Types                      ( fromHeadroomError
                                                     , toHeadroomError
                                                     )
import qualified Network.HTTP.Client                as HC
import           Network.HTTP.Req                    ( BsResponse
                                                     , GET(GET)
                                                     , HttpException(..)
                                                     , MonadHttp
                                                     , NoReqBody(NoReqBody)
                                                     , bsResponse
                                                     , defaultHttpConfig
                                                     , req
                                                     , responseBody
                                                     , runReq
                                                     , useURI
                                                     )
import qualified Network.HTTP.Types.Status          as HC
import           RIO
import qualified RIO.Text                           as T
import           Text.URI                            ( URI )
import qualified Text.URI                           as URI


--------------------------------  TYPE ALIASES  --------------------------------

-- | Type of a function that returns content of remote resource.
type DownloadContentFn m
  =  URI    -- ^ /URI/ of remote resource
  -> m Text -- ^ downloaded content


-----------------------------  POLYMORPHIC RECORD  -----------------------------

-- | Polymorphic record of functions performing network IO operations.
data Network m = Network
  { nDownloadContent :: DownloadContentFn m -- ^ downloads remote content
  }


-- | Constructs new 'Network' that performs real network /IO/ operations.
mkNetwork :: MonadIO m => Network m
mkNetwork = Network { nDownloadContent = downloadContent }


------------------------------  PUBLIC FUNCTIONS  ------------------------------

-- | Downloads content of remote resource as 'Text'. Note that only @http@ and
-- @https@ is supported at this moment.
downloadContent :: MonadIO m
                => URI    -- ^ /URI/ of remote resource
                -> m Text -- ^ downloaded content
downloadContent uri = runReq defaultHttpConfig $ do
  response <- httpGet uri
  case T.decodeUtf8' $ responseBody response of
    Left  err  -> throwM $ InvalidResponse uri (T.pack $ displayException err)
    Right body -> pure body


------------------------------  PRIVATE FUNCTIONS  -----------------------------

httpGet :: (MonadHttp m, MonadThrow m, MonadUnliftIO m) => URI -> m BsResponse
httpGet uri = do
  urlE      <- maybe (throwM $ InvalidURL uri) pure (useURI uri)
  eitherRes <- case urlE of
    Left  url -> doGet $ fst url
    Right url -> doGet $ fst url
  case eitherRes of
    Left  err -> handleHttpException uri err
    Right res -> pure res
 where
  doGet = \u -> try @_ @HttpException $ req GET u NoReqBody bsResponse mempty


handleHttpException :: MonadThrow m => URI -> HttpException -> m BsResponse
handleHttpException uri ex = case ex of
  VanillaHttpException (HC.HttpExceptionRequest _ c) -> case c of
    HC.ConnectionFailure ex' ->
      throwM $ ConnectionFailure uri (T.pack $ displayException ex')
    HC.StatusCodeException response _ ->
      let code    = HC.statusCode . HC.responseStatus $ response
          message = HC.statusMessage . HC.responseStatus $ response
      in  throwM $ InvalidStatus uri code (decodeUtf8Lenient message)
    _ -> throwM ex
  _ -> throwM ex

---------------------------------  ERROR TYPES  --------------------------------

-- | Error related to network operations.
data NetworkError
  = ConnectionFailure URI Text -- ^ connection failure
  | InvalidResponse URI Text        -- ^ error during obtaining response
  | InvalidStatus URI Int Text      -- ^ invalid response status
  | InvalidURL URI                  -- ^ given /URI/ is not valid
  deriving (Eq, Show)


instance Exception NetworkError where
  displayException = displayException'
  toException      = toHeadroomError
  fromException    = fromHeadroomError


displayException' :: NetworkError -> String
displayException' = \case
  ConnectionFailure uri ex -> [i|Error connecting to #{URI.render uri}: #{ex}|]
  InvalidResponse uri reason ->
    [i|Cannot decode response for '#{URI.render uri}': #{reason}|]
  InvalidStatus uri status message ->
    [i|Error downloading #{URI.render uri}: #{status} #{message}|]
  InvalidURL uri -> [i|Cannot build URL from input URI: #{URI.render uri}|]