{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE UndecidableInstances #-} module Linnet.Output ( Output(..) , Payload(..) , ok , created , accepted , noContent , badRequest , unauthorized , paymentRequired , forbidden , notFound , methodNotAllowed , notAcceptable , conflict , gone , lengthRequired , preconditionFailed , requestEntityTooLarge , unprocessableEntity , tooManyRequests , internalServerError , notImplemented , badGateway , serviceUnavailable , gatewayTimeout , payloadOutput , payloadError , payloadEmpty , transformM , withHeader , outputToResponse ) where import Control.Exception (Exception, SomeException, toException) import Control.Monad.Catch (MonadThrow (..)) import qualified Data.ByteString as B import qualified Data.CaseInsensitive as CI import Network.HTTP.Types (Header) import Network.HTTP.Types.Status import Network.Wai -- | Output of 'Endpoint' that carries some 'Payload' @a@ together with response status and headers data Output a = Output { outputStatus :: Status , outputPayload :: Payload a , outputHeaders :: [Header] } deriving (Eq) -- | Payload of 'Output' that could be: data Payload a = Payload a -- ^ Payload with some value @a@ | NoPayload -- ^ Represents empty response | forall e. Exception e => ErrorPayload e -- ^ Failed payload with an exception inside instance (Eq a) => Eq (Payload a) where (==) (Payload a) (Payload b) = a == b (==) NoPayload NoPayload = True (==) (ErrorPayload e) (ErrorPayload e') = show e == show e' (==) _ _ = False deriving instance (Show a) => Show (Payload a) instance (Show a) => Show (Output a) where show out = "Output(" ++ show (statusCode $ outputStatus out) ++ ", " ++ show (outputPayload out) ++ ")" instance Functor Output where fmap f (Output status (Payload a) headers) = Output status (Payload (f a)) headers fmap _ (Output status NoPayload headers) = Output status NoPayload headers fmap _ (Output status (ErrorPayload e) headers) = Output status (ErrorPayload e) headers -- This applicative isn't lawful due to the HTTP status. There is no logical way to combine two HTTP statuses instance Applicative Output where pure = ok (<*>) (Output _ (Payload f) _) (Output status (Payload a) headers) = Output {outputStatus = status, outputPayload = Payload (f a), outputHeaders = headers} (<*>) (Output status NoPayload headers) _ = Output status NoPayload headers (<*>) (Output status (ErrorPayload e) headers) _ = Output {outputStatus = status, outputPayload = ErrorPayload e, outputHeaders = headers} (<*>) _ (Output status NoPayload headers) = Output {outputStatus = status, outputPayload = NoPayload, outputHeaders = headers} (<*>) _ (Output status (ErrorPayload e) headers) = Output {outputStatus = status, outputPayload = ErrorPayload e, outputHeaders = headers} -- This monad isn't lawful due to the HTTP status. There is no logical way to combine two HTTP statuses instance Monad Output where (>>=) (Output _ (Payload a) _) f = f a (>>=) (Output status NoPayload headers) _ = Output {outputStatus = status, outputPayload = NoPayload, outputHeaders = headers} (>>=) (Output status (ErrorPayload e) headers) _ = Output {outputStatus = status, outputPayload = ErrorPayload e, outputHeaders = headers} instance MonadThrow Output where throwM = internalServerError instance Foldable Output where foldMap fn (Output _ (Payload a) _) = fn a foldMap _ _ = mempty instance Traversable Output where traverse fn (Output status (Payload a) headers) = (\v -> Output status (Payload v) headers) <$> fn a traverse _ (Output status NoPayload headers) = pure $ Output {outputStatus = status, outputPayload = NoPayload, outputHeaders = headers} traverse _ (Output status (ErrorPayload e) headers) = pure $ Output {outputStatus = status, outputPayload = ErrorPayload e, outputHeaders = headers} -- | Add header to given 'Output' withHeader :: (B.ByteString, B.ByteString) -> Output a -> Output a withHeader (k, v) (Output status payload headers) = Output {outputStatus = status, outputPayload = payload, outputHeaders = (CI.mk k, v) : headers} -- | Transform payload of output transformM :: (Applicative m) => (a -> m (Output b)) -> Output a -> m (Output b) transformM fn (Output _ (Payload a) _) = fn a transformM _ (Output status NoPayload headers) = pure $ Output {outputStatus = status, outputPayload = NoPayload, outputHeaders = headers} transformM _ (Output status (ErrorPayload e) headers) = pure $ Output {outputStatus = status, outputPayload = ErrorPayload e, outputHeaders = headers} -- | Create 'Output' with 'Payload' @a@ and status @OK 200@ ok :: a -> Output a ok = payloadOutput ok200 -- | Create 'Output' with 'Payload' @a@ and status @CREATED 201@ created :: a -> Output a created = payloadOutput created201 -- | Create 'Output' with @NoPayload@ and status @ACCEPTED 202@ accepted :: Output a accepted = payloadEmpty accepted202 -- | Create 'Output' with @NoPayload@ and status @NO CONTENT 202@ noContent :: Output a noContent = payloadEmpty noContent204 -- | Create 'Output' with @ErrorPayload e@ and status @BAD REQUEST 400@ badRequest :: (Exception e) => e -> Output a badRequest = payloadError badRequest400 -- | Create 'Output' with @ErrorPayload e@ and status @UNAUTHORIZED 401@ unauthorized :: (Exception e) => e -> Output a unauthorized = payloadError unauthorized401 -- | Create 'Output' with @ErrorPayload e@ and status @PAYMENT REQUIRED 402@ paymentRequired :: (Exception e) => e -> Output a paymentRequired = payloadError paymentRequired402 -- | Create 'Output' with @ErrorPayload e@ and status @FORBIDDEN 403@ forbidden :: (Exception e) => e -> Output a forbidden = payloadError forbidden403 -- | Create 'Output' with @ErrorPayload e@ and status @NOT FOUND 404@ notFound :: (Exception e) => e -> Output a notFound = payloadError notFound404 -- | Create 'Output' with @ErrorPayload e@ and status @METHOD NOT ALLOWED 405@ methodNotAllowed :: (Exception e) => e -> Output a methodNotAllowed = payloadError methodNotAllowed405 -- | Create 'Output' with @ErrorPayload e@ and status @NOT ACCEPTABLE 406@ notAcceptable :: (Exception e) => e -> Output a notAcceptable = payloadError notAcceptable406 -- | Create 'Output' with @ErrorPayload e@ and status @CONFLICT 409@ conflict :: (Exception e) => e -> Output a conflict = payloadError conflict409 -- | Create 'Output' with @ErrorPayload e@ and status @GONE 410@ gone :: (Exception e) => e -> Output a gone = payloadError gone410 -- | Create 'Output' with @ErrorPayload e@ and status @LENGTH REQUIRED 411@ lengthRequired :: (Exception e) => e -> Output a lengthRequired = payloadError lengthRequired411 -- | Create 'Output' with @ErrorPayload e@ and status @PRECONDITIONED FAILED 412@ preconditionFailed :: (Exception e) => e -> Output a preconditionFailed = payloadError preconditionFailed412 -- | Create 'Output' with @ErrorPayload e@ and status @REQUEST ENTITY TOO LARGE 413@ requestEntityTooLarge :: (Exception e) => e -> Output a requestEntityTooLarge = payloadError requestEntityTooLarge413 -- | Create 'Output' with @ErrorPayload e@ and status @UNPROCESSABLE ENTITY 422@ unprocessableEntity :: (Exception e) => e -> Output a unprocessableEntity = payloadError unprocessableEntity422 -- | Create 'Output' with @ErrorPayload e@ and status @TOO MANY REQUESTS 422@ tooManyRequests :: (Exception e) => e -> Output a tooManyRequests = payloadError tooManyRequests429 -- | Create 'Output' with @ErrorPayload e@ and status @INTERNAL SERVER ERROR 500@ internalServerError :: (Exception e) => e -> Output a internalServerError = payloadError internalServerError500 -- | Create 'Output' with @ErrorPayload e@ and status @NOT IMPLEMENTED 501@ notImplemented :: (Exception e) => e -> Output a notImplemented = payloadError notImplemented501 -- | Create 'Output' with @ErrorPayload e@ and status @BAD GATEWAY 502@ badGateway :: (Exception e) => e -> Output a badGateway = payloadError badGateway502 -- | Create 'Output' with @ErrorPayload e@ and status @SERVICE UNAVAILABLE 503@ serviceUnavailable :: (Exception e) => e -> Output a serviceUnavailable = payloadError serviceUnavailable503 -- | Create 'Output' with @ErrorPayload e@ and status @GATEWAY TIMEOUT 504@ gatewayTimeout :: (Exception e) => e -> Output a gatewayTimeout = payloadError gatewayTimeout504 -- | Create successful 'Output' with payload @a@ and given status payloadOutput :: Status -> a -> Output a payloadOutput status payload = Output {outputStatus = status, outputPayload = Payload payload, outputHeaders = []} -- | Create failed 'Output' with exception @e@ and given status payloadError :: (Exception e) => Status -> e -> Output a payloadError status err = Output {outputStatus = status, outputPayload = ErrorPayload err, outputHeaders = []} -- | Create empty 'Output' with given status payloadEmpty :: Status -> Output a payloadEmpty status = Output {outputStatus = status, outputPayload = NoPayload, outputHeaders = []} outputToResponse :: (Status -> [Header] -> a -> Response) -> (Status -> [Header] -> SomeException -> Response) -> (Status -> [Header] -> () -> Response) -> Output a -> Response outputToResponse tr tre tru Output {..} = case outputPayload of Payload a -> tr outputStatus outputHeaders a NoPayload -> tru outputStatus outputHeaders () ErrorPayload e -> tre outputStatus outputHeaders $ toException e