@@ -1,24 +1,22 @@
1−{-# LANGUAGE AllowAmbiguousTypes #-}
2−{-# LANGUAGE CPP #-}
3−{-# LANGUAGE DataKinds #-}
4−{-# LANGUAGE FlexibleContexts #-}
5−{-# LANGUAGE FlexibleInstances #-}
6−{-# LANGUAGE InstanceSigs #-}
7−{-# LANGUAGE OverloadedStrings #-}
8−{-# LANGUAGE PolyKinds #-}
9−{-# LANGUAGE ScopedTypeVariables #-}
10−{-# LANGUAGE TupleSections #-}
11−{-# LANGUAGE TypeFamilies #-}
12−{-# LANGUAGE TypeOperators #-}
13−{-# LANGUAGE UndecidableInstances #-}
14−{-# LANGUAGE MultiParamTypeClasses #-}
15−{-# LANGUAGE OverloadedStrings #-}
16−{-# LANGUAGE PolyKinds #-}
17−{-# LANGUAGE ScopedTypeVariables #-}
18−{-# LANGUAGE TupleSections #-}
19−{-# LANGUAGE TypeFamilies #-}
20−{-# LANGUAGE TypeOperators #-}
21−{-# LANGUAGE UndecidableInstances #-}
1+{-# LANGUAGE AllowAmbiguousTypes #-}
2+{-# LANGUAGE CPP #-}
3+{-# LANGUAGE DataKinds #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
7+{-# LANGUAGE InstanceSigs #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE OverloadedStrings #-}
10+{-# LANGUAGE PolyKinds #-}
11+{-# LANGUAGE ScopedTypeVariables #-}
12+{-# LANGUAGE TupleSections #-}
13+{-# LANGUAGE TypeFamilies #-}
14+{-# LANGUAGE TypeOperators #-}
15+{-# LANGUAGE UndecidableInstances #-}
16+
17+#if MIN_VERSION_base(4,9,0)
18+{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
19+#endif
2220
2321-- #include "overlapping-compat.h"
2422-- | This module provides 'client' which can automatically generate
@@ -26,9 +24,13 @@
2624-- API.
2725module Servant.Reflex
2826 ( client
27+ , clientWithOpts
28+ , clientWithRoute
2929 , BuildHeaderKeysTo(..)
3030 , toHeaders
31− , HasClient(..)
31+ , HasClient
32+ , Client
33+ , withCredentials
3234 , module Servant.Common.Req
3335 , module Servant.Common.BaseUrl
3436 ) where
@@ -44,6 +46,7 @@ import Data.Proxy (Proxy (..))
4446import qualified Data.Map as Map
4547import Data.Text (Text)
4648import qualified Data.Text as T
49+import GHC.Exts (Constraint)
4750import GHC.TypeLits (KnownSymbol, symbolVal)
4851import Servant.API ((:<|>)(..),(:>), BasicAuth,
4952 BasicAuthData, BuildHeadersTo(..),
@@ -54,6 +57,7 @@ import Servant.API ((:<|>)(..),(:>), BasicAuth,
5457 QueryParams, Raw, ReflectMethod(..),
5558 RemoteHost, ReqBody,
5659 ToHttpApiData(..), Vault, Verb)
60+import qualified Servant.Auth as Auth
5761
5862import Reflex.Dom (Dynamic, Event, Reflex,
5963 XhrRequest(..),
@@ -65,8 +69,9 @@ import Reflex.Dom (Dynamic, Event, Reflex,
6569import Servant.Common.BaseUrl (BaseUrl(..), Scheme(..), baseUrlWidget,
6670 showBaseUrl,
6771 SupportsServantReflex)
68−import Servant.Common.Req (Req, ReqResult(..), QParam(..),
69− MimeUnrender,
72+import Servant.Common.Req (ClientOptions(..), MimeUnrender,
73+ defaultClientOptions,
74+ Req, ReqResult(..), QParam(..),
7075 QueryPart(..), addHeader, authData,
7176 defReq, evalResponse, prependToPathParts,
7277 -- performRequestCT,
@@ -76,9 +81,10 @@ import Servant.Common.Req (Req, ReqResult(..), QParam(..),
7681 performSomeRequestsAsync,
7782 qParamToQueryPart, reqBody,
7883 reqSuccess, reqFailure,
79− reqMethod, respHeaders, response,
84+ reqMethod, respHeaders,
85+ response,
8086 reqTag,
81− qParams)
87+ qParams, withCredentials)
8288
8389
8490-- * Accessing APIs as a Client
@@ -96,9 +102,24 @@ import Servant.Common.Req (Req, ReqResult(..), QParam(..),
96102-- -> m (Event t (l, ReqResult Book)))
97103-- > (getAllBooks :<|> postNewBook) = client myApi host
98104-- > where host = constDyn $ BaseUrl Http "localhost" 8080
99−client :: (HasClient t m layout tag)
100− => Proxy layout -> Proxy m -> Proxy tag -> Dynamic t BaseUrl -> Client t m layout tag
101−client p q t baseurl = clientWithRoute p q t defReq baseurl
105+client
106+ :: (HasClient t m layout tag)
107+ => Proxy layout
108+ -> Proxy m
109+ -> Proxy tag
110+ -> Dynamic t BaseUrl
111+ -> Client t m layout tag
112+client p q t baseurl = clientWithRoute p q t defReq baseurl defaultClientOptions
113+
114+clientWithOpts
115+ :: (HasClient t m layout tag)
116+ => Proxy layout
117+ -> Proxy m
118+ -> Proxy tag
119+ -> Dynamic t BaseUrl
120+ -> ClientOptions
121+ -> Client t m layout tag
122+clientWithOpts p q t baseurl = clientWithRoute p q t defReq baseurl
102123
103124
104125-- | This class lets us define how each API combinator
@@ -106,15 +127,15 @@ client p q t baseurl = clientWithRoute p q t defReq baseurl
106127-- an internal class, you can just use 'client'.
107128class HasClient t m layout (tag :: *) where
108129 type Client t m layout tag :: *
109− clientWithRoute :: Proxy layout -> Proxy m -> Proxy tag -> Req t -> Dynamic t BaseUrl -> Client t m layout tag
130+ clientWithRoute :: Proxy layout -> Proxy m -> Proxy tag -> Req t -> Dynamic t BaseUrl -> ClientOptions -> Client t m layout tag
110131
111132
112133instance (HasClient t m a tag, HasClient t m b tag) => HasClient t m (a :<|> b) tag where
113134 type Client t m (a :<|> b) tag = Client t m a tag :<|> Client t m b tag
114135
115− clientWithRoute Proxy q pTag req baseurl =
116− clientWithRoute (Proxy :: Proxy a) q pTag req baseurl :<|>
117− clientWithRoute (Proxy :: Proxy b) q pTag req baseurl
136+ clientWithRoute Proxy q pTag req baseurl opts =
137+ clientWithRoute (Proxy :: Proxy a) q pTag req baseurl opts :<|>
138+ clientWithRoute (Proxy :: Proxy b) q pTag req baseurl opts
118139
119140
120141-- Capture. Example:
@@ -135,11 +156,11 @@ instance (SupportsServantReflex t m, ToHttpApiData a, HasClient t m sublayout ta
135156 type Client t m (Capture capture a :> sublayout) tag =
136157 Dynamic t (Either Text a) -> Client t m sublayout tag
137158
138− clientWithRoute Proxy q t req baseurl val =
159+ clientWithRoute Proxy q t req baseurl opts val =
139160 clientWithRoute (Proxy :: Proxy sublayout)
140161 q t
141162 (prependToPathParts p req)
142− baseurl
163+ baseurl opts
143164 where p = (fmap . fmap) (toUrlPiece) val
144165
145166
@@ -152,9 +173,8 @@ instance {-# OVERLAPPABLE #-}
152173 Event t tag -> m (Event t (ReqResult tag a))
153174 -- TODO how to access input types here?
154175 -- ExceptT ServantError IO a
155− clientWithRoute Proxy _ _ req baseurl trigs =
156− -- performRequestCT (Proxy :: Proxy ct) method req' baseurl trigs
157− fmap runIdentity <$> performRequestsCT (Proxy :: Proxy ct) method (constDyn $ Identity $ req') baseurl trigs
176+ clientWithRoute Proxy _ _ req baseurl opts trigs =
177+ fmap runIdentity <$> performRequestsCT (Proxy :: Proxy ct) method (constDyn $ Identity $ req') baseurl opts trigs
158178 where method = E.decodeUtf8 $ reflectMethod (Proxy :: Proxy method)
159179 req' = req { reqMethod = method }
160180
@@ -167,8 +187,8 @@ instance {-# OVERLAPPING #-}
167187 Event t tag -> m (Event t (ReqResult tag NoContent))
168188 -- TODO: how to access input types here?
169189 -- ExceptT ServantError IO NoContent
170− clientWithRoute Proxy _ _ req baseurl =
171− (fmap . fmap) runIdentity . performRequestsNoBody method (constDyn $ Identity req) baseurl
190+ clientWithRoute Proxy _ _ req baseurl opts =
191+ (fmap . fmap) runIdentity . performRequestsNoBody method (constDyn $ Identity req) baseurl opts
172192 where method = E.decodeUtf8 $ reflectMethod (Proxy :: Proxy method)
173193
174194
@@ -204,9 +224,9 @@ instance {-# OVERLAPPABLE #-}
204224 ) => HasClient t m (Verb method status cts' (Headers ls a)) tag where
205225 type Client t m (Verb method status cts' (Headers ls a)) tag =
206226 Event t tag -> m (Event t (ReqResult tag (Headers ls a)))
207− clientWithRoute Proxy _ _ req baseurl trigs = do
227+ clientWithRoute Proxy _ _ req baseurl opts trigs = do
208228 let method = E.decodeUtf8 $ reflectMethod (Proxy :: Proxy method)
209− resp <- fmap runIdentity <$> performRequestsCT (Proxy :: Proxy ct) method (constDyn $ Identity req') baseurl trigs
229+ resp <- fmap runIdentity <$> performRequestsCT (Proxy :: Proxy ct) method (constDyn $ Identity req') baseurl opts trigs
210230 return $ toHeaders <$> resp
211231 where req' = req { respHeaders =
212232 OnlyHeaders (Set.fromList (buildHeaderKeysTo (Proxy :: Proxy ls)))
@@ -220,9 +240,9 @@ instance {-# OVERLAPPABLE #-}
220240 ) => HasClient t m (Verb method status cts (Headers ls NoContent)) tag where
221241 type Client t m (Verb method status cts (Headers ls NoContent)) tag
222242 = Event t tag -> m (Event t (ReqResult tag (Headers ls NoContent)))
223− clientWithRoute Proxy _ _ req baseurl trigs = do
243+ clientWithRoute Proxy _ _ req baseurl opts trigs = do
224244 let method = E.decodeUtf8 $ reflectMethod (Proxy :: Proxy method)
225− resp <- fmap runIdentity <$> performRequestsNoBody method (constDyn $ Identity req') baseurl trigs
245+ resp <- fmap runIdentity <$> performRequestsNoBody method (constDyn $ Identity req') baseurl opts trigs
226246 return $ toHeaders <$> resp
227247 where req' = req {respHeaders =
228248 OnlyHeaders (Set.fromList (buildHeaderKeysTo (Proxy :: Proxy ls)))
@@ -250,11 +270,11 @@ instance (KnownSymbol sym, ToHttpApiData a,
250270 type Client t m (Header sym a :> sublayout) tag =
251271 Dynamic t (Either Text a) -> Client t m sublayout tag
252272
253− clientWithRoute Proxy q t req baseurl eVal =
273+ clientWithRoute Proxy q t req baseurl opts eVal =
254274 clientWithRoute (Proxy :: Proxy sublayout)
255275 q t
256276 (Servant.Common.Req.addHeader hname eVal req)
257− baseurl
277+ baseurl opts
258278 where hname = T.pack $ symbolVal (Proxy :: Proxy sym)
259279
260280
@@ -305,9 +325,9 @@ instance (KnownSymbol sym, ToHttpApiData a, HasClient t m sublayout tag, Reflex
305325 Dynamic t (QParam a) -> Client t m sublayout tag
306326
307327 -- if mparam = Nothing, we don't add it to the query string
308− clientWithRoute Proxy q t req baseurl mparam =
328+ clientWithRoute Proxy q t req baseurl opts mparam =
309329 clientWithRoute (Proxy :: Proxy sublayout) q t
310− (req {qParams = paramPair : qParams req}) baseurl
330+ (req {qParams = paramPair : qParams req}) baseurl opts
311331
312332 where pname = symbolVal (Proxy :: Proxy sym)
313333 --p prm = QueryPartParam $ (fmap . fmap) (toQueryParam) prm
@@ -351,8 +371,8 @@ instance (KnownSymbol sym, ToHttpApiData a, HasClient t m sublayout tag, Reflex
351371 type Client t m (QueryParams sym a :> sublayout) tag =
352372 Dynamic t [a] -> Client t m sublayout tag
353373
354− clientWithRoute Proxy q t req baseurl paramlist =
355− clientWithRoute (Proxy :: Proxy sublayout) q t req' baseurl
374+ clientWithRoute Proxy q t req baseurl opts paramlist =
375+ clientWithRoute (Proxy :: Proxy sublayout) q t req' baseurl opts
356376
357377 where req' = req { qParams = (T.pack pname, params') : qParams req }
358378 pname = symbolVal (Proxy :: Proxy sym)
@@ -391,8 +411,8 @@ instance (KnownSymbol sym, HasClient t m sublayout tag, Reflex t)
391411 type Client t m (QueryFlag sym :> sublayout) tag =
392412 Dynamic t Bool -> Client t m sublayout tag
393413
394− clientWithRoute Proxy q t req baseurl flag =
395− clientWithRoute (Proxy :: Proxy sublayout) q t req' baseurl
414+ clientWithRoute Proxy q t req baseurl opts flag =
415+ clientWithRoute (Proxy :: Proxy sublayout) q t req' baseurl opts
396416
397417 where req' = req { qParams = thisPair : qParams req }
398418 thisPair = (T.pack pName, QueryPartFlag flag) :: (Text, QueryPart t)
@@ -406,7 +426,7 @@ instance SupportsServantReflex t m => HasClient t m Raw tag where
406426 -> Event t tag
407427 -> m (Event t (ReqResult tag ()))
408428
409− clientWithRoute _ _ _ _ baseurl xhrs triggers = do
429+ clientWithRoute _ _ _ _ baseurl _ xhrs triggers = do
410430
411431 let xhrs' = liftA2 (\x path -> case x of
412432 Left e -> Left e
@@ -448,8 +468,8 @@ instance (MimeRender ct a, HasClient t m sublayout tag, Reflex t)
448468 type Client t m (ReqBody (ct ': cts) a :> sublayout) tag =
449469 Dynamic t (Either Text a) -> Client t m sublayout tag
450470
451− clientWithRoute Proxy q t req baseurl body =
452− clientWithRoute (Proxy :: Proxy sublayout) q t req' baseurl
471+ clientWithRoute Proxy q t req baseurl opts body =
472+ clientWithRoute (Proxy :: Proxy sublayout) q t req' baseurl opts
453473 where req' = req { reqBody = bodyBytesCT }
454474 ctProxy = Proxy :: Proxy ct
455475 ctString = T.pack $ show $ contentType ctProxy
@@ -463,9 +483,9 @@ instance (MimeRender ct a, HasClient t m sublayout tag, Reflex t)
463483instance (KnownSymbol path, HasClient t m sublayout tag, Reflex t) => HasClient t m (path :> sublayout) tag where
464484 type Client t m (path :> sublayout) tag = Client t m sublayout tag
465485
466− clientWithRoute Proxy q t req baseurl =
486+ clientWithRoute Proxy q t req baseurl opts =
467487 clientWithRoute (Proxy :: Proxy sublayout) q t
468− (prependToPathParts (pure (Right $ T.pack p)) req) baseurl
488+ (prependToPathParts (pure (Right $ T.pack p)) req) baseurl opts
469489
470490 where p = symbolVal (Proxy :: Proxy path)
471491
@@ -498,8 +518,8 @@ instance (HasClient t m api tag, Reflex t)
498518 type Client t m (BasicAuth realm usr :> api) tag = Dynamic t (Maybe BasicAuthData)
499519 -> Client t m api tag
500520
501− clientWithRoute Proxy q t req baseurl authdata =
502− clientWithRoute (Proxy :: Proxy api) q t req' baseurl
521+ clientWithRoute Proxy q t req baseurl opts authdata =
522+ clientWithRoute (Proxy :: Proxy api) q t req' baseurl opts
503523 where
504524 req' = req { authData = Just authdata }
505525
@@ -525,3 +545,30 @@ non-empty lists, but is otherwise more specific, no instance will be overall
525545more specific. This in turn generally means adding yet another instance (one
526546for empty and one for non-empty lists).
527547-}
548+
549+
550+-- SUPPORT FOR servant-auth --
551+
552+-- For JavaScript clients we should be sending/storing JSON web tokens in a
553+-- way that is inaccessible to JavaScript.
554+--
555+-- For @servant-auth@ this is done with HTTP-only cookies. In a Reflex-DOM
556+-- app this means the @servant-auth@ client should only verify that the API
557+-- supports Cookie-based authentication but do nothing with the token
558+-- directly.
559+
560+-- @HasCookieAuth auths@ is nominally a redundant constraint, but ensures
561+-- we're not trying to rely on cookies when the API does not use them.
562+instance (HasCookieAuth auths, HasClient t m api tag) => HasClient t m (Auth.Auth auths a :> api) tag where
563+
564+ type Client t m (Auth.Auth auths a :> api) tag = Client t m api tag
565+ clientWithRoute Proxy = clientWithRoute (Proxy :: Proxy api)
566+
567+
568+type family HasCookieAuth xs :: Constraint where
569+ HasCookieAuth (Auth.Cookie ': xs) = ()
570+ HasCookieAuth (x ': xs) = HasCookieAuth xs
571+ HasCookieAuth '[] = CookieAuthNotEnabled
572+
573+class CookieAuthNotEnabled
574+