on c41cee3 help sign in
26 files changed+159−304
since splitsnapshots
unifiedsplit
.github/workflows/cabal.yml−64 .github/workflows/ci.yml+2−2 README.md+1−2 package.yaml+1−2 roboservant.cabal+4−10 src/Roboservant/Client.hs+10−20 src/Roboservant/Direct.hs+14−16 src/Roboservant/Server.hs+6−2 src/Roboservant/Types.hs−1 src/Roboservant/Types/Breakdown.hs+1−6 src/Roboservant/Types/BuildFrom.hs+4−10 src/Roboservant/Types/Internal.hs+2−2 src/Roboservant/Types/Orphans.hs−7 src/Roboservant/Types/ReifiedApi.hs+4−43 src/Roboservant/Types/ReifiedApi/Server.hs+5−4 stack.yaml+10−5 stack.yaml.lock+58−9 test/Breakdown.hs+1−1 test/Nested.hs+3−3 test/Post.hs+2−2 test/Product.hs+1−1 test/Put.hs−33 test/QueryParams.hs−15 test/Spec.hs+24−39 test/UnsafeIO.hs+3−3 test/Valid.hs+3−2
.github/workflows/cabal.ymldeleted−64
@@ -1,64 +0,0 @@
1−# modified from https://github.com/jgm/pandoc/blob/master/.github/workflows/ci.yml
2−name: Cabal CI
3−
4−on:
5− push:
6− branches:
7− - '**'
8− paths-ignore: []
9− pull_request:
10− paths-ignore: []
11−
12−jobs:
13− linux:
14−
15− runs-on: ubuntu-20.04
16− strategy:
17− fail-fast: false
18− matrix:
19− versions:
20− - ghc: '8.8.4'
21− cabal: '3.4'
22− - ghc: '8.10.4'
23− cabal: '3.4'
24− steps:
25− - uses: actions/checkout@v2
26−
27− # need to install older cabal/ghc versions from ppa repository
28−
29− - name: Install recent cabal/ghc
30− uses: actions/setup-haskell@v1.1.3
31− with:
32− ghc-version: ${{ matrix.versions.ghc }}
33− cabal-version: ${{ matrix.versions.cabal }}
34−
35− # declare/restore cached things
36− # caching doesn't work for scheduled runs yet
37− # https://github.com/actions/cache/issues/63
38−
39− - name: Cache cabal global package db
40− id: cabal-global
41− uses: actions/cache@v2
42− with:
43− path: |
44− ~/.cabal
45− key: ${{ runner.os }}-${{ matrix.versions.ghc }}-${{ matrix.versions.cabal }}-cabal-global-${{ hashFiles('cabal.project') }}
46−
47− - name: Cache cabal work
48− id: cabal-local
49− uses: actions/cache@v2
50− with:
51− path: |
52− dist-newstyle
53− key: ${{ runner.os }}-${{ matrix.versions.ghc }}-${{ matrix.versions.cabal }}-cabal-local
54−
55− - name: Install dependencies
56− run: |
57− cabal update
58− cabal build all --dependencies-only --enable-tests --disable-optimization
59− - name: Build
60− run: |
61− cabal build all --enable-tests --disable-optimization 2>&1 | tee build.log
62− - name: Test
63− run: |
64− cabal test all --disable-optimization
.github/workflows/ci.yml+2−2
@@ -1,4 +1,4 @@
1−name: Stack CI
1+name: CI
22
33# Trigger the workflow on push or pull request, but only for the master branch
44on:
@@ -12,7 +12,7 @@ jobs:
1212 strategy:
1313 matrix:
1414 stack: ["2.3.3"]
15− ghc: ["8.10.4"]
15+ ghc: ["8.8.3"]
1616 os: [ubuntu-latest]
1717 steps:
1818 - uses: actions/checkout@v2
README.md+1−2
@@ -2,8 +2,7 @@
22
33Automatically fuzz your servant apis in a contextually-aware way.
44
5−[![Stack CI](https://github.com/mwotton/roboservant/actions/workflows/ci.yml/badge.svg)](https://github.com/mwotton/roboservant/actions/workflows/ci.yml)
6−[![Cabal CI](https://github.com/mwotton/roboservant/actions/workflows/cabal.yml/badge.svg)](https://github.com/mwotton/roboservant/actions/workflows/cabal.yml)
5+![CI](https://github.com/mwotton/roboservant/workflows/CI/badge.svg)
76
87## example
98
package.yaml+1−2
@@ -1,6 +1,6 @@
11
22name: roboservant
3−version: 0.1.0.3
3+version: 0.1.0.2
44github: "mwotton/roboservant"
55license: BSD3
66copyright: "2020 Mark Wotton, Samuel Schlesinger"
@@ -19,7 +19,6 @@ dependencies:
1919- containers
2020- random
2121- hashable
22−- http-types
2322- lifted-base
2423- monad-control
2524- mtl
roboservant.cabal+4−10
@@ -1,13 +1,13 @@
11cabal-version: 1.12
22
3−-- This file has been generated from package.yaml by hpack version 0.35.0.
3+-- This file has been generated from package.yaml by hpack version 0.33.0.
44--
55-- see: https://github.com/sol/hpack
66--
7−-- hash: 0c0c35e1dca1d6c52e5bc61591cbd8c0da9d811b83341bbc676c5f8b93b0b20d
7+-- hash: 4c71b6b9460f4719791bb8e67f4a5398b807cd8d17c43dc69da806bb5ba9fb4b
88
99name: roboservant
10−version: 0.1.0.3
10+version: 0.1.0.2
1111synopsis: Automatic session-aware servant testing
1212description: Please see the README on GitHub at <https://github.com/mwotton/roboservant#readme>
1313category: Web
@@ -36,7 +36,6 @@ library
3636 Roboservant.Types.BuildFrom
3737 Roboservant.Types.Config
3838 Roboservant.Types.Internal
39− Roboservant.Types.Orphans
4039 Roboservant.Types.ReifiedApi
4140 Roboservant.Types.ReifiedApi.Server
4241 other-modules:
@@ -51,7 +50,6 @@ library
5150 , dependent-map
5251 , dependent-sum
5352 , hashable
54− , http-types
5553 , lifted-base
5654 , monad-control
5755 , mtl
@@ -73,7 +71,7 @@ test-suite example
7371 other-modules:
7472 Paths_roboservant
7573 hs-source-dirs:
76− ./
74+ ./.
7775 ghc-options: -Wall -fwrite-ide-info -hiedir=.hie -pgmL markdown-unlit
7876 build-tool-depends:
7977 markdown-unlit:markdown-unlit
@@ -88,7 +86,6 @@ test-suite example
8886 , hspec
8987 , hspec-core
9088 , http-client
91− , http-types
9289 , lifted-base
9390 , monad-control
9491 , mtl
@@ -116,8 +113,6 @@ test-suite roboservant-test
116113 Nested
117114 Post
118115 Product
119− Put
120− QueryParams
121116 Seeded
122117 UnsafeIO
123118 Valid
@@ -138,7 +133,6 @@ test-suite roboservant-test
138133 , hspec-wai
139134 , http-api-data
140135 , http-client
141− , http-types
142136 , lifted-base
143137 , monad-control
144138 , mtl
src/Roboservant/Client.hs+10−20
@@ -1,18 +1,18 @@
11{-# LANGUAGE RankNTypes #-}
22{-# LANGUAGE TypeOperators #-}
3−{-# LANGUAGE NamedFieldPuns #-}
43{-# LANGUAGE DataKinds #-}
54{-# LANGUAGE AllowAmbiguousTypes #-}
65{-# LANGUAGE DataKinds #-}
6+{-# LANGUAGE NamedFieldPuns#-}
77{-# LANGUAGE GADTs #-}
88{-# LANGUAGE PolyKinds #-}
99{-# LANGUAGE ScopedTypeVariables #-}
1010{-# LANGUAGE TypeApplications #-}
11−
11+{-# LANGUAGE TypeOperators #-}
1212{-# LANGUAGE UndecidableInstances #-}
1313{-# LANGUAGE TypeFamilies #-}
1414{-# LANGUAGE FlexibleContexts #-}
15−-- should all the NormalizeFunction instances be in one place?
15+-- should all the NormalizeFunction instances be in one place?
1616{-# OPTIONS_GHC -fno-warn-orphans #-}
1717module Roboservant.Client where
1818
@@ -28,14 +28,13 @@ import qualified Data.Vinyl.Curry as V
2828import qualified Data.Text as T
2929import Control.Monad.Reader
3030import Data.Hashable
31−import Network.HTTP.Types.Status
3231
3332-- fuzz :: forall api.
3433-- (FlattenServer api, ToReifiedApi (Endpoints api)) =>
3534-- Server api ->
3635-- Config ->
3736-- IO (Maybe Report)
38−-- fuzz s = fuzz' (reifyServer s)
37+-- fuzz s = fuzz' (reifyServer s)
3938-- -- todo: how do we pull reifyServer out?
4039-- where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api))
4140-- => Server api -> ReifiedApi
@@ -43,10 +42,7 @@ import Network.HTTP.Types.Status
4342
4443fuzz :: forall api . (ToReifiedClientApi (Endpoints api), FlattenClient api, HasClient ClientM api)
4544 => ClientEnv -> Config -> IO (Maybe Report)
46−fuzz clientEnv
47− = fuzz'
48− (toReifiedClientApi
49− (flattenClient @api apiClient) (Proxy @(Endpoints api)) clientEnv)
45+fuzz clientEnv config = fuzz' (toReifiedClientApi (flattenClient @api apiClient) (Proxy @(Endpoints api)) clientEnv ) config
5046 where apiClient = client (Proxy @api)
5147
5248
@@ -82,7 +78,7 @@ instance
8278 foo :: V.Curried (EndpointArgs endpoint) (ReaderT ClientEnv IO ResultType)
8379 -> V.Curried (EndpointArgs endpoint) (IO ResultType)
8480 foo = mapCurried @(EndpointArgs endpoint) @(ReaderT ClientEnv IO ResultType) (`runReaderT` clientEnv)
85−
81+
8682mapCurried :: forall ts a b. V.RecordCurry' ts => (a -> b) -> V.Curried ts a -> V.Curried ts b
8783mapCurried f g = V.rcurry' @ts $ f . V.runcurry' g
8884
@@ -92,15 +88,10 @@ type ResultType = Either InteractionError (NonEmpty (Dynamic,Int))
9288
9389instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (ClientM x) where
9490 type Normal (ClientM x) = ReaderT ClientEnv IO (Either InteractionError (NonEmpty (Dynamic,Int)))
95− normalize c = ReaderT $
96− fmap (bimap renderClientError breakdown) . runClientM c
97− where
98− renderClientError :: ClientError -> InteractionError
99− renderClientError err = case err of
100− FailureResponse _ Response{responseStatusCode} -> InteractionError textual (responseStatusCode == status500)
101− _ -> InteractionError textual True
91+ normalize c = ReaderT $
92+ fmap (bimap (InteractionError . T.pack . show) breakdown) . runClientM c
93+--
10294
103− where textual = T.pack $ show err
10495instance ToReifiedClientApi '[] where
10596 toReifiedClientApi NoClientEndpoints _ _ = []
10697
@@ -112,7 +103,7 @@ instance
112103 FlattenClient (endpoint :<|> api)
113104 where
114105 flattenClient (endpoint :<|> c) = endpoint `AClientEndpoint` flattenClient @api c
115−
106+
116107instance
117108 (
118109 Endpoints api ~ '[api]
@@ -121,7 +112,6 @@ instance
121112 where
122113 flattenClient c = c `AClientEndpoint` NoClientEndpoints
123114
124−
125115instance FlattenClient (Verb method statusCode contentTypes responseType)
126116 where
127117 flattenClient c = c `AClientEndpoint` NoClientEndpoints
src/Roboservant/Direct.hs+14−16
@@ -7,7 +7,7 @@
77{-# LANGUAGE RecordWildCards #-}
88{-# LANGUAGE ScopedTypeVariables #-}
99{-# LANGUAGE TupleSections #-}
10−
10+{-# LANGUAGE TypeApplications #-}
1111{-# LANGUAGE TypeFamilies #-}
1212{-# LANGUAGE UndecidableInstances #-}
1313
@@ -56,7 +56,7 @@ import GHC.Generics ((:*:) (..))
5656import Roboservant.Types
5757 ( ApiOffset (..),
5858 Argument (..),
59− InteractionError(..),
59+ InteractionError,
6060 Provenance (..),
6161 ReifiedApi,
6262 ReifiedEndpoint (..),
@@ -150,12 +150,12 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
150150 Report
151151 (unlines [show failureType, show exception])
152152 r
153− displayDiagnostics FuzzState {..} = liftIO $
153+ displayDiagnostics FuzzState {..} = liftIO $ do
154154 logInfo $ unlines $
155− ["api endpoints covered"]
156− <> (map show . Set.toList . Set.fromList $ map apiOffset path)
157− <> ["", "types in stash"]
158− <> DM.foldrWithKey (\_ v r -> (show . NEL.length . getStashValue $ v) : r) [] (getStash stash)
155+ ["api endpoints covered"]
156+ <> (map show . Set.toList . Set.fromList $ map apiOffset path)
157+ <> ["", "types in stash"]
158+ <> DM.foldrWithKey (\_ v r -> (show . NEL.length . getStashValue $ v) : r) [] (getStash stash)
159159 -- <> (map (show . NEL.length . getStashValue ) $ DM.assocs (getStash stash))
160160 -- $ \_k v ->
161161 -- (show . NEL.length $ getStashValue v))
@@ -242,15 +242,12 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
242242 execute fuzzop func args = do
243243 (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
244244 liftIO (V.runcurry' func argVals) >>= \case
245− Left (e::InteractionError) ->
246− if fatalError e
247− then throw e
248− else pure ()
249− Right (dyn :: NEL.NonEmpty (Dynamic, Int)) ->
245+ Left (e::InteractionError) -> throw e
246+ Right (dyn :: NEL.NonEmpty (Dynamic, Int)) -> do
250247 modify'
251− ( \fs@FuzzState {..} ->
252− fs {stash = addToStash (NEL.toList dyn) stash}
253− )
248+ ( \fs@FuzzState {..} ->
249+ fs {stash = addToStash (NEL.toList dyn) stash}
250+ )
254251 where
255252 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
256253 -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
@@ -262,7 +259,8 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
262259 (execute op func args)
263260 [ Handler (\(e :: SomeAsyncException) -> throw e),
264261 Handler
265− ( \(e :: SomeException) ->
262+ ( \(e :: SomeException) -> do
263+ -- displayDiagnostics =<< get
266264 throw . RoboservantException ServerCrashed (Just e) =<< get
267265 )
268266 ]
src/Roboservant/Server.hs+6−2
@@ -4,9 +4,12 @@
44{-# LANGUAGE AllowAmbiguousTypes #-}
55{-# LANGUAGE FlexibleContexts #-}
66{-# LANGUAGE GADTs #-}
7+{-# LANGUAGE LambdaCase #-}
78{-# LANGUAGE PolyKinds #-}
8−
9+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE RecordWildCards #-}
911{-# LANGUAGE ScopedTypeVariables #-}
12+{-# LANGUAGE TupleSections #-}
1013{-# LANGUAGE TypeApplications #-}
1114{-# LANGUAGE TypeFamilies #-}
1215{-# LANGUAGE UndecidableInstances #-}
@@ -27,10 +30,11 @@ fuzz :: forall api.
2730 Server api ->
2831 Config ->
2932 IO (Maybe Report)
30−fuzz s = fuzz' (reifyServer s)
33+fuzz s = fuzz' (reifyServer s)
3134 -- todo: how do we pull reifyServer out?
3235 where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api))
3336 => Server api -> ReifiedApi
3437 reifyServer server = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
3538-- reifyServer server = toReifiedApi server (Proxy @(Endpoints api))
3639
40+
src/Roboservant/Types.hs−1
@@ -23,4 +23,3 @@ import Roboservant.Types.Config
2323import Roboservant.Types.Internal
2424import Roboservant.Types.ReifiedApi
2525import Roboservant.Types.ReifiedApi.Server
26−import Roboservant.Types.Orphans()
src/Roboservant/Types/Breakdown.hs+1−6
@@ -1,6 +1,5 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
22{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE FlexibleContexts #-}
43{-# LANGUAGE DerivingVia #-}
54{-# LANGUAGE FlexibleInstances #-}
65{-# LANGUAGE GADTs #-}
@@ -23,8 +22,6 @@ import qualified Data.List.NonEmpty as NEL
2322import Data.Typeable (Typeable)
2423import GHC.Generics
2524import Roboservant.Types.Internal
26−import Servant
27−import Roboservant.Types.Orphans()
2825
2926breakdown ::
3027 (Hashable x, Typeable x, Breakdown x) =>
@@ -49,7 +46,7 @@ instance (Hashable x, Typeable x, Breakdown x) => Breakdown [x] where
4946 breakdownExtras stash = concatMap (NEL.toList . breakdown) stash
5047
5148
52−class GBreakdown (f :: k -> Type) where
49+class GBreakdown (f :: k -> *) where
5350 gBreakdownExtras :: f a -> [(Dynamic, Int)]
5451
5552instance (Hashable x, Typeable x, Generic x, GBreakdown (Rep x)) => Breakdown (Compound (x :: Type)) where
@@ -77,5 +74,3 @@ instance (Hashable a, Typeable a, Breakdown a) => GBreakdown (K1 R a) where
7774
7875instance GBreakdown U1 where
7976 gBreakdownExtras U1 = []
80−
81−deriving via (Atom NoContent) instance Breakdown NoContent
src/Roboservant/Types/BuildFrom.hs+4−10
@@ -12,7 +12,6 @@
1212
1313module Roboservant.Types.BuildFrom where
1414
15−import Data.List(nub)
1615import qualified Data.Dependent.Map as DM
1716import Data.Hashable
1817import qualified Data.IntSet as IntSet
@@ -23,8 +22,6 @@ import Data.Typeable (Typeable)
2322import GHC.Generics
2423import Roboservant.Types.Internal
2524import qualified Type.Reflection as R
26−import Servant(NoContent)
27−import Roboservant.Types.Orphans()
2825
2926buildFrom :: forall x. (Hashable x, BuildFrom x, Typeable x) => Stash -> Maybe (StashValue x)
3027buildFrom = buildStash . buildFrom'
@@ -62,12 +59,11 @@ deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, BuildFrom x)
6259
6360-- this isn't wonderful, but we need a hand-rolled instance for recursive datatypes right now.
6461-- with an arbitrary-ish interface, we could use a size parameter, rng access etc.
65−instance (Eq x, BuildFrom x) => BuildFrom [x] where
66− extras stash =
67− nub $ map (\xs -> (concatMap fst xs, map snd xs)) $ notpowerset $ buildFrom' @x stash
62+instance (BuildFrom x) => BuildFrom [x] where
63+ extras stash = map (\xs -> (concatMap fst xs, map snd xs)) $ notpowerset $ extras @x stash
6864 where
6965 -- powerset creates way too much stuff. something better here eventually.
70− notpowerset xs = []:xs:map pure xs
66+ notpowerset xs = xs:map pure xs
7167
7268
7369instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x :: Type)) where
@@ -77,7 +73,7 @@ deriving via (Atom Int) instance BuildFrom Int
7773
7874deriving via (Atom Char) instance BuildFrom Char
7975
80−class GBuildFrom (f :: k -> Type) where
76+class GBuildFrom (f :: k -> *) where
8177 gExtras :: Stash -> [([Provenance], f a)]
8278
8379instance GBuildFrom b => GBuildFrom (M1 D a b) where
@@ -103,5 +99,3 @@ instance BuildFrom a => GBuildFrom (K1 i a) where
10399
104100instance GBuildFrom U1 where
105101 gExtras _ = [([], U1)]
106−
107−deriving via (Atom NoContent) instance BuildFrom NoContent
src/Roboservant/Types/Internal.hs+2−2
@@ -43,11 +43,11 @@ instance Show Stash where
4343
4444-- | Can't be built up from parts, can't be broken down further.
4545newtype Atom x = Atom {unAtom :: x}
46− deriving newtype (Hashable, Typeable,Eq)
46+ deriving newtype (Hashable, Typeable)
4747
4848-- | can be broken down and built up from generic pieces
4949newtype Compound x = Compound {unCompound :: x}
50− deriving newtype (Hashable, Typeable, Eq)
50+ deriving newtype (Hashable, Typeable)
5151
5252hashedDyn :: (Hashable a, Typeable a) => a -> (Dynamic, Int)
5353hashedDyn a = (toDyn a, hash a)
src/Roboservant/Types/Orphans.hsdeleted−7
@@ -1,7 +0,0 @@
1−{-# OPTIONS_GHC -fno-warn-orphans #-}
2−module Roboservant.Types.Orphans where
3−
4−import Servant
5−import Data.Hashable
6−
7−instance Hashable NoContent
src/Roboservant/Types/ReifiedApi.hs+4−43
@@ -28,7 +28,7 @@ import GHC.Generics ((:*:)(..))
2828import Roboservant.Types.Internal
2929import Roboservant.Types.Breakdown
3030import Roboservant.Types.BuildFrom
31−import Data.Kind(Type)
31+import Data.Kind
3232import Servant
3333import Servant.API.Modifiers(FoldRequired,FoldLenient)
3434import GHC.TypeLits (Symbol)
@@ -52,12 +52,9 @@ data ReifiedEndpoint = forall as. (V.RecordToList as, V.RMap as) => ReifiedEndpo
5252 , reEndpointFunc :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
5353 }
5454
55−instance Show ReifiedEndpoint where
56− show _ = "lol"
57−
5855class ( V.RecordToList (EndpointArgs endpoint)
5956 , V.RMap (EndpointArgs endpoint)
60− ) => ToReifiedEndpoint (endpoint :: Type) where
57+ ) => ToReifiedEndpoint (endpoint :: *) where
6158 type EndpointArgs endpoint :: [Type]
6259 type EndpointRes endpoint :: Type
6360
@@ -67,15 +64,13 @@ class ( V.RecordToList (EndpointArgs endpoint)
6764tagType :: Typeable a => f a -> TypedF f a
6865tagType = (R.typeRep :*:)
6966
70−data InteractionError = InteractionError
71− { errorMessage :: T.Text
72− , fatalError :: Bool
73− }
67+newtype InteractionError = InteractionError T.Text
7468 deriving Show
7569instance Exception InteractionError
7670
7771
7872
73+
7974instance
8075 (Typeable responseType, Breakdown responseType) =>
8176 ToReifiedEndpoint (Verb method statusCode contentTypes responseType)
@@ -84,12 +79,6 @@ instance
8479 type EndpointRes (Verb method statusCode contentTypes responseType) = responseType
8580 reifiedEndpointArguments = V.RNil
8681
87−instance ToReifiedEndpoint (NoContentVerb method)
88− where
89− type EndpointArgs (NoContentVerb method) = '[]
90− type EndpointRes (NoContentVerb method) = NoContent
91− reifiedEndpointArguments = V.RNil
92−
9382instance
9483 (ToReifiedEndpoint endpoint) =>
9584 ToReifiedEndpoint ((x :: Symbol) :> endpoint)
@@ -98,16 +87,6 @@ instance
9887 type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
9988 reifiedEndpointArguments = reifiedEndpointArguments @endpoint
10089
101−instance
102− (ToReifiedEndpoint endpoint) =>
103− ToReifiedEndpoint (RemoteHost :> endpoint)
104− where
105− type EndpointArgs (RemoteHost :> endpoint) = EndpointArgs endpoint
106− type EndpointRes (RemoteHost :> endpoint) = EndpointRes endpoint
107− reifiedEndpointArguments = reifiedEndpointArguments @endpoint
108−
109−
110−
11190instance
11291 (ToReifiedEndpoint endpoint) =>
11392 ToReifiedEndpoint (Description s :> endpoint)
@@ -150,24 +129,6 @@ instance
150129 tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
151130 V.:& reifiedEndpointArguments @endpoint
152131
153−
154−instance
155− ( BuildFrom paramType
156− , ToReifiedEndpoint endpoint
157− , Show paramType
158− , Eq paramType
159− ) =>
160− ToReifiedEndpoint (QueryParams name paramType :> endpoint)
161− where
162− type EndpointArgs (QueryParams name paramType :> endpoint) = [paramType] ': EndpointArgs endpoint
163− type EndpointRes (QueryParams name paramType :> endpoint) = EndpointRes endpoint
164− reifiedEndpointArguments =
165− tagType (Argument (buildFrom @[paramType]))
166− V.:& reifiedEndpointArguments @endpoint
167−
168−
169−
170−
171132instance
172133 ( BuildFrom (IfRequiredLenient T.Text mods headerType)
173134 , ToReifiedEndpoint endpoint
src/Roboservant/Types/ReifiedApi/Server.hs+5−4
@@ -6,8 +6,10 @@
66{-# LANGUAGE GADTs #-}
77{-# LANGUAGE LambdaCase #-}
88{-# LANGUAGE PolyKinds #-}
9−
9+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE RecordWildCards #-}
1011{-# LANGUAGE ScopedTypeVariables #-}
12+{-# LANGUAGE TupleSections #-}
1113{-# LANGUAGE TypeApplications #-}
1214{-# LANGUAGE TypeFamilies #-}
1315{-# LANGUAGE UndecidableInstances #-}
@@ -66,8 +68,7 @@ instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x)
6668 where
6769 -- | TODO improve this
6870 renderServerError :: ServerError -> InteractionError
69− renderServerError s = InteractionError (T.pack $ show s) (errHTTPCode serverError == 500)
70−
71+ renderServerError = InteractionError . T.pack . show
7172 Right x -> pure $ Right $ breakdown x
7273
7374
@@ -92,7 +93,7 @@ instance
9293 FlattenServer (endpoint :<|> api)
9394 where
9495 flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
95−
96+
9697instance
9798 (
9899 Endpoints api ~ '[api]
stack.yaml+10−5
@@ -1,10 +1,15 @@
1−resolver: lts-20.4
1+resolver: lts-16.27
22packages:
33- .
44
55extra-deps:
6−# - dependent-map-0.4.0.0@sha256:ca2b131046f4340a1c35d138c5a003fe4a5be96b14efc26291ed35fd08c62221,1657
7−# - dependent-sum-0.7.1.0@sha256:5599aa89637db434431b1dd3fa7c34bc3d565ee44f0519bfbc877be1927c2531,2068
86- servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
9−# - constraints-extras-0.3.0.2@sha256:013b8d0392582c6ca068e226718a4fe8be8e22321cc0634f6115505bf377ad26,1853
10−- some-1.0.3
7+- vinyl-0.13.0
8+- dependent-sum-0.7.1.0
9+- constraints-extras-0.3.0.2
10+- dependent-map-0.4.0.0
11+
12+- servant-0.18.2
13+- servant-client-0.18.2
14+- servant-client-core-0.18.2
15+- servant-server-0.18.2
stack.yaml.lock+58−9
@@ -7,20 +7,69 @@ packages:
77- completed:
88 hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
99 pantry-tree:
10− sha256: 04f12c7bef2c3f9a25d94eb9489752ed498db8e243069fe95838dbb51df1dcb3
1110 size: 325
11+ sha256: 04f12c7bef2c3f9a25d94eb9489752ed498db8e243069fe95838dbb51df1dcb3
1212 original:
1313 hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
1414- completed:
15− hackage: some-1.0.3@sha256:938cec5fc0d3a08dde34cfc0acd5a0aa2e9fd489c9dbb64417f16588dfb47af0,2399
15+ hackage: vinyl-0.13.0@sha256:0f247cd3f8682b30881a07de18e6fec52d540646fbcb328420049cc8d63cd407,3724
16+ pantry-tree:
17+ size: 1857
18+ sha256: 860fb95820b595161cdbdec5f376100ebae2d14e5ef0dbe311546202f7525d01
19+ original:
20+ hackage: vinyl-0.13.0
21+- completed:
22+ hackage: dependent-sum-0.7.1.0@sha256:5599aa89637db434431b1dd3fa7c34bc3d565ee44f0519bfbc877be1927c2531,2068
23+ pantry-tree:
24+ size: 290
25+ sha256: 9cbfb32b5a8a782b7a1c941803fd517633cb699159b851c1d82267a9e9391b50
26+ original:
27+ hackage: dependent-sum-0.7.1.0
28+- completed:
29+ hackage: constraints-extras-0.3.0.2@sha256:013b8d0392582c6ca068e226718a4fe8be8e22321cc0634f6115505bf377ad26,1853
30+ pantry-tree:
31+ size: 594
32+ sha256: 3ce1012bfb02e4d7def9df19ce80b8cd2b472c691b25b181d9960638673fecd1
33+ original:
34+ hackage: constraints-extras-0.3.0.2
35+- completed:
36+ hackage: dependent-map-0.4.0.0@sha256:ca2b131046f4340a1c35d138c5a003fe4a5be96b14efc26291ed35fd08c62221,1657
37+ pantry-tree:
38+ size: 551
39+ sha256: 5defa30010904d2ad05a036f3eaf83793506717c93cbeb599f40db1a3632cfc5
40+ original:
41+ hackage: dependent-map-0.4.0.0
42+- completed:
43+ hackage: servant-0.18.2@sha256:f8c9f0e9891a3ada1337a3c0b369333a3b5a2d0909dd3cd09d79bc26adeaca44,5298
44+ pantry-tree:
45+ size: 2662
46+ sha256: e930e814de1aa4d24274bdf18341a50b7ed38604ae4734f730e09238ac5bf7e2
47+ original:
48+ hackage: servant-0.18.2
49+- completed:
50+ hackage: servant-client-0.18.2@sha256:82578ade7468873259bb2fdc9d62290a0f998550900683e1410a237ed4b05410,4591
51+ pantry-tree:
52+ size: 1300
53+ sha256: 6324892c77bedbce32f0d6f1612fc2cb0d82c163d3be39efb951d3cc3792ce4a
54+ original:
55+ hackage: servant-client-0.18.2
56+- completed:
57+ hackage: servant-client-core-0.18.2@sha256:ad63ae0f227373fea7e547d4c2a7b0b69e112ff409a83cbadffc9f6ee049926f,3763
58+ pantry-tree:
59+ size: 1444
60+ sha256: 9e37bc5f8cbb70cf1accb20bd6f83fca4c2ca42472cd0fc4b22183c2c57cbe3b
61+ original:
62+ hackage: servant-client-core-0.18.2
63+- completed:
64+ hackage: servant-server-0.18.2@sha256:56679af62ab8820a2108da6153d9ae9dde37199e62172365bdaea1458c3f7c2d,5482
1665 pantry-tree:
17− sha256: b88dddc6b991cd570c6750f8d309a9feb76938b29a690abdabc4ababb8c7b2f8
18− size: 709
66+ size: 2614
67+ sha256: 3ac7430134439e4b67f0f5333f63b89d0cb7de5e2e07f0af7801c8e223942b9c
1968 original:
20− hackage: some-1.0.3
69+ hackage: servant-server-0.18.2
2170snapshots:
2271- completed:
23− sha256: 3770dfd79f5aed67acdcc65c4e7730adddffe6dba79ea723cfb0918356fc0f94
24− size: 648660
25− url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/4.yaml
26− original: lts-20.4
72+ size: 533252
73+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/27.yaml
74+ sha256: c2aaae52beeacf6a5727c1010f50e89d03869abfab6d2c2658ade9da8ed50c73
75+ original: lts-16.27
test/Breakdown.hs+1−1
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
test/Nested.hs+3−3
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
2−
2+{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
@@ -11,7 +11,7 @@ import Servant
1111import Servant.API.Flatten
1212
1313type Api =
14− ( "one" :> Summary "foo" :> Post '[JSON] Int
14+ ( "one" :> Post '[JSON] Int
1515 :<|> "two" :> Post '[JSON] Int
1616 )
1717 :<|> ( "three" :> Post '[JSON] Int
test/Post.hs+2−2
@@ -1,8 +1,8 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
5−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
88module Post where
test/Product.hs+1−1
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
test/Put.hsdeleted−33
@@ -1,33 +0,0 @@
1−{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE DeriveGeneric #-}
3−{-# LANGUAGE DerivingStrategies #-}
4−
5−
6−{-# LANGUAGE TypeOperators #-}
7−
8−module Put where
9−
10−import Data.Aeson
11−import Data.Hashable
12−import GHC.Generics (Generic)
13−import Servant
14−
15−type Api =
16− Get '[JSON] Foo
17− :<|> ReqBody '[JSON] Foo :> Put '[JSON] ()
18− :<|> ReqBody '[JSON] Foo :> Put '[JSON] NoContent
19−
20−data Foo = Foo
21− deriving (Eq, Show, Generic)
22−
23−instance Hashable Foo
24−
25−instance ToJSON Foo
26−
27−instance FromJSON Foo
28−
29−server :: Server Api
30−server =
31− pure Foo
32− :<|> const (pure ())
33− :<|> const (pure NoContent)
test/QueryParams.hsdeleted−15
@@ -1,15 +0,0 @@
1−{-# LANGUAGE DataKinds #-}
2−
3−{-# LANGUAGE DerivingStrategies #-}
4−
5−
6−{-# LANGUAGE TypeOperators #-}
7−
8−module QueryParams where
9−
10−import Servant
11−
12−type Api = QueryParams "ints" Int :> Get '[JSON] [Int]
13−
14−server :: Server Api
15−server = pure
test/Spec.hs+24−39
@@ -21,9 +21,7 @@ import qualified Foo
2121import qualified Headers
2222import qualified Nested
2323import qualified Post
24−import qualified Put
2524import qualified Product
26−import qualified QueryParams
2725import qualified Roboservant as R
2826import qualified Roboservant.Server as RS
2927import qualified Roboservant.Client as RC
@@ -31,32 +29,31 @@ import qualified Seeded
3129import Test.Hspec
3230import Test.Hspec.Core.Spec (FailureReason (Reason), ResultStatus (Failure, Success), itemExample, mapSpecItem_, resultStatus)
3331import qualified Valid
34−import Servant ( Server, Proxy(..), serve, Endpoints, HasServer )
35−
32+import Servant(Server,Proxy(..), serve)
3633import Servant.Client(ClientEnv, mkClientEnv, baseUrlPort, parseBaseUrl,HasClient,ClientM)
3734import Network.Wai(Application)
3835import qualified Network.Wai.Handler.Warp as Warp
3936import Network.HTTP.Client (newManager, defaultManagerSettings)
40−import Control.Monad((>=>))
37+import Servant(Endpoints,HasServer)
4138
4239main :: IO ()
4340main = hspec spec
4441
4542fuzzBoth
46− :: forall a .
43+ :: forall a .
4744 (R.ToReifiedApi (Endpoints a), HasServer a '[], RS.FlattenServer a, RC.ToReifiedClientApi (Endpoints a), RC.FlattenClient a,
4845 HasClient ClientM a)
4946 => String -> Server a -> R.Config -> (Maybe R.Report -> IO ()) -> Spec
5047fuzzBoth name server config condition = do
51− it (name <> " via server") $
48+ it (name <> " via server") $ do
5249 RS.fuzz @a server config >>= condition
5350
54− around (withServer (serve (Proxy :: Proxy a) server)) $
51+ around (withServer (serve (Proxy :: Proxy a) server)) $ do
5552 it (name <> " via client") $ \(clientEnv::ClientEnv) -> do
56− RC.fuzz @a clientEnv config >>= condition
53+ RC.fuzz @a clientEnv config >>= condition
5754
5855withServer :: Application -> ActionWith ClientEnv -> IO ()
59−withServer app action = Warp.testWithApplication (pure app) (genClientEnv >=> action)
56+withServer app action = Warp.testWithApplication (pure app) (\p -> genClientEnv p >>= action)
6057 where genClientEnv port = do
6158 baseUrl <- parseBaseUrl "http://localhost"
6259 manager <- newManager defaultManagerSettings
@@ -75,45 +72,37 @@ spec = do
7572 Just (R.InsufficientCoverage _) -> True
7673 _ -> False
7774 ))
78− describe "posted body" $
75+ describe "posted body" $ do
7976 fuzzBoth @Post.Api "passes a coverage check using a posted body" Post.server R.defaultConfig {R.coverageThreshold = 0.99}
80− (`shouldSatisfy` isNothing)
81−
82−
83− describe "PUTted body" $
84− fuzzBoth @Put.Api "passes a coverage check using a posted body" Put.server R.defaultConfig {R.coverageThreshold = 0.99}
85− (`shouldSatisfy` isNothing)
86−
87−
77+ (`shouldSatisfy` isNothing)
78+
8879 describe "seeded" $ do
8980 let res = Seeded.Seed 1
9081 shouldFail $ fuzzBoth @Seeded.Api "finds an error using information passed in" Seeded.server
9182 (R.defaultConfig {R.seed = [(toDyn res, hash res)]})
9283 (`shouldSatisfy` isNothing)
93−
94− describe "Foo" $
84+
85+ describe "Foo" $ do
9586 fuzzBoth @Foo.Api "finds an error in a basic app" Foo.server R.defaultConfig (`shouldSatisfy` serverFailure)
9687
97− describe "QueryParams" $
98− fuzzBoth @QueryParams.Api "can handle query params" QueryParams.server R.defaultConfig { R.seed = [R.hashedDyn (12::Int)] }
99− (`shouldSatisfy` isNothing)
100−
10188 describe "BuildFrom" $ do
102− describe "headers (and sum types)" $
89+ describe "headers (and sum types)" $ do
10390 fuzzBoth @Headers.Api "should find a failure that's dependent on using header info" Headers.server R.defaultConfig
104− (`shouldSatisfy` serverFailure)
105− describe "product types" $
106− fuzzBoth @Product.Api "should find a failure that's dependent on creating a product" Product.server
107− R.defaultConfig {R.seed = [R.hashedDyn 'a', R.hashedDyn (1 :: Int)]}
108− (`shouldSatisfy` serverFailure)
91+ (`shouldSatisfy` serverFailure)
92+ describe "product types" $ do
93+ fuzzBoth @Product.Api "should find a failure that's dependent on creating a product" Product.server
94+ R.defaultConfig {R.seed = [R.hashedDyn 'a', R.hashedDyn (1 :: Int)]}
95+ (`shouldSatisfy` serverFailure)
10996 describe "Breakdown" $ do
11097 fuzzBoth @Breakdown.ProductApi "handles products" Breakdown.productServer R.defaultConfig
11198 (`shouldSatisfy` serverFailure)
11299 fuzzBoth @Breakdown.SumApi "handles sums" Breakdown.sumServer R.defaultConfig
113100 (`shouldSatisfy` serverFailure)
114− describe "flattening" $
101+ describe "flattening" $ do
102+ -- we don't actually do much here, this is just here to document the appropriate response
103+ -- if you get a type error with a nested api.
115104 fuzzBoth @Nested.FlatApi "can handle nested apis" Nested.server R.defaultConfig {R.coverageThreshold = 0.99}
116− (`shouldSatisfy` isNothing)
105+ (`shouldSatisfy` isNothing)
117106
118107serverFailure :: Maybe R.Report -> Bool
119108serverFailure = \case
@@ -137,12 +126,8 @@ deriving via (R.Atom Seeded.Seed) instance R.BuildFrom Seeded.Seed
137126deriving via (R.Atom Void) instance R.BuildFrom Void
138127
139128deriving via (R.Atom Post.FooPost) instance R.Breakdown Post.FooPost
140−deriving via (R.Atom Post.FooPost) instance R.BuildFrom Post.FooPost
141−
142−deriving via (R.Atom Put.Foo) instance R.Breakdown Put.Foo
143−deriving via (R.Atom Put.Foo) instance R.BuildFrom Put.Foo
144−
145129
130+deriving via (R.Atom Post.FooPost) instance R.BuildFrom Post.FooPost
146131
147132deriving via (R.Compound Breakdown.Foo) instance R.Breakdown Breakdown.Foo
148133
@@ -158,7 +143,7 @@ shouldFail =
158143 ( \i ->
159144 i
160145 { itemExample = \p a cb -> do
161− r <- itemExample i p a cb
146+ r <- (itemExample i) p a cb
162147 pure
163148 r
164149 { resultStatus = case resultStatus r of
test/UnsafeIO.hs+3−3
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
2−
2+{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
@@ -22,7 +22,7 @@ healthcheck ref = do
2222 t <- liftIO $ readIORef ref
2323 case t of
2424 0 -> pure ()
25− n -> throwError $ err500 {errBody = "observed inconsistency: " <> BL8.pack (show n)}
25+ n -> throwError $ err500 {errBody = "observed inconsistency: " <> (BL8.pack $ show n)}
2626
2727makeServer :: IO (Server UnsafeApi)
2828makeServer = do
test/Valid.hs+3−2
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
@@ -10,6 +10,7 @@ module Valid where
1010import Data.Void
1111import GHC.Generics
1212import Servant
13+import Servant.API.Generic
1314import Servant.Server.Generic
1415
1516type Api =
@@ -20,7 +21,7 @@ data Routes route
2021 = Routes
2122 { getInt ::
2223 route
23− :- Summary "i'm a summary!" :> Get '[JSON] Int,
24+ :- Get '[JSON] Int,
2425 captureIt ::
2526 route
2627 :- Capture "void" Void :> Get '[JSON] ()