module Hedgehog.Servant
( GList(..)
, HasGen(..)
, GenRequest(..)
) where
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Internal as BS (c2w)
import qualified Data.CaseInsensitive as CI
import Data.Proxy (Proxy(..))
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
import qualified Data.Text as Text
import Data.String.Conversions (ConvertibleStrings, cs)
import GHC.TypeLits (KnownSymbol, symbolVal)
import Hedgehog
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Network.HTTP.Media (renderHeader)
import Network.HTTP.Client (Request(..), RequestBody(..))
import Network.HTTP.Client (defaultRequest)
import Network.HTTP.Types (HeaderName)
import Servant.API (ToHttpApiData(..))
import Servant.API (Capture', CaptureAll, Header', Description, Summary)
import Servant.API (QueryParam', QueryParams, QueryFlag)
import Servant.API (ReqBody', Verb, ReflectMethod)
import Servant.API (BasicAuth, HttpVersion, IsSecure, RemoteHost, Vault)
import Servant.API (WithNamedContext)
import Servant.API ((:>), (:<|>))
import Servant.API (reflectMethod)
import Servant.API.ContentTypes (AllMimeRender(..))
import Servant.Client (BaseUrl(..), Scheme(..))
data GList (a :: [*]) where
GNil :: GList '[]
(:*:) :: Gen x -> GList xs -> GList (Gen x ': xs)
infixr 6 :*:
class HasGen (g :: *) (gens :: [*]) where
getGen :: GList gens -> Gen g
instance {-# OVERLAPPING #-} HasGen h (Gen h ': rest) where
getGen (ha :*: _) = ha
instance {-# OVERLAPPABLE #-} (HasGen h rest) => HasGen h (first ': rest) where
getGen (_ :*: hs) = getGen hs
class GenRequest (api :: *) (gens :: [*]) where
genRequest :: Proxy api -> GList gens -> Gen (BaseUrl -> Request)
instance
( GenRequest a reqs
, GenRequest b reqs
) => GenRequest (a :<|> b) reqs where
genRequest _ gens =
Gen.choice
[ genRequest (Proxy @a) gens
, genRequest (Proxy @b) gens
]
instance
( GenRequest api reqs
) => GenRequest (Description d :> api) reqs where
genRequest _ = genRequest (Proxy @api)
instance
( GenRequest api reqs
) => GenRequest (Summary s :> api) reqs where
genRequest _ = genRequest (Proxy @api)
instance
( KnownSymbol path
, GenRequest api reqs
) => GenRequest (path :> api) reqs where
genRequest _ gens = do
makeRequest <- genRequest (Proxy @api) gens
pure $ prependPath (symbolVal $ Proxy @path) . makeRequest
instance
( ToHttpApiData a
, HasGen a gens
, GenRequest api gens
) => GenRequest (Capture' modifiers sym a :> api) gens where
genRequest _ gens = do
capture <- toUrlPiece <$> getGen @a @gens gens
makeRequest <- genRequest (Proxy @api) gens
pure $ prependPath capture . makeRequest
instance
( ToHttpApiData a
, HasGen a gens
, GenRequest api gens
) => GenRequest (CaptureAll sym a :> api) gens where
genRequest _ gens = do
captures <- Gen.list (Range.linear 0 10) (getGen @a @gens gens)
makeRequest <- genRequest (Proxy @api) gens
pure $ \baseUrl ->
foldr (prependPath . toUrlPiece) (makeRequest baseUrl) captures
instance
( HasGen header gens
, KnownSymbol headerName
, ToHttpApiData header
, GenRequest api gens
) => GenRequest (Header' mods headerName header :> api) gens where
genRequest _ gens = do
let headerName = CI.mk . cs . symbolVal $ Proxy @headerName
header <- getGen @header @gens gens
makeRequest <- genRequest (Proxy @api) gens
pure $ addHeader headerName (toHeader header) . makeRequest
instance
( KnownSymbol name
, GenRequest api gens
) => GenRequest (QueryFlag name :> api) gens where
genRequest _ gens = do
let paramName = toUrlPiece . symbolVal $ Proxy @name
makeRequest <- genRequest (Proxy @api) gens
pure $ \baseUrl ->
let
partialReq = makeRequest baseUrl
oldQuery = decodeUtf8 $ queryString partialReq
newQuery =
if Text.null oldQuery then paramName
else paramName <> "&" <> oldQuery
in
partialReq { queryString = encodeUtf8 newQuery }
instance
( KnownSymbol paramName
, ToHttpApiData param
, HasGen param gens
, GenRequest api gens
) => GenRequest (QueryParam' mods paramName param :> api) gens where
genRequest _ gens = do
queryParam <- toUrlPiece <$> getGen @param @gens gens
let
paramName = toUrlPiece . symbolVal $ Proxy @paramName
query = paramName <> "=" <> queryParam
makeRequest <- genRequest (Proxy @api) gens
pure $ \baseUrl ->
let
partialReq = makeRequest baseUrl
oldQuery = decodeUtf8 $ queryString partialReq
newQuery =
if Text.null oldQuery then query
else query <> "&" <> oldQuery
in
partialReq { queryString = encodeUtf8 newQuery }
instance
( KnownSymbol paramName
, HasGen param gens
, ToHttpApiData param
, GenRequest api gens
) => GenRequest (QueryParams paramName param :> api) gens where
genRequest _ gens = do
params <- Gen.list (Range.linear 1 20) (getGen @param @gens gens)
let
paramName = toUrlPiece . symbolVal $ Proxy @paramName
params' = fmap (((paramName <> "[]=") <>) . toUrlPiece) params
queryParams = Text.intercalate "&" params'
makeRequest <- genRequest (Proxy @api) gens
pure $ \baseUrl ->
let
partialReq = makeRequest baseUrl
oldQuery = decodeUtf8 $ queryString partialReq
newQuery =
if Text.null oldQuery then queryParams
else queryParams <> "&" <> oldQuery
in
partialReq { queryString = encodeUtf8 newQuery }
instance
( AllMimeRender contentTypes body
, HasGen body gens
, GenRequest api gens
) => GenRequest (ReqBody' mods contentTypes body :> api) gens where
genRequest _ gens = do
newBody <- getGen @body @gens gens
(contentType, body) <-
Gen.element $ allMimeRender (Proxy @contentTypes) newBody
makeRequest <- genRequest (Proxy @api) gens
pure $ setBody body
. addHeader "Content-Type" (renderHeader contentType)
. makeRequest
instance
( ReflectMethod method
) => GenRequest (Verb method status contentTypes body) gens where
genRequest _ _ =
pure $ \baseUrl -> defaultRequest
{ host = cs . baseUrlHost $ baseUrl
, port = baseUrlPort baseUrl
, secure = baseUrlScheme baseUrl == Https
, method = reflectMethod (Proxy @method)
}
instance
( GenRequest api gens
) => GenRequest (BasicAuth x y :> api) gens where
genRequest _ gens = genRequest (Proxy @api) gens
instance
( GenRequest api gens
) => GenRequest (HttpVersion :> api) gens where
genRequest _ gens = genRequest (Proxy @api) gens
instance
( GenRequest api gens
) => GenRequest (IsSecure :> api) gens where
genRequest _ gens = genRequest (Proxy @api) gens
instance
( GenRequest api gens
) => GenRequest (RemoteHost :> api) gens where
genRequest _ gens = genRequest (Proxy @api) gens
instance
( GenRequest api gens
) => GenRequest (Vault :> api) gens where
genRequest _ gens = genRequest (Proxy @api) gens
instance
( GenRequest api gens
) => GenRequest (WithNamedContext x y api) gens where
genRequest _ gens = genRequest (Proxy @api) gens
setBody :: LBS.ByteString -> Request -> Request
setBody body oldReq = oldReq { requestBody = RequestBodyLBS body }
addHeader :: HeaderName -> BS.ByteString -> Request -> Request
addHeader name value oldReq =
let
headers = (name, value) : requestHeaders oldReq
in
oldReq { requestHeaders = headers }
prependPath :: ConvertibleStrings s BS.ByteString => s -> Request -> Request
prependPath new oldReq =
let
partialUrl = BS.dropWhile (== BS.c2w '/') . path $ oldReq
urlPieces = filter (not . BS.null) [cs new, partialUrl]
in
oldReq { path = "/" <> BS.intercalate "/" urlPieces }