Simple design for client with content-types

This commit is contained in:
Timo von Holtz 2015-02-17 17:17:10 +11:00
parent 6c99dfcb6c
commit 48030a6a1b
4 changed files with 57 additions and 43 deletions

View file

@ -50,6 +50,7 @@ library
, network-uri >= 2.6
, safe
, servant >= 0.2.2
, servant-server
, string-conversions
, text
, transformers

View file

@ -1,3 +1,4 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
@ -26,6 +27,7 @@ import Network.HTTP.Media
import qualified Network.HTTP.Types as H
import Servant.API
import Servant.Common.BaseUrl
import Servant.Server.ContentTypes
import Servant.Common.Req
import Servant.Common.Text
@ -116,10 +118,10 @@ instance HasClient Delete where
-- side querying function that is created when calling 'client'
-- will just require an argument that specifies the scheme, host
-- and port to send the request to.
instance FromJSON result => HasClient (Get result) where
type Client (Get result) = BaseUrl -> EitherT String IO result
instance (MimeUnrender ct result) => HasClient (Get (ct ': cts) result) where
type Client (Get (ct ': cts) result) = BaseUrl -> EitherT String IO result
clientWithRoute Proxy req host =
performRequestJSON H.methodGet req 200 host
performRequestCT (Proxy :: Proxy ct) H.methodGet req 200 host
-- | If you use a 'Header' in one of your endpoints in your API,
-- the corresponding querying function will automatically take
@ -162,21 +164,21 @@ instance (KnownSymbol sym, ToText a, HasClient sublayout)
-- side querying function that is created when calling 'client'
-- will just require an argument that specifies the scheme, host
-- and port to send the request to.
instance FromJSON a => HasClient (Post a) where
type Client (Post a) = BaseUrl -> EitherT String IO a
instance (MimeUnrender ct a) => HasClient (Post (ct ': cts) a) where
type Client (Post (ct ': cts) a) = BaseUrl -> EitherT String IO a
clientWithRoute Proxy req uri =
performRequestJSON H.methodPost req 201 uri
performRequestCT (Proxy :: Proxy ct) H.methodPost req 201 uri
-- | If you have a 'Put' endpoint in your API, the client
-- side querying function that is created when calling 'client'
-- will just require an argument that specifies the scheme, host
-- and port to send the request to.
instance FromJSON a => HasClient (Put a) where
type Client (Put a) = BaseUrl -> EitherT String IO a
instance (MimeUnrender ct a) => HasClient (Put (ct ': cts) a) where
type Client (Put (ct ': cts) a) = BaseUrl -> EitherT String IO a
clientWithRoute Proxy req host =
performRequestJSON H.methodPut req 200 host
performRequestCT (Proxy :: Proxy ct) H.methodPut req 200 host
-- | If you use a 'QueryParam' in one of your endpoints in your API,
-- the corresponding querying function will automatically take
@ -437,9 +439,9 @@ instance HasClient Raw where
-- > addBook = client myApi
-- > -- then you can just use "addBook" to query that endpoint
instance (ToJSON a, HasClient sublayout)
=> HasClient (ReqBody a :> sublayout) where
=> HasClient (ReqBody (ct ': cts) a :> sublayout) where
type Client (ReqBody a :> sublayout) =
type Client (ReqBody (ct ': cts) a :> sublayout) =
a -> Client sublayout
clientWithRoute Proxy req body =

View file

@ -16,15 +16,17 @@ import Data.Attoparsec.ByteString
import Data.ByteString.Lazy hiding (pack, filter, map, null)
import Data.String
import Data.String.Conversions
import Data.Proxy
import Data.Text (Text)
import Data.Text.Encoding
import Network.HTTP.Client
import Network.HTTP.Client hiding (Proxy)
import Network.HTTP.Client.TLS
import Network.HTTP.Media
import Network.HTTP.Types
import Network.URI
import Servant.Common.BaseUrl
import Servant.Common.Text
import Servant.Server.ContentTypes
import System.IO.Unsafe
import qualified Network.HTTP.Client as Client
@ -142,18 +144,18 @@ performRequest reqMethod req isWantedStatus reqHost = do
showStatus (Status code message) =
show code ++ " - " ++ cs message
performRequestJSON :: FromJSON result =>
Method -> Req -> Int -> BaseUrl -> EitherT String IO result
performRequestJSON reqMethod req wantedStatus reqHost = do
(_status, respBody, contentType) <-
performRequest reqMethod (req { reqAccept = ["application"//"json"] }) (== wantedStatus) reqHost
unless (matches contentType ("application"//"json")) $
left $ "requested Content-Type application/json, but got " <> show contentType
either
(\ message -> left (displayHttpRequest reqMethod ++ " returned invalid json: " ++ message))
performRequestCT :: MimeUnrender ct result =>
Proxy ct -> Method -> Req -> Int -> BaseUrl -> EitherT String IO result
performRequestCT ct reqMethod req wantedStatus reqHost = do
let acceptCT = contentType ct
(_status, respBody, respCT) <-
performRequest reqMethod (req { reqAccept = [acceptCT] }) (== wantedStatus) reqHost
unless (matches respCT (acceptCT)) $
left $ "requested Content-Type " <> show acceptCT <> ", but got " <> show respCT
maybe
(left (displayHttpRequest reqMethod ++ " returned invalid response of type: " ++ show respCT))
return
(decodeLenient respBody)
(fromByteString ct respBody)
catchStatusCodeException :: IO a -> IO (Either Status a)

View file

@ -45,24 +45,26 @@ alice :: Person
alice = Person "Alice" 42
type Api =
"get" :> Get Person
"get" :> Get '[JSON] Person
:<|> "delete" :> Delete
:<|> "capture" :> Capture "name" String :> Get Person
:<|> "body" :> ReqBody Person :> Post Person
:<|> "param" :> QueryParam "name" String :> Get Person
:<|> "params" :> QueryParams "names" String :> Get [Person]
:<|> "flag" :> QueryFlag "flag" :> Get Bool
:<|> "matrixparam" :> MatrixParam "name" String :> Get Person
:<|> "matrixparams" :> MatrixParams "name" String :> Get [Person]
:<|> "matrixflag" :> MatrixFlag "flag" :> Get Bool
:<|> "capture" :> Capture "name" String :> Get '[JSON] Person
:<|> "body" :> ReqBody '[JSON] Person :> Post '[JSON] Person
:<|> "param" :> QueryParam "name" String :> Get '[JSON] Person
:<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]
:<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool
{-
:<|> "matrixparam" :> MatrixParam "name" String :> Get '[JSON] Person
:<|> "matrixparams" :> MatrixParams "name" String :> Get '[JSON] [Person]
:<|> "matrixflag" :> MatrixFlag "flag" :> Get '[JSON] Bool
-}
:<|> "rawSuccess" :> Raw
:<|> "rawFailure" :> Raw
:<|> "multiple" :>
Capture "first" String :>
QueryParam "second" Int :>
QueryFlag "third" :>
ReqBody [(String, [Rational])] :>
Get (String, Maybe Int, Bool, [(String, [Rational])])
ReqBody '[JSON] [(String, [Rational])] :>
Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])
api :: Proxy Api
api = Proxy
@ -78,12 +80,14 @@ server = serve api (
Nothing -> left (400, "missing parameter"))
:<|> (\ names -> return (zipWith Person names [0..]))
:<|> return
{-
:<|> (\ name -> case name of
Just "alice" -> return alice
Just name -> left (400, name ++ " not found")
Nothing -> left (400, "missing parameter"))
:<|> (\ names -> return (zipWith Person names [0..]))
:<|> return
-}
:<|> (\ _request respond -> respond $ responseLBS ok200 [] "rawSuccess")
:<|> (\ _request respond -> respond $ responseLBS badRequest400 [] "rawFailure")
:<|> \ a b c d -> return (a, b, c, d)
@ -99,9 +103,11 @@ getBody :: Person -> BaseUrl -> EitherT String IO Person
getQueryParam :: Maybe String -> BaseUrl -> EitherT String IO Person
getQueryParams :: [String] -> BaseUrl -> EitherT String IO [Person]
getQueryFlag :: Bool -> BaseUrl -> EitherT String IO Bool
{-
getMatrixParam :: Maybe String -> BaseUrl -> EitherT String IO Person
getMatrixParams :: [String] -> BaseUrl -> EitherT String IO [Person]
getMatrixFlag :: Bool -> BaseUrl -> EitherT String IO Bool
-}
getRawSuccess :: Method -> BaseUrl -> EitherT String IO (Int, ByteString, MediaType)
getRawFailure :: Method -> BaseUrl -> EitherT String IO (Int, ByteString, MediaType)
getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
@ -114,9 +120,11 @@ getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
:<|> getQueryParam
:<|> getQueryParams
:<|> getQueryFlag
{-
:<|> getMatrixParam
:<|> getMatrixParams
:<|> getMatrixFlag
-}
:<|> getRawSuccess
:<|> getRawFailure
:<|> getMultiple)
@ -152,6 +160,7 @@ spec = do
it (show flag) $ withServer $ \ host -> do
runEitherT (getQueryFlag flag host) `shouldReturn` Right flag
{-
it "Servant.API.MatrixParam" $ withServer $ \ host -> do
runEitherT (getMatrixParam (Just "alice") host) `shouldReturn` Right alice
Left result <- runEitherT (getMatrixParam (Just "bob") host)
@ -166,6 +175,7 @@ spec = do
forM_ [False, True] $ \ flag ->
it (show flag) $ withServer $ \ host -> do
runEitherT (getMatrixFlag flag host) `shouldReturn` Right flag
-}
it "Servant.API.Raw on success" $ withServer $ \ host -> do
runEitherT (getRawSuccess methodGet host) `shouldReturn` Right (200, "rawSuccess", "application"//"octet-stream")
@ -184,9 +194,9 @@ spec = do
context "client correctly handles error status codes" $ do
let test :: WrappedApi -> Spec
test (WrappedApi api) =
it (show (typeOf api)) $
let test :: (WrappedApi, String) -> Spec
test (WrappedApi api, desc) =
it desc $
withWaiDaemon (return (serve api (left (500, "error message")))) $
\ host -> do
let getResponse :: BaseUrl -> EitherT String IO ()
@ -194,16 +204,15 @@ spec = do
Left result <- runEitherT (getResponse host)
result `shouldContain` "error message"
mapM_ test $
(WrappedApi (Proxy :: Proxy Delete)) :
(WrappedApi (Proxy :: Proxy (Get ()))) :
(WrappedApi (Proxy :: Proxy (Post ()))) :
(WrappedApi (Proxy :: Proxy (Put ()))) :
(WrappedApi (Proxy :: Proxy Delete), "Delete") :
(WrappedApi (Proxy :: Proxy (Get '[JSON] ())), "Delete") :
(WrappedApi (Proxy :: Proxy (Post '[JSON] ())), "Delete") :
(WrappedApi (Proxy :: Proxy (Put '[JSON] ())), "Delete") :
[]
data WrappedApi where
WrappedApi :: (HasServer api, Server api ~ EitherT (Int, String) IO a,
HasClient api, Client api ~ (BaseUrl -> EitherT String IO ()),
Typeable api) =>
HasClient api, Client api ~ (BaseUrl -> EitherT String IO ())) =>
Proxy api -> WrappedApi