queryparams

This commit is contained in:
Mark Wotton 2021-03-18 12:44:43 -04:00
parent 1728c99798
commit 601694a30e
11 changed files with 95 additions and 24 deletions

View File

@ -19,6 +19,7 @@ dependencies:
- containers - containers
- random - random
- hashable - hashable
- http-types
- lifted-base - lifted-base
- monad-control - monad-control
- mtl - mtl

View File

@ -4,7 +4,7 @@ cabal-version: 1.12
-- --
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
-- --
-- hash: 4c71b6b9460f4719791bb8e67f4a5398b807cd8d17c43dc69da806bb5ba9fb4b -- hash: 596d33d4f2e29f15b48525514def52b101c9e89b6193275bd5c0a4c253de8d5c
name: roboservant name: roboservant
version: 0.1.0.2 version: 0.1.0.2
@ -50,6 +50,7 @@ library
, dependent-map , dependent-map
, dependent-sum , dependent-sum
, hashable , hashable
, http-types
, lifted-base , lifted-base
, monad-control , monad-control
, mtl , mtl
@ -86,6 +87,7 @@ test-suite example
, hspec , hspec
, hspec-core , hspec-core
, http-client , http-client
, http-types
, lifted-base , lifted-base
, monad-control , monad-control
, mtl , mtl
@ -113,6 +115,7 @@ test-suite roboservant-test
Nested Nested
Post Post
Product Product
QueryParams
Seeded Seeded
UnsafeIO UnsafeIO
Valid Valid
@ -133,6 +136,7 @@ test-suite roboservant-test
, hspec-wai , hspec-wai
, http-api-data , http-api-data
, http-client , http-client
, http-types
, lifted-base , lifted-base
, monad-control , monad-control
, mtl , mtl

View File

