servant/servant/src/Servant/API/ResponseHeaders.hs

246 lines
9.2 KiB
Haskell
Raw Normal View History

2015-04-13 15:12:33 +02:00
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
2015-04-13 15:12:33 +02:00
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE FunctionalDependencies #-}
2015-04-13 15:12:33 +02:00
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
2015-05-02 03:21:03 +02:00
{-# LANGUAGE TypeFamilies #-}
2015-04-13 15:12:33 +02:00
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
2015-05-02 03:21:03 +02:00
{-# OPTIONS_HADDOCK not-home #-}
2015-04-13 15:12:33 +02:00
-- | This module provides facilities for adding headers to a response.
--
-- >>> let headerVal = addHeader "some-url" 5 :: Headers '[Header "Location" String] Int
--
-- The value is added to the header specified by the type (@Location@ in the
-- example above).
module Servant.API.ResponseHeaders
2015-05-02 03:21:03 +02:00
( Headers(..)
, ResponseHeader (..)
, AddHeader
, addHeader
, noHeader
2018-10-28 06:33:39 +01:00
, HasResponseHeader
, lookupResponseHeader
2015-05-02 03:21:03 +02:00
, BuildHeadersTo(buildHeadersTo)
, GetHeaders(getHeaders)
2018-10-13 22:19:51 +02:00
, GetHeaders'
2015-05-02 03:21:03 +02:00
, HeaderValMap
, HList(..)
2015-04-13 15:12:33 +02:00
) where
import Control.DeepSeq
(NFData (..))
2018-03-11 16:58:31 +01:00
import Data.ByteString.Char8 as BS
(ByteString, init, pack, unlines)
import qualified Data.CaseInsensitive as CI
2015-04-13 15:12:33 +02:00
import Data.Proxy
2018-03-11 16:58:31 +01:00
import Data.Typeable
(Typeable)
import GHC.TypeLits
(KnownSymbol, Symbol, symbolVal)
import qualified Network.HTTP.Types.Header as HTTP
import Web.HttpApiData
(FromHttpApiData, ToHttpApiData, parseHeader, toHeader)
2015-04-13 15:12:33 +02:00
2018-03-11 16:58:31 +01:00
import Prelude ()
2016-03-01 12:21:21 +01:00
import Prelude.Compat
2018-03-11 16:58:31 +01:00
import Servant.API.Header
(Header)
2015-04-13 15:12:33 +02:00
-- | Response Header objects. You should never need to construct one directly.
2017-03-19 21:49:52 +01:00
-- Instead, use 'addOptionalHeader'.
2015-04-13 15:12:33 +02:00
data Headers ls a = Headers { getResponse :: a
-- ^ The underlying value of a 'Headers'
2015-05-02 03:21:03 +02:00
, getHeadersHList :: HList ls
-- ^ HList of headers.
} deriving (Functor)
instance (NFDataHList ls, NFData a) => NFData (Headers ls a) where
rnf (Headers x hdrs) = rnf x `seq` rnf hdrs
data ResponseHeader (sym :: Symbol) a
= Header a
| MissingHeader
| UndecodableHeader ByteString
deriving (Typeable, Eq, Show, Functor)
instance NFData a => NFData (ResponseHeader sym a) where
rnf MissingHeader = ()
rnf (UndecodableHeader bs) = rnf bs
rnf (Header x) = rnf x
2015-05-02 03:21:03 +02:00
data HList a where
HNil :: HList '[]
HCons :: ResponseHeader h x -> HList xs -> HList (Header h x ': xs)
2015-05-02 03:21:03 +02:00
class NFDataHList xs where rnfHList :: HList xs -> ()
instance NFDataHList '[] where rnfHList HNil = ()
instance (y ~ Header h x, NFData x, NFDataHList xs) => NFDataHList (y ': xs) where
rnfHList (HCons h xs) = rnf h `seq` rnfHList xs
instance NFDataHList xs => NFData (HList xs) where
rnf = rnfHList
2015-05-02 03:21:03 +02:00
type family HeaderValMap (f :: * -> *) (xs :: [*]) where
HeaderValMap f '[] = '[]
HeaderValMap f (Header h x ': xs) = Header h (f x) ': HeaderValMap f xs
2015-05-02 03:21:03 +02:00
class BuildHeadersTo hs where
buildHeadersTo :: [HTTP.Header] -> HList hs
-- ^ Note: if there are multiple occurrences of a header in the argument,
2015-05-02 03:21:03 +02:00
-- the values are interspersed with commas before deserialization (see
-- <http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 RFC2616 Sec 4.2>)
2018-07-11 00:39:38 +02:00
instance {-# OVERLAPPING #-} BuildHeadersTo '[] where
2015-05-02 03:21:03 +02:00
buildHeadersTo _ = HNil
2018-07-11 00:39:38 +02:00
instance {-# OVERLAPPABLE #-} ( FromHttpApiData v, BuildHeadersTo xs, KnownSymbol h )
=> BuildHeadersTo (Header h v ': xs) where
2015-05-02 03:21:03 +02:00
buildHeadersTo headers =
let wantedHeader = CI.mk . pack $ symbolVal (Proxy :: Proxy h)
matching = snd <$> filter (\(h, _) -> h == wantedHeader) headers
in case matching of
[] -> MissingHeader `HCons` buildHeadersTo headers
xs -> case parseHeader (BS.init $ BS.unlines xs) of
Left _err -> UndecodableHeader (BS.init $ BS.unlines xs)
2015-05-02 03:21:03 +02:00
`HCons` buildHeadersTo headers
Right h -> Header h `HCons` buildHeadersTo headers
2015-05-02 03:21:03 +02:00
-- * Getting headers
2015-05-02 03:21:03 +02:00
class GetHeaders ls where
getHeaders :: ls -> [HTTP.Header]
-- | Auxiliary class for @'GetHeaders' ('HList' hs)@ instance
class GetHeadersFromHList hs where
getHeadersFromHList :: HList hs -> [HTTP.Header]
instance GetHeadersFromHList hs => GetHeaders (HList hs) where
getHeaders = getHeadersFromHList
instance GetHeadersFromHList '[] where
getHeadersFromHList _ = []
instance (KnownSymbol h, ToHttpApiData x, GetHeadersFromHList xs)
=> GetHeadersFromHList (Header h x ': xs)
where
getHeadersFromHList hdrs = case hdrs of
Header val `HCons` rest -> (headerName , toHeader val) : getHeadersFromHList rest
UndecodableHeader h `HCons` rest -> (headerName, h) : getHeadersFromHList rest
MissingHeader `HCons` rest -> getHeadersFromHList rest
where
headerName = CI.mk . pack $ symbolVal (Proxy :: Proxy h)
-- | Auxiliary class for @'GetHeaders' ('Headers' hs a)@ instance
class GetHeaders' hs where
getHeaders' :: Headers hs a -> [HTTP.Header]
instance GetHeaders' hs => GetHeaders (Headers hs a) where
getHeaders = getHeaders'
-- | This instance is an optimisation
instance GetHeaders' '[] where
getHeaders' _ = []
instance (KnownSymbol h, GetHeadersFromHList rest, ToHttpApiData v)
=> GetHeaders' (Header h v ': rest)
where
getHeaders' hs = getHeadersFromHList $ getHeadersHList hs
2015-05-02 03:21:03 +02:00
-- * Adding headers
2015-04-13 15:12:33 +02:00
-- We need all these fundeps to save type inference
class AddHeader h v orig new
| h v orig -> new, new -> h, new -> v, new -> orig where
addOptionalHeader :: ResponseHeader h v -> orig -> new -- ^ N.B.: The same header can't be added multiple times
2015-05-02 03:21:03 +02:00
-- In this instance, we add a Header on top of something that is already decorated with some headers
2018-07-11 00:39:38 +02:00
instance {-# OVERLAPPING #-} ( KnownSymbol h, ToHttpApiData v )
2015-12-27 17:54:29 +01:00
=> AddHeader h v (Headers (fst ': rest) a) (Headers (Header h v ': fst ': rest) a) where
2016-10-24 17:15:29 +02:00
addOptionalHeader hdr (Headers resp heads) = Headers resp (HCons hdr heads)
2015-04-13 15:12:33 +02:00
-- In this instance, 'a' parameter is decorated with a Header.
instance {-# OVERLAPPABLE #-} ( KnownSymbol h, ToHttpApiData v , new ~ Headers '[Header h v] a)
2015-12-27 17:54:29 +01:00
=> AddHeader h v a new where
2016-10-24 17:15:29 +02:00
addOptionalHeader hdr resp = Headers resp (HCons hdr HNil)
-- | @addHeader@ adds a header to a response. Note that it changes the type of
-- the value in the following ways:
--
2017-03-19 21:49:52 +01:00
-- 1. A simple value is wrapped in "Headers '[hdr]":
--
-- >>> let example1 = addHeader 5 "hi" :: Headers '[Header "someheader" Int] String;
-- >>> getHeaders example1
-- [("someheader","5")]
--
-- 2. A value that already has a header has its new header *prepended* to the
-- existing list:
--
-- >>> let example1 = addHeader 5 "hi" :: Headers '[Header "someheader" Int] String;
-- >>> let example2 = addHeader True example1 :: Headers '[Header "1st" Bool, Header "someheader" Int] String
-- >>> getHeaders example2
-- [("1st","true"),("someheader","5")]
--
-- Note that while in your handlers type annotations are not required, since
-- the type can be inferred from the API type, in other cases you may find
-- yourself needing to add annotations.
addHeader :: AddHeader h v orig new => v -> orig -> new
2016-10-24 17:15:29 +02:00
addHeader = addOptionalHeader . Header
-- | Deliberately do not add a header to a value.
--
-- >>> let example1 = noHeader "hi" :: Headers '[Header "someheader" Int] String
-- >>> getHeaders example1
-- []
noHeader :: AddHeader h v orig new => orig -> new
2016-10-24 17:15:29 +02:00
noHeader = addOptionalHeader MissingHeader
2015-04-13 15:12:33 +02:00
2018-10-28 06:33:39 +01:00
class HasResponseHeader h a headers where
hlistLookupHeader :: HList headers -> ResponseHeader h a
instance {-# OVERLAPPING #-} HasResponseHeader h a (Header h a ': rest) where
hlistLookupHeader (HCons ha _) = ha
instance {-# OVERLAPPABLE #-} (HasResponseHeader h a rest) => HasResponseHeader h a (first ': rest) where
hlistLookupHeader (HCons _ hs) = hlistLookupHeader hs
-- | Look up a specific ResponseHeader,
-- without having to know what position it is in the HList.
--
-- >>> let example1 = addHeader 5 "hi" :: Headers '[Header "someheader" Int] String
-- >>> let example2 = addHeader True example1 :: Headers '[Header "1st" Bool, Header "someheader" Int] String
-- >>> lookupResponseHeader example2 :: ResponseHeader "someheader" Int
-- Header 5
2018-11-08 17:36:31 +01:00
--
2018-10-28 06:33:39 +01:00
-- >>> lookupResponseHeader example2 :: ResponseHeader "1st" Bool
-- Header True
--
-- Usage of this function relies on an explicit type annotation of the header to be looked up.
-- This can be done with type annotations on the result, or with an explicit type application.
2018-11-08 17:36:31 +01:00
-- In this example, the type of header value is determined by the type-inference,
-- we only specify the name of the header:
--
-- >>> :set -XTypeApplications
-- >>> case lookupResponseHeader @"1st" example2 of { Header b -> b ; _ -> False }
-- True
--
-- @since 0.15
--
2018-10-28 06:33:39 +01:00
lookupResponseHeader :: (HasResponseHeader h a headers)
=> Headers headers r -> ResponseHeader h a
lookupResponseHeader = hlistLookupHeader . getHeadersHList
2015-04-13 15:12:33 +02:00
-- $setup
2018-11-08 17:36:31 +01:00
-- >>> :set -XFlexibleContexts
2015-04-13 15:12:33 +02:00
-- >>> import Servant.API
-- >>> import Data.Aeson
-- >>> import Data.Text
-- >>> data Book
-- >>> instance ToJSON Book where { toJSON = undefined }