-- | This module provides a 'HasResponse' class that Interprets -- 'Verb' content into a particular api endpoint query result. -- -- For instance @Verb GET '[]@ gets interpreted as an empty response of type Unit i.e @()@ -- module Hreq.Core.Client.HasResponse where import Control.Monad.Except import Data.Kind import Data.Hlist import Data.Proxy import Data.Singletons import qualified Data.List.NonEmpty as NE import GHC.TypeLits import Network.HTTP.Types (hContentType) import Hreq.Core.API import Hreq.Core.Client.ClientError import Hreq.Core.Client.Response class MonadError ClientError m => HasResponse (a :: k) m where type HttpOutput a :: Type httpRes :: sing a -> Response -> m (HttpOutput a) instance ( UniqMembers rs "Response" , HasResponse rs m ) => HasResponse (Verb method rs ) m where type HttpOutput (Verb method rs) = HttpOutput rs httpRes _ = httpRes (Proxy @rs) instance (MonadError ClientError m) => HasResponse '[] m where type HttpOutput '[] = () httpRes _ _ = return () instance {-# OVERLAPPING #-} (MonadError ClientError m) => HasResponse '[ 'Raw a ] m where type HttpOutput '[ 'Raw a ] = Response httpRes _ = return instance MonadError ClientError m => HasResponse ('ResStream ': rs) m where type HttpOutput ('ResStream ': rs) = TypeError ('Text "ResStream shouldn't be an instance of HasResponse class") httpRes _ _ = error "GHC Error" instance {-# OVERLAPPING #-} MonadError ClientError m => HasResponse ('Raw a : r : rs) m where type HttpOutput ('Raw a : r : rs) = TypeError ('Text "Raw should be used only in a singleton list") httpRes _ _ = error "GHC Error" -- | Expected status code much match received code instance {-# OVERLAPPING #-} ( MonadError ClientError m , KnownNat n ) => HasResponse '[ ResStatus n ] m where type HttpOutput '[ ResStatus n ] = Response httpRes _ res = do let expectedCode = fromIntegral @Integer @Int $ natVal (Proxy @n) rcode = resStatusCode res when (expectedCode /= rcode) $ throwError (InvalidStatusCode res) return res -- | Expected status code much match received code in a response code list instance {-# OVERLAPPING #-} ( MonadError ClientError m , KnownNat n , SingI ('Res (r ': rs)) , HttpResConstraints (r ': rs) ) => HasResponse (ResStatus n : r : rs) m where type HttpOutput (ResStatus n : r : rs) = Hlist (HttpRes (r ': rs)) httpRes _ response = do let expectedCode = fromIntegral @Integer @Int $ natVal (Proxy @n) rcode = resStatusCode response when (expectedCode /= rcode) $ throwError (InvalidStatusCode response) case sing @('Res (r ': rs)) of SRes xs -> decodeAsHlist xs response instance {-# OVERLAPPING #-} ( MediaDecode ctyp a , MonadError ClientError m ) => HasResponse '[ 'ResBody ctyp a ] m where type HttpOutput '[ 'ResBody ctyp a ] = a httpRes _ = decodeAsBody (Proxy @ctyp) -- | The following type instance is overly restrictive to avoid -- overlapping type family instance error. instance {-# OVERLAPPING #-} ( MediaDecode ctyp a , MonadError ClientError m , SingI ('Res (r ': rs)) , HttpResConstraints (r ': rs) ) => HasResponse ( 'ResBody ctyp a ': r ': rs ) m where type HttpOutput ( 'ResBody ctyp a ': r ': rs) = Hlist ( a ': HttpRes (r ': rs)) httpRes _ response = case sing @('Res (r ': rs)) of SRes xs -> do body <- decodeAsBody (Proxy @ctyp) response reset <- decodeAsHlist xs response return $ body :. reset instance {-# OVERLAPPING #-} ( MonadError ClientError m , SingI ('Res ('ResHeaders hs ': rs)) , HttpResConstraints ('ResHeaders hs ': rs) ) => HasResponse ('ResHeaders hs ': rs) m where type HttpOutput ('ResHeaders hs : rs) = Hlist (HttpRes ('ResHeaders hs ': rs)) httpRes _ response = case sing @('Res ('ResHeaders hs ': rs)) of SRes xs -> decodeAsHlist xs response decodeAsBody :: forall ctyp a m sing . (MonadError ClientError m, MediaDecode ctyp a) => sing ctyp -> Response -> m a decodeAsBody _ response = do responseContentType <- checkContentType unless (any (responseContentType `matches`) accepts) . throwError $ UnsupportedContentType (NE.head accepts) response case mediaDecode ctypProxy (resBody response) of Left err -> throwError $ DecodeFailure (unDecodeError err) response Right val -> pure val where ctypProxy :: Proxy ctyp ctypProxy = Proxy accepts :: NE.NonEmpty MediaType accepts = mediaTypes ctypProxy checkContentType :: m MediaType checkContentType = case lookup hContentType $ resHeaders response of Nothing -> return $ mediaType (Proxy @PlainText) -- fall back to plain text Just t -> maybe (throwError $ InvalidContentTypeHeader response) return $ parseAccept t -- | Turn a Response into a 'Hlist' of outputs decodeAsHlist :: (MonadError ClientError m, HttpResConstraints rs) => Sing rs -> Response -> m (Hlist (HttpRes rs)) decodeAsHlist srs response = case srs of SNil -> return Nil SCons (SResBody ctyp _a) xs -> do body <- decodeAsBody ctyp response rest <- decodeAsHlist xs response return $ body :. rest SCons (SResHeaders (SCons _h _hs)) xs -> do let headers = resHeaders response rest <- decodeAsHlist xs response return $ headers :. rest SCons (SResHeaders SNil) xs -> decodeAsHlist xs response SCons (SResStatus _ snat) xs -> do let rcode = resStatusCode response expectedCode = withKnownNat snat (fromIntegral @Integer @Int $ natVal snat) when (rcode /= expectedCode) $ throwError (InvalidStatusCode response) decodeAsHlist xs response -- Should never match because we have a class instance -- that triggers a type error when 'Raw' is in a non-singleton -- type level list (SCons (SRaw _) _xs)-> error "GHC Error" -- Should never match because we have a class instance -- that triggers a type error when 'ResStream' is in a instance of -- HasResponse class (SCons (SResStream _ _) _xs)-> error "GHC Error"