195 lines
7.6 KiB
Haskell
195 lines
7.6 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE PolyKinds #-}
|
|
module Servant.QuickCheck.Internal.HasGenRequest where
|
|
|
|
import Data.String (fromString)
|
|
import Data.String.Conversions (cs)
|
|
import GHC.TypeLits (KnownSymbol, Nat, symbolVal)
|
|
import Network.HTTP.Client (Request, RequestBody (..), host, method, path,
|
|
port, queryString, requestBody, requestHeaders,
|
|
secure, defaultRequest)
|
|
import Network.HTTP.Media (renderHeader)
|
|
import Prelude.Compat
|
|
import Servant
|
|
import Servant.API.ContentTypes (AllMimeRender (..))
|
|
import Servant.Client (BaseUrl (..), Scheme (..))
|
|
import Test.QuickCheck (Arbitrary (..), Gen, elements, frequency)
|
|
|
|
import qualified Data.ByteString as BS
|
|
import qualified Data.ByteString.Internal as BS (c2w)
|
|
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- runGenRequest
|
|
|
|
-- | This function returns a QuickCheck `Gen a` when passed a servant API value,
|
|
-- typically a `Proxy API`. The generator returned is a function
|
|
-- that accepts a `BaseUrl` and returns a `Request`, which can then be used
|
|
-- to issue network requests. This `Gen` type makes it easier to compare distinct
|
|
-- APIs across different `BaseUrl`s.
|
|
runGenRequest :: HasGenRequest a => Proxy a -> Gen (BaseUrl -> Request)
|
|
runGenRequest = snd . genRequest
|
|
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- HasGenRequest
|
|
|
|
-- | This is the core Servant-Quickcheck generator, which, when given a `Proxy API`
|
|
-- will return a pair of `Int` and `Gen a`, where `a` is a function from
|
|
-- `BaseUrl` to a `Network.Http.Client.Request`. The `Int` is a weight for the
|
|
-- QuickCheck `frequency` function which ensures a random distribution across
|
|
-- all endpoints in an API.
|
|
class HasGenRequest a where
|
|
genRequest :: Proxy a -> (Int, Gen (BaseUrl -> Request))
|
|
|
|
|
|
instance (HasGenRequest a, HasGenRequest b) => HasGenRequest (a :<|> b) where
|
|
genRequest _
|
|
= (lf + rf, frequency [l, r])
|
|
where
|
|
l@(lf, _) = genRequest (Proxy :: Proxy a)
|
|
r@(rf, _) = genRequest (Proxy :: Proxy b)
|
|
|
|
|
|
instance (KnownSymbol path, HasGenRequest b) => HasGenRequest (path :> b) where
|
|
genRequest _ = (oldf, do
|
|
old' <- old
|
|
return $ \burl -> let r = old' burl
|
|
oldPath = path r
|
|
oldPath' = BS.dropWhile (== BS.c2w '/') oldPath
|
|
paths = filter (not . BS.null) [new, oldPath']
|
|
in r { path = "/" <> BS.intercalate "/" paths })
|
|
where
|
|
(oldf, old) = genRequest (Proxy :: Proxy b)
|
|
new = cs $ symbolVal (Proxy :: Proxy path)
|
|
|
|
#if MIN_VERSION_servant(0,11,0)
|
|
instance HasGenRequest EmptyAPI where
|
|
genRequest _ = (0, error "EmptyAPIs cannot be queried.")
|
|
#endif
|
|
|
|
#if MIN_VERSION_servant(0,12,0)
|
|
instance HasGenRequest api => HasGenRequest (Summary d :> api) where
|
|
genRequest _ = genRequest (Proxy :: Proxy api)
|
|
|
|
instance HasGenRequest api => HasGenRequest (Description d :> api) where
|
|
genRequest _ = genRequest (Proxy :: Proxy api)
|
|
#endif
|
|
|
|
instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
|
|
=> HasGenRequest (Capture' mods x c :> b) where
|
|
genRequest _ = (oldf, do
|
|
old' <- old
|
|
new' <- toUrlPiece <$> new
|
|
return $ \burl -> let r = old' burl in r { path = cs new' <> path r })
|
|
where
|
|
(oldf, old) = genRequest (Proxy :: Proxy b)
|
|
new = arbitrary :: Gen c
|
|
|
|
#if MIN_VERSION_servant(0,8,0)
|
|
instance (Arbitrary c, HasGenRequest b, ToHttpApiData c )
|
|
=> HasGenRequest (CaptureAll x c :> b) where
|
|
genRequest _ = (oldf, do
|
|
old' <- old
|
|
new' <- fmap (cs . toUrlPiece) <$> new
|
|
let new'' = BS.intercalate "/" new'
|
|
return $ \burl -> let r = old' burl in r { path = new'' <> path r })
|
|
where
|
|
(oldf, old) = genRequest (Proxy :: Proxy b)
|
|
new = arbitrary :: Gen [c]
|
|
#endif
|
|
|
|
instance (Arbitrary c, KnownSymbol h, HasGenRequest b, ToHttpApiData c)
|
|
=> HasGenRequest (Header' mods h c :> b) where
|
|
genRequest _ = (oldf, do
|
|
old' <- old
|
|
new' <- toUrlPiece <$> new -- TODO: generate lenient or/and optional
|
|
return $ \burl -> let r = old' burl in r {
|
|
requestHeaders = (hdr, cs new') : requestHeaders r })
|
|
where
|
|
(oldf, old) = genRequest (Proxy :: Proxy b)
|
|
hdr = fromString $ symbolVal (Proxy :: Proxy h)
|
|
new = arbitrary :: Gen c
|
|
|
|
instance (AllMimeRender x c, Arbitrary c, HasGenRequest b)
|
|
=> HasGenRequest (ReqBody' mods x c :> b) where
|
|
genRequest _ = (oldf, do
|
|
old' <- old -- TODO: generate lenient
|
|
new' <- new
|
|
(ct, bd) <- elements $ allMimeRender (Proxy :: Proxy x) new'
|
|
return $ \burl -> let r = old' burl in r {
|
|
requestBody = RequestBodyLBS bd
|
|
, requestHeaders = ("Content-Type", renderHeader ct) : requestHeaders r
|
|
})
|
|
where
|
|
(oldf, old) = genRequest (Proxy :: Proxy b)
|
|
new = arbitrary :: Gen c
|
|
|
|
instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
|
|
=> HasGenRequest (QueryParam' mods x c :> b) where
|
|
genRequest _ = (oldf, do
|
|
new' <- new -- TODO: generate lenient or/and optional
|
|
old' <- old
|
|
return $ \burl -> let r = old' burl
|
|
newExpr = param <> "=" <> cs (toQueryParam new')
|
|
qs = queryString r in r {
|
|
queryString = if BS.null qs then newExpr else newExpr <> "&" <> qs })
|
|
where
|
|
(oldf, old) = genRequest (Proxy :: Proxy b)
|
|
param = cs $ symbolVal (Proxy :: Proxy x)
|
|
new = arbitrary :: Gen c
|
|
|
|
instance (KnownSymbol x, Arbitrary c, ToHttpApiData c, HasGenRequest b)
|
|
=> HasGenRequest (QueryParams x c :> b) where
|
|
genRequest _ = (oldf, do
|
|
new' <- new
|
|
old' <- old
|
|
return $ \burl -> let r = old' burl in r {
|
|
queryString = queryString r
|
|
<> if length new' > 0 then fold (toParam <$> new') else ""})
|
|
where
|
|
(oldf, old) = genRequest (Proxy :: Proxy b)
|
|
param = cs $ symbolVal (Proxy :: Proxy x)
|
|
new = arbitrary :: Gen [c]
|
|
toParam c = param <> "[]=" <> cs (toQueryParam c)
|
|
fold = foldr1 (\a b -> a <> "&" <> b)
|
|
|
|
instance (KnownSymbol x, HasGenRequest b)
|
|
=> HasGenRequest (QueryFlag x :> b) where
|
|
genRequest _ = (oldf, do
|
|
old' <- old
|
|
return $ \burl -> let r = old' burl
|
|
qs = queryString r in r {
|
|
queryString = if BS.null qs then param else param <> "&" <> qs })
|
|
where
|
|
(oldf, old) = genRequest (Proxy :: Proxy b)
|
|
param = cs $ symbolVal (Proxy :: Proxy x)
|
|
|
|
instance (ReflectMethod method)
|
|
=> HasGenRequest (Verb (method :: k) (status :: Nat) (cts :: [*]) a) where
|
|
genRequest _ = (1, return $ \burl -> defaultRequest
|
|
{ host = cs $ baseUrlHost burl
|
|
, port = baseUrlPort burl
|
|
, secure = baseUrlScheme burl == Https
|
|
, method = reflectMethod (Proxy :: Proxy method)
|
|
})
|
|
|
|
instance (HasGenRequest a) => HasGenRequest (RemoteHost :> a) where
|
|
genRequest _ = genRequest (Proxy :: Proxy a)
|
|
|
|
instance (HasGenRequest a) => HasGenRequest (IsSecure :> a) where
|
|
genRequest _ = genRequest (Proxy :: Proxy a)
|
|
|
|
instance (HasGenRequest a) => HasGenRequest (HttpVersion :> a) where
|
|
genRequest _ = genRequest (Proxy :: Proxy a)
|
|
|
|
instance (HasGenRequest a) => HasGenRequest (Vault :> a) where
|
|
genRequest _ = genRequest (Proxy :: Proxy a)
|
|
|
|
instance (HasGenRequest a) => HasGenRequest (WithNamedContext x y a) where
|
|
genRequest _ = genRequest (Proxy :: Proxy a)
|
|
|
|
-- TODO: Try logging in
|
|
instance (HasGenRequest a) => HasGenRequest (BasicAuth x y :> a) where
|
|
genRequest _ = genRequest (Proxy :: Proxy a)
|