Skip to content

Commit 72f5d5c

Browse files
authored
Use full header type in response header instances (#1697)
* Use `Header'` in response headers Use `Header'` instead of `Header` in response, so it's possible to provide `Description`, for example: ``` type PaginationTotalCountHeader = Header' '[ Description "Indicates to the client total count of items in collection" , Optional , Strict ] "Total-Count" Int ``` Note: if you want to add header with description you should use `addHeader'` or `noHeader'` which accepts `Header'` with all modifiers.
1 parent 02242e9 commit 72f5d5c

File tree

10 files changed

+107
-49
lines changed

10 files changed

+107
-49
lines changed

changelog.d/full-header-type

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
synopsis: Use `Header'` in response headers.
2+
prs: #1697
3+
4+
description: {
5+
6+
Use `Header'` instead of `Header` in response, so it's possible to provide
7+
`Description`, for example:
8+
9+
```
10+
type PaginationTotalCountHeader =
11+
Header'
12+
'[ Description "Indicates to the client total count of items in collection"
13+
, Optional
14+
, Strict
15+
]
16+
"Total-Count"
17+
Int
18+
```
19+
20+
Note: if you want to add header with description you should use `addHeader'`
21+
or `noHeader'` which accepts `Header'` with all modifiers.
22+
23+
}

servant-auth/servant-auth-server/src/Servant/Auth/Server/Internal/AddSetCookie.hs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ module Servant.Auth.Server.Internal.AddSetCookie where
77

88
import Blaze.ByteString.Builder (toByteString)
99
import qualified Data.ByteString as BS
10-
import Data.Tagged (Tagged (..))
1110
import qualified Network.HTTP.Types as HTTP
1211
import Network.Wai (mapResponseHeaders)
1312
import Servant
14-
import Servant.API.UVerb.Union
1513
import Servant.API.Generic
1614
import Servant.Server.Generic
1715
import Web.Cookie
@@ -76,12 +74,12 @@ instance (orig1 ~ orig2) => AddSetCookies 'Z orig1 orig2 where
7674
instance {-# OVERLAPPABLE #-}
7775
( Functor m
7876
, AddSetCookies n (m old) (m cookied)
79-
, AddHeader "Set-Cookie" SetCookie cookied new
77+
, AddHeader mods "Set-Cookie" SetCookie cookied new
8078
) => AddSetCookies ('S n) (m old) (m new) where
8179
addSetCookies (mCookie `SetCookieCons` rest) oldVal =
8280
case mCookie of
83-
Nothing -> noHeader <$> addSetCookies rest oldVal
84-
Just cookie -> addHeader cookie <$> addSetCookies rest oldVal
81+
Nothing -> noHeader' <$> addSetCookies rest oldVal
82+
Just cookie -> addHeader' cookie <$> addSetCookies rest oldVal
8583

8684
instance {-# OVERLAPS #-}
8785
(AddSetCookies ('S n) a a', AddSetCookies ('S n) b b')

servant-auth/servant-auth-server/src/Servant/Auth/Server/Internal/Cookie.hs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import Blaze.ByteString.Builder (toByteString)
55
import Control.Monad (MonadPlus(..), guard)
66
import Control.Monad.Except
77
import Control.Monad.Reader
8-
import qualified Crypto.JOSE as Jose
9-
import qualified Crypto.JWT as Jose
108
import Data.ByteArray (constEq)
119
import qualified Data.ByteString as BS
1210
import qualified Data.ByteString.Base64 as BS64
@@ -18,11 +16,11 @@ import Data.Time.Clock (UTCTime(..), secondsToDiffTime)
1816
import Network.HTTP.Types (methodGet)
1917
import Network.HTTP.Types.Header(hCookie)
2018
import Network.Wai (Request, requestHeaders, requestMethod)
21-
import Servant (AddHeader, addHeader)
19+
import Servant (AddHeader, addHeader')
2220
import System.Entropy (getEntropy)
2321
import Web.Cookie
2422

25-
import Servant.Auth.JWT (FromJWT (decodeJWT), ToJWT)
23+
import Servant.Auth.JWT (FromJWT, ToJWT)
2624
import Servant.Auth.Server.Internal.ConfigTypes
2725
import Servant.Auth.Server.Internal.JWT (makeJWT, verifyJWT)
2826
import Servant.Auth.Server.Internal.Types
@@ -132,8 +130,8 @@ applySessionCookieSettings cookieSettings setCookie = setCookie
132130
-- provided response object with XSRF and session cookies. This should be used
133131
-- when a user successfully authenticates with credentials.
134132
acceptLogin :: ( ToJWT session
135-
, AddHeader "Set-Cookie" SetCookie response withOneCookie
136-
, AddHeader "Set-Cookie" SetCookie withOneCookie withTwoCookies )
133+
, AddHeader mods "Set-Cookie" SetCookie response withOneCookie
134+
, AddHeader mods "Set-Cookie" SetCookie withOneCookie withTwoCookies )
137135
=> CookieSettings
138136
-> JWTSettings
139137
-> session
@@ -144,20 +142,20 @@ acceptLogin cookieSettings jwtSettings session = do
144142
Nothing -> pure Nothing
145143
Just sessionCookie -> do
146144
xsrfCookie <- makeXsrfCookie cookieSettings
147-
return $ Just $ addHeader sessionCookie . addHeader xsrfCookie
145+
return $ Just $ addHeader' sessionCookie . addHeader' xsrfCookie
148146

149147
-- | Arbitrary cookie expiry time set back in history after unix time 0
150148
expireTime :: UTCTime
151149
expireTime = UTCTime (ModifiedJulianDay 50000) 0
152150

153151
-- | Adds headers to a response that clears all session cookies
154152
-- | using max-age and expires cookie attributes.
155-
clearSession :: ( AddHeader "Set-Cookie" SetCookie response withOneCookie
156-
, AddHeader "Set-Cookie" SetCookie withOneCookie withTwoCookies )
153+
clearSession :: ( AddHeader mods "Set-Cookie" SetCookie response withOneCookie
154+
, AddHeader mods "Set-Cookie" SetCookie withOneCookie withTwoCookies )
157155
=> CookieSettings
158156
-> response
159157
-> withTwoCookies
160-
clearSession cookieSettings = addHeader clearedSessionCookie . addHeader clearedXsrfCookie
158+
clearSession cookieSettings = addHeader' clearedSessionCookie . addHeader' clearedXsrfCookie
161159
where
162160
-- According to RFC6265 max-age takes precedence, but IE/Edge ignore it completely so we set both
163161
cookieSettingsExpires = cookieSettings

servant-server/test/Servant/ServerSpec.hs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ import Network.Wai.Test
5353
import Servant.API
5454
((:<|>) (..), (:>), AuthProtect, BasicAuth,
5555
BasicAuthData (BasicAuthData), Capture, Capture', CaptureAll,
56-
Delete, EmptyAPI, Fragment, Get, HasStatus (StatusOf), Header,
57-
Headers, HttpVersion, IsSecure (..), JSON, Lenient,
58-
NoContent (..), NoContentVerb, NoFraming, OctetStream, Patch,
56+
Delete, Description, EmptyAPI, Fragment, Get, HasStatus (StatusOf),
57+
Header, Header', Headers, HttpVersion, IsSecure (..), JSON, Lenient,
58+
NoContent (..), NoContentVerb, NoFraming, OctetStream, Optional, Patch,
5959
PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw, RawM,
6060
RemoteHost, ReqBody, SourceIO, StdMethod (..), Stream, Strict,
61-
UVerb, Union, Verb, WithStatus (..), addHeader)
61+
UVerb, Union, Verb, WithStatus (..), addHeader, addHeader')
6262
import Servant.Server
6363
(Context ((:.), EmptyContext), Handler, Server, ServerT, Tagged (..),
6464
emptyServer, err401, err403, err404, hoistServer, respond, serve,
@@ -121,6 +121,7 @@ type VerbApi method status
121121
:<|> "noContent" :> NoContentVerb method
122122
:<|> "header" :> Verb method status '[JSON] (Headers '[Header "H" Int] Person)
123123
:<|> "headerNC" :> Verb method status '[JSON] (Headers '[Header "H" Int] NoContent)
124+
:<|> "headerD" :> Verb method status '[JSON] (Headers '[Header' '[Description "desc", Optional, Strict] "H" Int] Person)
124125
:<|> "accept" :> ( Verb method status '[JSON] Person
125126
:<|> Verb method status '[PlainText] String
126127
)
@@ -133,6 +134,7 @@ verbSpec = describe "Servant.API.Verb" $ do
133134
:<|> return NoContent
134135
:<|> return (addHeader 5 alice)
135136
:<|> return (addHeader 10 NoContent)
137+
:<|> return (addHeader' 5 alice)
136138
:<|> (return alice :<|> return "B")
137139
:<|> return (S.source ["bytestring"])
138140

@@ -177,6 +179,10 @@ verbSpec = describe "Servant.API.Verb" $ do
177179
liftIO $ statusCode (simpleStatus response2) `shouldBe` status
178180
liftIO $ simpleHeaders response2 `shouldContain` [("H", "5")]
179181

182+
response3 <- THW.request method "/headerD" [] ""
183+
liftIO $ statusCode (simpleStatus response3) `shouldBe` status
184+
liftIO $ simpleHeaders response3 `shouldContain` [("H", "5")]
185+
180186
it "handles trailing '/' gracefully" $ do
181187
response <- THW.request method "/headerNC/" [] ""
182188
liftIO $ statusCode (simpleStatus response) `shouldBe` status

servant-swagger/src/Servant/Swagger/Internal.hs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import Network.HTTP.Media (MediaType)
3838
import Servant.API
3939
import Servant.API.Description (FoldDescription,
4040
reflectDescription)
41-
import Servant.API.Generic (ToServantApi, AsApi)
4241
import Servant.API.Modifiers (FoldRequired)
4342

4443
import Servant.Swagger.Internal.TypeLevel.API
@@ -470,10 +469,15 @@ instance (Accept c, AllAccept cs) => AllAccept (c ': cs) where
470469
class ToResponseHeader h where
471470
toResponseHeader :: Proxy h -> (HeaderName, Swagger.Header)
472471

473-
instance (KnownSymbol sym, ToParamSchema a) => ToResponseHeader (Header sym a) where
474-
toResponseHeader _ = (hname, Swagger.Header Nothing hschema)
472+
instance (KnownSymbol sym, ToParamSchema a, KnownSymbol (FoldDescription mods)) => ToResponseHeader (Header' mods sym a) where
473+
toResponseHeader _ =
474+
( hname
475+
, Swagger.Header (transDesc $ reflectDescription (Proxy :: Proxy mods)) hschema
476+
)
475477
where
476478
hname = Text.pack (symbolVal (Proxy :: Proxy sym))
479+
transDesc "" = Nothing
480+
transDesc desc = Just (Text.pack desc)
477481
hschema = toParamSchema (Proxy :: Proxy a)
478482

479483
class AllToResponseHeader hs where

servant/src/Servant/API.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ import Servant.API.ReqBody
127127
import Servant.API.ResponseHeaders
128128
(AddHeader, BuildHeadersTo (buildHeadersTo),
129129
GetHeaders (getHeaders), HList (..), HasResponseHeader,
130-
Headers (..), ResponseHeader (..), addHeader, getHeadersHList,
131-
getResponse, lookupResponseHeader, noHeader)
130+
Headers (..), ResponseHeader (..), addHeader, addHeader',
131+
getHeadersHList, getResponse, lookupResponseHeader, noHeader,
132+
noHeader')
132133
import Servant.API.Stream
133134
(FramingRender (..), FramingUnrender (..), FromSourceIO (..),
134135
NetstringFraming, NewlineFraming, NoFraming, SourceIO, Stream,

servant/src/Servant/API/ResponseHeaders.hs

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ module Servant.API.ResponseHeaders
2424
, ResponseHeader (..)
2525
, AddHeader
2626
, addHeader
27+
, addHeader'
2728
, noHeader
29+
, noHeader'
2830
, HasResponseHeader
2931
, lookupResponseHeader
3032
, BuildHeadersTo(buildHeadersTo)
@@ -37,7 +39,7 @@ module Servant.API.ResponseHeaders
3739
import Control.DeepSeq
3840
(NFData (..))
3941
import Data.ByteString.Char8 as BS
40-
(ByteString, init, pack, unlines)
42+
(ByteString, pack)
4143
import qualified Data.CaseInsensitive as CI
4244
import qualified Data.List as L
4345
import Data.Proxy
@@ -52,7 +54,9 @@ import Web.HttpApiData
5254
import Prelude ()
5355
import Prelude.Compat
5456
import Servant.API.Header
55-
(Header)
57+
(Header')
58+
import Servant.API.Modifiers
59+
(Optional, Strict)
5660
import Servant.API.UVerb.Union
5761
import qualified Data.SOP.BasicFunctors as SOP
5862
import qualified Data.SOP.NS as SOP
@@ -81,19 +85,19 @@ instance NFData a => NFData (ResponseHeader sym a) where
8185

8286
data HList a where
8387
HNil :: HList '[]
84-
HCons :: ResponseHeader h x -> HList xs -> HList (Header h x ': xs)
88+
HCons :: ResponseHeader h x -> HList xs -> HList (Header' mods h x ': xs)
8589

8690
class NFDataHList xs where rnfHList :: HList xs -> ()
8791
instance NFDataHList '[] where rnfHList HNil = ()
88-
instance (y ~ Header h x, NFData x, NFDataHList xs) => NFDataHList (y ': xs) where
92+
instance (y ~ Header' mods h x, NFData x, NFDataHList xs) => NFDataHList (y ': xs) where
8993
rnfHList (HCons h xs) = rnf h `seq` rnfHList xs
9094

9195
instance NFDataHList xs => NFData (HList xs) where
9296
rnf = rnfHList
9397

9498
type family HeaderValMap (f :: * -> *) (xs :: [*]) where
9599
HeaderValMap f '[] = '[]
96-
HeaderValMap f (Header h x ': xs) = Header h (f x) ': HeaderValMap f xs
100+
HeaderValMap f (Header' mods h x ': xs) = Header' mods h (f x) ': HeaderValMap f xs
97101

98102

99103
class BuildHeadersTo hs where
@@ -105,7 +109,7 @@ instance {-# OVERLAPPING #-} BuildHeadersTo '[] where
105109
-- The current implementation does not manipulate HTTP header field lines in any way,
106110
-- like merging field lines with the same field name in a single line.
107111
instance {-# OVERLAPPABLE #-} ( FromHttpApiData v, BuildHeadersTo xs, KnownSymbol h )
108-
=> BuildHeadersTo (Header h v ': xs) where
112+
=> BuildHeadersTo (Header' mods h v ': xs) where
109113
buildHeadersTo headers = case L.find wantedHeader headers of
110114
Nothing -> MissingHeader `HCons` buildHeadersTo headers
111115
Just header@(_, val) -> case parseHeader val of
@@ -130,7 +134,7 @@ instance GetHeadersFromHList '[] where
130134
getHeadersFromHList _ = []
131135

132136
instance (KnownSymbol h, ToHttpApiData x, GetHeadersFromHList xs)
133-
=> GetHeadersFromHList (Header h x ': xs)
137+
=> GetHeadersFromHList (Header' mods h x ': xs)
134138
where
135139
getHeadersFromHList hdrs = case hdrs of
136140
Header val `HCons` rest -> (headerName , toHeader val) : getHeadersFromHList rest
@@ -151,42 +155,42 @@ instance GetHeaders' '[] where
151155
getHeaders' _ = []
152156

153157
instance (KnownSymbol h, GetHeadersFromHList rest, ToHttpApiData v)
154-
=> GetHeaders' (Header h v ': rest)
158+
=> GetHeaders' (Header' mods h v ': rest)
155159
where
156160
getHeaders' hs = getHeadersFromHList $ getHeadersHList hs
157161

158162
-- * Adding headers
159163

160164
-- We need all these fundeps to save type inference
161-
class AddHeader h v orig new
162-
| h v orig -> new, new -> h, new -> v, new -> orig where
165+
class AddHeader (mods :: [*]) h v orig new
166+
| mods h v orig -> new, new -> mods, new -> h, new -> v, new -> orig where
163167
addOptionalHeader :: ResponseHeader h v -> orig -> new -- ^ N.B.: The same header can't be added multiple times
164168

165169
-- In this instance, we add a Header on top of something that is already decorated with some headers
166170
instance {-# OVERLAPPING #-} ( KnownSymbol h, ToHttpApiData v )
167-
=> AddHeader h v (Headers (fst ': rest) a) (Headers (Header h v ': fst ': rest) a) where
171+
=> AddHeader mods h v (Headers (fst ': rest) a) (Headers (Header' mods h v ': fst ': rest) a) where
168172
addOptionalHeader hdr (Headers resp heads) = Headers resp (HCons hdr heads)
169173

170174
-- In this instance, 'a' parameter is decorated with a Header.
171-
instance {-# OVERLAPPABLE #-} ( KnownSymbol h, ToHttpApiData v , new ~ Headers '[Header h v] a)
172-
=> AddHeader h v a new where
175+
instance {-# OVERLAPPABLE #-} ( KnownSymbol h, ToHttpApiData v , new ~ Headers '[Header' mods h v] a)
176+
=> AddHeader mods h v a new where
173177
addOptionalHeader hdr resp = Headers resp (HCons hdr HNil)
174178

175179
-- Instances to decorate all responses in a 'Union' with headers. The functional
176180
-- dependencies force us to consider singleton lists as the base case in the
177181
-- recursion (it is impossible to determine h and v otherwise from old / new
178182
-- responses if the list is empty).
179-
instance (AddHeader h v old new) => AddHeader h v (Union '[old]) (Union '[new]) where
183+
instance (AddHeader mods h v old new) => AddHeader mods h v (Union '[old]) (Union '[new]) where
180184
addOptionalHeader hdr resp =
181185
SOP.Z $ SOP.I $ addOptionalHeader hdr $ SOP.unI $ SOP.unZ $ resp
182186

183187
instance
184-
( AddHeader h v old new, AddHeader h v (Union oldrest) (Union newrest)
188+
( AddHeader mods h v old new, AddHeader mods h v (Union oldrest) (Union newrest)
185189
-- This ensures that the remainder of the response list is _not_ empty
186190
-- It is necessary to prevent the two instances for union types from
187191
-- overlapping.
188192
, oldrest ~ (a ': as), newrest ~ (b ': bs))
189-
=> AddHeader h v (Union (old ': (a ': as))) (Union (new ': (b ': bs))) where
193+
=> AddHeader mods h v (Union (old ': (a ': as))) (Union (new ': (b ': bs))) where
190194
addOptionalHeader hdr resp = case resp of
191195
SOP.Z (SOP.I rHead) -> SOP.Z $ SOP.I $ addOptionalHeader hdr rHead
192196
SOP.S rOthers -> SOP.S $ addOptionalHeader hdr rOthers
@@ -211,21 +215,29 @@ instance
211215
-- Note that while in your handlers type annotations are not required, since
212216
-- the type can be inferred from the API type, in other cases you may find
213217
-- yourself needing to add annotations.
214-
addHeader :: AddHeader h v orig new => v -> orig -> new
218+
addHeader :: AddHeader '[Optional, Strict] h v orig new => v -> orig -> new
215219
addHeader = addOptionalHeader . Header
216220

221+
-- | Same as 'addHeader' but works with `Header'`, so it's possible to use any @mods@.
222+
addHeader' :: AddHeader mods h v orig new => v -> orig -> new
223+
addHeader' = addOptionalHeader . Header
224+
217225
-- | Deliberately do not add a header to a value.
218226
--
219227
-- >>> let example1 = noHeader "hi" :: Headers '[Header "someheader" Int] String
220228
-- >>> getHeaders example1
221229
-- []
222-
noHeader :: AddHeader h v orig new => orig -> new
230+
noHeader :: AddHeader '[Optional, Strict] h v orig new => orig -> new
223231
noHeader = addOptionalHeader MissingHeader
224232

233+
-- | Same as 'noHeader' but works with `Header'`, so it's possible to use any @mods@.
234+
noHeader' :: AddHeader mods h v orig new => orig -> new
235+
noHeader' = addOptionalHeader MissingHeader
236+
225237
class HasResponseHeader h a headers where
226238
hlistLookupHeader :: HList headers -> ResponseHeader h a
227239

228-
instance {-# OVERLAPPING #-} HasResponseHeader h a (Header h a ': rest) where
240+
instance {-# OVERLAPPING #-} HasResponseHeader h a (Header' mods h a ': rest) where
229241
hlistLookupHeader (HCons ha _) = ha
230242

231243
instance {-# OVERLAPPABLE #-} (HasResponseHeader h a rest) => HasResponseHeader h a (first ': rest) where

servant/src/Servant/API/TypeLevel.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import Servant.API.Capture
5959
(Capture, CaptureAll)
6060
import Servant.API.Fragment
6161
import Servant.API.Header
62-
(Header)
62+
(Header, Header')
6363
import Servant.API.QueryParam
6464
(QueryFlag, QueryParam, QueryParams)
6565
import Servant.API.ReqBody
@@ -130,6 +130,7 @@ type family IsElem endpoint api :: Constraint where
130130
IsElem e (sa :<|> sb) = Or (IsElem e sa) (IsElem e sb)
131131
IsElem (e :> sa) (e :> sb) = IsElem sa sb
132132
IsElem sa (Header sym x :> sb) = IsElem sa sb
133+
IsElem sa (Header' mods sym x :> sb) = IsElem sa sb
133134
IsElem sa (ReqBody y x :> sb) = IsElem sa sb
134135
IsElem (CaptureAll z y :> sa) (CaptureAll x y :> sb)
135136
= IsElem sa sb

servant/test/Servant/API/ResponseHeadersSpec.hs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import GHC.TypeLits
77
import Test.Hspec
88

99
import Servant.API.ContentTypes
10+
import Servant.API.Description
11+
(Description)
1012
import Servant.API.Header
13+
import Servant.API.Modifiers
14+
(Optional, Strict)
1115
import Servant.API.ResponseHeaders
1216
import Servant.API.UVerb
1317

@@ -27,6 +31,10 @@ spec = describe "Servant.API.ResponseHeaders" $ do
2731
let val = addHeader 10 $ addHeader "b" 5 :: Headers '[Header "first" Int, Header "second" String] Int
2832
getHeaders val `shouldBe` [("first", "10"), ("second", "b")]
2933

34+
it "adds a header with description to a value" $ do
35+
let val = addHeader' "hi" 5 :: Headers '[Header' '[Description "desc", Optional, Strict] "test" String] Int
36+
getHeaders val `shouldBe` [("test", "hi")]
37+
3038
describe "noHeader" $ do
3139

3240
it "does not add a header" $ do

0 commit comments

Comments
 (0)