@ -11,7 +11,7 @@
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
-- should all the NormalizeFunction instances be in one place? -- should all the NormalizeFunction instances be in one place?
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module Roboservant.Client where module Roboservant.Client where
@ -27,13 +27,14 @@ import qualified Data.Vinyl.Curry as V
import qualified Data.Text as T import qualified Data.Text as T
import Control.Monad.Reader import Control.Monad.Reader
import Data.Hashable import Data.Hashable
import Network.HTTP.Types.Status
-- fuzz :: forall api. -- fuzz :: forall api.
-- (FlattenServer api, ToReifiedApi (Endpoints api)) => -- (FlattenServer api, ToReifiedApi (Endpoints api)) =>
-- Server api -> -- Server api ->
-- Config -> -- Config ->
-- IO (Maybe Report) -- IO (Maybe Report)
-- fuzz s = fuzz' (reifyServer s) -- fuzz s = fuzz' (reifyServer s)
-- -- todo: how do we pull reifyServer out? -- -- todo: how do we pull reifyServer out?
-- where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api)) -- where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api))
-- => Server api -> ReifiedApi -- => Server api -> ReifiedApi
@ -80,7 +81,7 @@ instance
foo :: V.Curried (EndpointArgs endpoint) (ReaderT ClientEnv IO ResultType) foo :: V.Curried (EndpointArgs endpoint) (ReaderT ClientEnv IO ResultType)
-> V.Curried (EndpointArgs endpoint) (IO ResultType) -> V.Curried (EndpointArgs endpoint) (IO ResultType)
foo = mapCurried @(EndpointArgs endpoint) @(ReaderT ClientEnv IO ResultType) (`runReaderT` clientEnv) foo = mapCurried @(EndpointArgs endpoint) @(ReaderT ClientEnv IO ResultType) (`runReaderT` clientEnv)
mapCurried :: forall ts a b. V.RecordCurry' ts => (a -> b) -> V.Curried ts a -> V.Curried ts b mapCurried :: forall ts a b. V.RecordCurry' ts => (a -> b) -> V.Curried ts a -> V.Curried ts b
mapCurried f g = V.rcurry' @ts $ f . V.runcurry' g mapCurried f g = V.rcurry' @ts $ f . V.runcurry' g
@ -90,10 +91,15 @@ type ResultType = Either InteractionError (NonEmpty (Dynamic,Int))
instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (ClientM x) where instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (ClientM x) where
type Normal (ClientM x) = ReaderT ClientEnv IO (Either InteractionError (NonEmpty (Dynamic,Int))) type Normal (ClientM x) = ReaderT ClientEnv IO (Either InteractionError (NonEmpty (Dynamic,Int)))
normalize c = ReaderT $ normalize c = ReaderT $
fmap (bimap (InteractionError . T.pack . show) breakdown) . runClientM c fmap (bimap renderClientError breakdown) . runClientM c
-- where
renderClientError :: ClientError -> InteractionError
renderClientError err = case err of
FailureResponse _ (Response{responseStatusCode}) -> InteractionError textual (responseStatusCode == status500)
_ -> InteractionError textual True
where textual = T.pack $ show err
instance ToReifiedClientApi '[] where instance ToReifiedClientApi '[] where
toReifiedClientApi NoClientEndpoints _ _ = [] toReifiedClientApi NoClientEndpoints _ _ = []
@ -105,7 +111,7 @@ instance
FlattenClient (endpoint :<|> api) FlattenClient (endpoint :<|> api)
where where
flattenClient (endpoint :<|> c) = endpoint `AClientEndpoint` flattenClient @api c flattenClient (endpoint :<|> c) = endpoint `AClientEndpoint` flattenClient @api c
instance instance
( (
Endpoints api ~ '[api] Endpoints api ~ '[api]
@ -114,6 +120,7 @@ instance
where where
flattenClient c = c `AClientEndpoint` NoClientEndpoints flattenClient c = c `AClientEndpoint` NoClientEndpoints
instance FlattenClient (Verb method statusCode contentTypes responseType) instance FlattenClient (Verb method statusCode contentTypes responseType)
where where
flattenClient c = c `AClientEndpoint` NoClientEndpoints flattenClient c = c `AClientEndpoint` NoClientEndpoints

View File

@ -56,7 +56,7 @@ import GHC.Generics ((:*:) (..))
import Roboservant.Types import Roboservant.Types
( ApiOffset (..), ( ApiOffset (..),
Argument (..), Argument (..),
InteractionError, InteractionError(..),
Provenance (..), Provenance (..),
ReifiedApi, ReifiedApi,
ReifiedEndpoint (..), ReifiedEndpoint (..),
@ -242,7 +242,10 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
execute fuzzop func args = do execute fuzzop func args = do
(liftIO . logInfo . show . (fuzzop,) . stash) =<< get (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
liftIO (V.runcurry' func argVals) >>= \case liftIO (V.runcurry' func argVals) >>= \case
Left (e::InteractionError) -> throw e Left (e::InteractionError) ->
if fatalError e
then throw e
else pure ()
Right (dyn :: NEL.NonEmpty (Dynamic, Int)) -> do Right (dyn :: NEL.NonEmpty (Dynamic, Int)) -> do
modify' modify'
( \fs@FuzzState {..} -> ( \fs@FuzzState {..} ->

View File

@ -12,6 +12,7 @@
module Roboservant.Types.BuildFrom where module Roboservant.Types.BuildFrom where
import Data.List(nub)
import qualified Data.Dependent.Map as DM import qualified Data.Dependent.Map as DM
import Data.Hashable import Data.Hashable
import qualified Data.IntSet as IntSet import qualified Data.IntSet as IntSet
@ -59,11 +60,12 @@ deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, BuildFrom x)
-- this isn't wonderful, but we need a hand-rolled instance for recursive datatypes right now. -- this isn't wonderful, but we need a hand-rolled instance for recursive datatypes right now.
-- with an arbitrary-ish interface, we could use a size parameter, rng access etc. -- with an arbitrary-ish interface, we could use a size parameter, rng access etc.
instance (BuildFrom x) => BuildFrom [x] where instance (Eq x, BuildFrom x) => BuildFrom [x] where
extras stash = map (\xs -> (concatMap fst xs, map snd xs)) $ notpowerset $ extras @x stash extras stash =
nub $ map (\xs -> (concatMap fst xs, map snd xs)) $ notpowerset $ buildFrom' @x stash
where where
-- powerset creates way too much stuff. something better here eventually. -- powerset creates way too much stuff. something better here eventually.
notpowerset xs = xs:map pure xs notpowerset xs = []:xs:map pure xs
instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x :: Type)) where instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x :: Type)) where

View File

@ -28,7 +28,7 @@ import GHC.Generics ((:*:)(..))
import Roboservant.Types.Internal import Roboservant.Types.Internal
import Roboservant.Types.Breakdown import Roboservant.Types.Breakdown
import Roboservant.Types.BuildFrom import Roboservant.Types.BuildFrom
import Data.Kind import Data.Kind(Type)
import Servant import Servant
import Servant.API.Modifiers(FoldRequired,FoldLenient) import Servant.API.Modifiers(FoldRequired,FoldLenient)
import GHC.TypeLits (Symbol) import GHC.TypeLits (Symbol)
@ -52,6 +52,9 @@ data ReifiedEndpoint = forall as. (V.RecordToList as, V.RMap as) => ReifiedEndpo
, reEndpointFunc :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic,Int)))) , reEndpointFunc :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
} }
instance Show ReifiedEndpoint where
show _ = "lol"
class ( V.RecordToList (EndpointArgs endpoint) class ( V.RecordToList (EndpointArgs endpoint)
, V.RMap (EndpointArgs endpoint) , V.RMap (EndpointArgs endpoint)
) => ToReifiedEndpoint (endpoint :: *) where ) => ToReifiedEndpoint (endpoint :: *) where
@ -64,13 +67,15 @@ class ( V.RecordToList (EndpointArgs endpoint)
tagType :: Typeable a => f a -> TypedF f a tagType :: Typeable a => f a -> TypedF f a
tagType = (R.typeRep :*:) tagType = (R.typeRep :*:)
newtype InteractionError = InteractionError T.Text data InteractionError = InteractionError
{ errorMessage :: T.Text
, fatalError :: Bool
}
deriving Show deriving Show
instance Exception InteractionError instance Exception InteractionError
instance instance
(Typeable responseType, Breakdown responseType) => (Typeable responseType, Breakdown responseType) =>
ToReifiedEndpoint (Verb method statusCode contentTypes responseType) ToReifiedEndpoint (Verb method statusCode contentTypes responseType)
@ -87,6 +92,16 @@ instance
type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
reifiedEndpointArguments = reifiedEndpointArguments @endpoint reifiedEndpointArguments = reifiedEndpointArguments @endpoint
instance
(ToReifiedEndpoint endpoint) =>
ToReifiedEndpoint (RemoteHost :> endpoint)
where
type EndpointArgs (RemoteHost :> endpoint) = EndpointArgs endpoint
type EndpointRes (RemoteHost :> endpoint) = EndpointRes endpoint
reifiedEndpointArguments = reifiedEndpointArguments @endpoint
instance instance
(ToReifiedEndpoint endpoint) => (ToReifiedEndpoint endpoint) =>
ToReifiedEndpoint (Description s :> endpoint) ToReifiedEndpoint (Description s :> endpoint)
@ -129,6 +144,24 @@ instance
tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType))) tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
V.:& reifiedEndpointArguments @endpoint V.:& reifiedEndpointArguments @endpoint
instance
( BuildFrom paramType
, ToReifiedEndpoint endpoint
, Show paramType
, Eq paramType
) =>
ToReifiedEndpoint (QueryParams name paramType :> endpoint)
where
type EndpointArgs (QueryParams name paramType :> endpoint) = [paramType] ': EndpointArgs endpoint
type EndpointRes (QueryParams name paramType :> endpoint) = EndpointRes endpoint
reifiedEndpointArguments =
tagType (Argument (buildFrom @[paramType]))
V.:& reifiedEndpointArguments @endpoint
instance instance
( BuildFrom (IfRequiredLenient T.Text mods headerType) ( BuildFrom (IfRequiredLenient T.Text mods headerType)
, ToReifiedEndpoint endpoint , ToReifiedEndpoint endpoint

View File

@ -66,7 +66,8 @@ instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x)
where where
-- | TODO improve this -- | TODO improve this
renderServerError :: ServerError -> InteractionError renderServerError :: ServerError -> InteractionError
renderServerError = InteractionError . T.pack . show renderServerError s = InteractionError (T.pack $ show s) (errHTTPCode serverError == 500)
Right x -> pure $ Right $ breakdown x Right x -> pure $ Right $ breakdown x
@ -91,7 +92,7 @@ instance
FlattenServer (endpoint :<|> api) FlattenServer (endpoint :<|> api)
where where
flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
instance instance
( (
Endpoints api ~ '[api] Endpoints api ~ '[api]

View File

@ -11,7 +11,7 @@ import Servant
import Servant.API.Flatten import Servant.API.Flatten
type Api = type Api =
( "one" :> Post '[JSON] Int ( "one" :> Summary "foo" :> Post '[JSON] Int
:<|> "two" :> Post '[JSON] Int :<|> "two" :> Post '[JSON] Int
) )
:<|> ( "three" :> Post '[JSON] Int :<|> ( "three" :> Post '[JSON] Int

15
test/QueryParams.hs Normal file
View File

@ -0,0 +1,15 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
module QueryParams where
import Servant
type Api = QueryParams "ints" Int :> Get '[JSON] [Int]
server :: Server Api
server = pure

View File

@ -22,6 +22,7 @@ import qualified Headers
import qualified Nested import qualified Nested
import qualified Post import qualified Post
import qualified Product import qualified Product
import qualified QueryParams
import qualified Roboservant as R import qualified Roboservant as R
import qualified Roboservant.Server as RS import qualified Roboservant.Server as RS
import qualified Roboservant.Client as RC import qualified Roboservant.Client as RC
@ -41,7 +42,7 @@ main :: IO ()
main = hspec spec main = hspec spec
fuzzBoth fuzzBoth
:: forall a . :: forall a .
(R.ToReifiedApi (Endpoints a), HasServer a '[], RS.FlattenServer a, RC.ToReifiedClientApi (Endpoints a), RC.FlattenClient a, (R.ToReifiedApi (Endpoints a), HasServer a '[], RS.FlattenServer a, RC.ToReifiedClientApi (Endpoints a), RC.FlattenClient a,
HasClient ClientM a) HasClient ClientM a)
=> String -> Server a -> R.Config -> (Maybe R.Report -> IO ()) -> Spec => String -> Server a -> R.Config -> (Maybe R.Report -> IO ()) -> Spec
@ -76,22 +77,26 @@ spec = do
describe "posted body" $ do describe "posted body" $ do
fuzzBoth @Post.Api "passes a coverage check using a posted body" Post.server R.defaultConfig {R.coverageThreshold = 0.99} fuzzBoth @Post.Api "passes a coverage check using a posted body" Post.server R.defaultConfig {R.coverageThreshold = 0.99}
(`shouldSatisfy` isNothing) (`shouldSatisfy` isNothing)
describe "seeded" $ do describe "seeded" $ do
let res = Seeded.Seed 1 let res = Seeded.Seed 1
shouldFail $ fuzzBoth @Seeded.Api "finds an error using information passed in" Seeded.server shouldFail $ fuzzBoth @Seeded.Api "finds an error using information passed in" Seeded.server
(R.defaultConfig {R.seed = [(toDyn res, hash res)]}) (R.defaultConfig {R.seed = [(toDyn res, hash res)]})
(`shouldSatisfy` isNothing) (`shouldSatisfy` isNothing)
describe "Foo" $ do describe "Foo" $ do
fuzzBoth @Foo.Api "finds an error in a basic app" Foo.server R.defaultConfig (`shouldSatisfy` serverFailure) fuzzBoth @Foo.Api "finds an error in a basic app" Foo.server R.defaultConfig (`shouldSatisfy` serverFailure)
describe "QueryParams" $ do
fuzzBoth @QueryParams.Api "can handle query params" QueryParams.server R.defaultConfig { R.seed = [R.hashedDyn (12::Int)] }
(`shouldSatisfy` isNothing)
describe "BuildFrom" $ do describe "BuildFrom" $ do
describe "headers (and sum types)" $ do describe "headers (and sum types)" $ do
fuzzBoth @Headers.Api "should find a failure that's dependent on using header info" Headers.server R.defaultConfig fuzzBoth @Headers.Api "should find a failure that's dependent on using header info" Headers.server R.defaultConfig
(`shouldSatisfy` serverFailure) (`shouldSatisfy` serverFailure)
describe "product types" $ do describe "product types" $ do
fuzzBoth @Product.Api "should find a failure that's dependent on creating a product" Product.server fuzzBoth @Product.Api "should find a failure that's dependent on creating a product" Product.server
R.defaultConfig {R.seed = [R.hashedDyn 'a', R.hashedDyn (1 :: Int)]} R.defaultConfig {R.seed = [R.hashedDyn 'a', R.hashedDyn (1 :: Int)]}
(`shouldSatisfy` serverFailure) (`shouldSatisfy` serverFailure)
describe "Breakdown" $ do describe "Breakdown" $ do

View File

@ -21,7 +21,7 @@ data Routes route
= Routes = Routes
{ getInt :: { getInt ::
route route
:- Get '[JSON] Int, :- Summary "i'm a summary!" :> Get '[JSON] Int,
captureIt :: captureIt ::
route route
:- Capture "void" Void :> Get '[JSON] () :- Capture "void" Void :> Get '[JSON] ()