on 53ab47e help sign in
31 files changed+374−752
since splitsnapshots
unifiedsplit
.github/workflows/cabal.yml−64 .github/workflows/ci.yml+2−2 EXAMPLE.md+3−40 README.md+1−2 benchmarks/genFile.sh+2−15 benchmarks/prelude+16−7 benchmarks/runTiming.sh+2−2 package.yaml+2−10 roboservant.cabal+6−20 src/Roboservant/Client.hs−127 src/Roboservant/Direct.hs+14−17 src/Roboservant/Server.hs−36 src/Roboservant/Types.hs+4−3 src/Roboservant/Types/Breakdown.hs+1−6 src/Roboservant/Types/BuildFrom.hs+4−10 src/Roboservant/Types/FlattenServer.hs+40 src/Roboservant/Types/Internal.hs+2−2 src/Roboservant/Types/Orphans.hs−7 src/Roboservant/Types/ReifiedApi.hs+3−168 src/Roboservant/Types/ReifiedApi/Server.hs+126−36 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+65−104 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
EXAMPLE.md+3−40
@@ -13,37 +13,23 @@ Our api under test:
1313{-# LANGUAGE DeriveGeneric #-}
1414{-# LANGUAGE GeneralizedNewtypeDeriving #-}
1515{-# LANGUAGE RecordWildCards #-}
16−{-# LANGUAGE ScopedTypeVariables #-}
17−{-# LANGUAGE TypeApplications #-}
1816
19−import qualified Roboservant.Server as RS
20−import qualified Roboservant.Client as RC
21−import Servant.Client(ClientEnv, baseUrlPort, parseBaseUrl, mkClientEnv)
22−import Network.HTTP.Client (newManager, defaultManagerSettings)
23−import Roboservant.Types
17+import Roboservant
2418import Test.Hspec
2519import Servant
2620import GHC.Generics
2721import Data.Typeable
2822import Data.Hashable
2923import Data.Maybe(isNothing, isJust)
30−import qualified Network.Wai.Handler.Warp as Warp
31−import Data.Aeson(FromJSON,ToJSON)
3224
3325newtype A = A Int
3426 deriving (Generic, Eq, Show, Typeable)
3527 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
3628
37−instance FromJSON A
38−instance ToJSON A
39−
4029newtype B = B Int
4130 deriving (Generic, Eq, Show, Typeable)
4231 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
4332
44−instance FromJSON B
45−instance ToJSON B
46−
4733type Api =
4834 "item" :> Get '[JSON] A
4935 :<|> "itemAdd" :> Capture "one" B :> Capture "two" B :> Get '[JSON] B
@@ -76,24 +62,13 @@ main = hspec spec
7662spec :: Spec
7763spec = describe "example" $ do
7864 it "good server should not fail" $ do
79− RS.fuzz @Api goodServer config
65+ fuzz @Api goodServer config
8066 >>= (`shouldSatisfy` isNothing)
8167 it "bad server should fail" $ do
82− RS.fuzz @Api badServer config
68+ fuzz @Api badServer config
8369 >>= (`shouldSatisfy` isJust)
8470```
8571
86−The previous test just picked apart the server and ran functions manually: sometimes, we want to test via
87−an honest-to-goodness network port, like so:
88−
89−```haskell
90− around (withServer (serve (Proxy :: Proxy Api) badServer)) $ do
91− it "we should also be able to run the _client_ to an independent server (ignore server error messages)" $ \(clientEnv::ClientEnv) -> do
92− RC.fuzz @Api clientEnv config >>= (`shouldSatisfy` isJust)
93−```
94−
95−(we use withApplication rather than testWithApplication because we don't primarily care what the server does here:
96−we want to check what a client does when presented with a faulty server.)
9772
9873We expect to be able to cover the whole api from our starting point, so let's set the coverage to 0.99.
9974There are other tweakable things in the config, like maximum runtime, reps,
@@ -129,15 +104,3 @@ build it up from components.
129104deriving via (Compound B) instance BuildFrom B
130105deriving via (Atom B) instance Breakdown B
131106```
132−
133−
134−test utilities:
135−
136−``` haskell
137−withServer :: Application -> ActionWith ClientEnv -> IO ()
138−withServer app action = Warp.withApplication (pure app) (\p -> genClientEnv p >>= action)
139− where genClientEnv port = do
140− baseUrl <- parseBaseUrl "http://localhost"
141− manager <- newManager defaultManagerSettings
142− pure $ mkClientEnv manager (baseUrl { baseUrlPort = port })
143−```
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
benchmarks/genFile.sh+2−15
@@ -1,11 +1,9 @@
11n=$1
2−k=$2
32
43cat prelude
5−echo -n "type SubApi = \"route1\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
4+echo -n "type Api = \"route1\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
65for i in $(seq 2 $n); do
7− echo -n " :<|> \"route${i}\" :> "
8− echo "ReqBody '[JSON] Input :> Post '[JSON] Output"
6+ echo " :<|> \"route${i}\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
97done
108
119echo -n "server = handler "
@@ -13,14 +11,3 @@ echo -n "server = handler "
1311for i in $(seq 2 $n); do
1412 echo -n ":<|> handler "
1513done
16−echo
17−echo
18−
19−echo -n "type Api = Flatten ("
20−for i in $(seq 1 $k); do
21− echo -n '"foo" :> '
22−done;
23−
24−echo "SubApi )"
25−
26−
benchmarks/prelude+16−7
@@ -12,7 +12,6 @@ import Servant
1212import Data.Hashable
1313import GHC.Generics
1414import Data.Aeson
15−import Servant.API.Flatten
1615
1716data Input = Input1 { foo :: String }
1817 | Input2 { bar :: Int }
@@ -53,9 +52,19 @@ handler = pure . \case
5352 Input1 foo -> Output1 foo
5453 Input2 bar -> Output2 bar
5554
56−main =
57− -- fuzz @Api server defaultConfig
58− pure ()
59−
60−f = flatten (Proxy @Api)
61−
55+main = fuzz @Api server defaultConfig (pure ())
56+
57+defaultConfig :: Config
58+defaultConfig = Config {
59+ -- you can pass extra values in using the seed argument. This can be useful
60+ -- for things that might not be produceable within the api, like auth tokens.
61+ seed = [hashedDyn "blah"]
62+ , maxRuntime = 0.5
63+ -- if we get to 1000 interactions with the api, call it quits.
64+ , maxReps = 1000
65+ -- if you're using this inside quickcheck or hedgehog, you might want to set this
66+ -- from their seed to make sure it stays deterministic
67+ , rngSeed = 0
68+ -- 0 to 100: fail tests if we hit less than this percentage of endpoints.
69+ , coverageThreshold = 0
70+ }
benchmarks/runTiming.sh+2−2
@@ -1,8 +1,8 @@
11lo=$1
22hi=$2
3−k=$3
3+
44for i in $(seq $lo 5 $hi); do
5− ./genFile.sh $i $k > main.hs;
5+ ./genFile.sh $i > main.hs;
66 echo "sievedn: $i,";
77 /usr/bin/time -f "%e" stack ghc main.hs 2>&1;
88# /usr/bin/time -f "%e" stack ghc -- -fomit-interface-pragmas main.hs 2>&1;
package.yaml+2−10
@@ -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
@@ -56,10 +55,7 @@ tests:
5655 - hspec
5756 - hspec-core
5857 - http-api-data
59− - http-client
60− - hspec-wai
61− - wai
62− - warp
58+
6359 example:
6460 main: Example.lhs
6561 source-dirs: .
@@ -67,10 +63,6 @@ tests:
6763 dependencies:
6864 - roboservant
6965 - hspec
70− - warp
71− - http-client
72− - aeson
73− - hspec-core
7466 ghc-options: -pgmL markdown-unlit
7567
7668 build-tools: markdown-unlit
roboservant.cabal+6−20
@@ -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: fa8c07f0c2fc10f18d3e925b8f702612c0c9fd81faa7942f901af388d509c231
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
@@ -28,15 +28,14 @@ source-repository head
2828library
2929 exposed-modules:
3030 Roboservant
31− Roboservant.Client
3231 Roboservant.Direct
3332 Roboservant.Server
3433 Roboservant.Types
3534 Roboservant.Types.Breakdown
3635 Roboservant.Types.BuildFrom
3736 Roboservant.Types.Config
37+ Roboservant.Types.FlattenServer
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,22 +71,18 @@ 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
8078 build-depends:
81− aeson
82− , base >=4.7 && <5
79+ base >=4.7 && <5
8380 , bytestring
8481 , containers
8582 , dependent-map
8683 , dependent-sum
8784 , hashable
8885 , hspec
89− , hspec-core
90− , http-client
91− , http-types
9286 , lifted-base
9387 , monad-control
9488 , mtl
@@ -103,7 +97,6 @@ test-suite example
10397 , time
10498 , unordered-containers
10599 , vinyl
106− , warp
107100 default-language: Haskell2010
108101
109102test-suite roboservant-test
@@ -116,8 +109,6 @@ test-suite roboservant-test
116109 Nested
117110 Post
118111 Product
119− Put
120− QueryParams
121112 Seeded
122113 UnsafeIO
123114 Valid
@@ -135,10 +126,7 @@ test-suite roboservant-test
135126 , hashable
136127 , hspec
137128 , hspec-core
138− , hspec-wai
139129 , http-api-data
140− , http-client
141− , http-types
142130 , lifted-base
143131 , monad-control
144132 , mtl
@@ -153,6 +141,4 @@ test-suite roboservant-test
153141 , time
154142 , unordered-containers
155143 , vinyl
156− , wai
157− , warp
158144 default-language: Haskell2010
src/Roboservant/Client.hsdeleted−127
@@ -1,127 +0,0 @@
1−{-# LANGUAGE RankNTypes #-}
2−{-# LANGUAGE TypeOperators #-}
3−{-# LANGUAGE NamedFieldPuns #-}
4−{-# LANGUAGE DataKinds #-}
5−{-# LANGUAGE AllowAmbiguousTypes #-}
6−{-# LANGUAGE DataKinds #-}
7−{-# LANGUAGE GADTs #-}
8−{-# LANGUAGE PolyKinds #-}
9−{-# LANGUAGE ScopedTypeVariables #-}
10−{-# LANGUAGE TypeApplications #-}
11−
12−{-# LANGUAGE UndecidableInstances #-}
13−{-# LANGUAGE TypeFamilies #-}
14−{-# LANGUAGE FlexibleContexts #-}
15−-- should all the NormalizeFunction instances be in one place?
16−{-# OPTIONS_GHC -fno-warn-orphans #-}
17−module Roboservant.Client where
18−
19−import Data.Proxy
20−import Servant.Client
21−import Roboservant.Types
22−import Roboservant(Report, fuzz')
23−import Servant
24−import Data.Bifunctor
25−import Data.List.NonEmpty (NonEmpty)
26−import Data.Dynamic (Dynamic,Typeable)
27−import qualified Data.Vinyl.Curry as V
28−import qualified Data.Text as T
29−import Control.Monad.Reader
30−import Data.Hashable
31−import Network.HTTP.Types.Status
32−
33−-- fuzz :: forall api.
34−-- (FlattenServer api, ToReifiedApi (Endpoints api)) =>
35−-- Server api ->
36−-- Config ->
37−-- IO (Maybe Report)
38−-- fuzz s = fuzz' (reifyServer s)
39−-- -- todo: how do we pull reifyServer out?
40−-- where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api))
41−-- => Server api -> ReifiedApi
42−-- reifyServer server = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
43−
44−fuzz :: forall api . (ToReifiedClientApi (Endpoints api), FlattenClient api, HasClient ClientM api)
45− => ClientEnv -> Config -> IO (Maybe Report)
46−fuzz clientEnv
47− = fuzz'
48− (toReifiedClientApi
49− (flattenClient @api apiClient) (Proxy @(Endpoints api)) clientEnv)
50− where apiClient = client (Proxy @api)
51−
52−
53−
54−class ToReifiedClientApi api where
55− toReifiedClientApi :: ClientBundled api -> Proxy api -> ClientEnv -> ReifiedApi
56−
57−data ClientBundled endpoints where
58− AClientEndpoint :: Client ClientM endpoint -> ClientBundled endpoints -> ClientBundled (endpoint ': endpoints)
59− NoClientEndpoints :: ClientBundled '[]
60−
61−
62−class FlattenClient api where
63− flattenClient :: Client ClientM api -> ClientBundled (Endpoints api)
64−
65−instance
66− ( NormalizeFunction (Client ClientM endpoint)
67− , Normal (Client ClientM endpoint) ~ V.Curried (EndpointArgs endpoint) (ReaderT ClientEnv IO (Either InteractionError (NonEmpty (Dynamic,Int))))
68− , ToReifiedClientApi endpoints
69− , V.RecordCurry' (EndpointArgs endpoint)
70− , ToReifiedEndpoint endpoint) =>
71− ToReifiedClientApi (endpoint : endpoints) where
72− toReifiedClientApi (endpoint `AClientEndpoint` endpoints) _ clientEnv =
73− (0, ReifiedEndpoint
74− { reArguments = reifiedEndpointArguments @endpoint
75− , reEndpointFunc = foo (normalize endpoint)
76− }
77− )
78− : (map . first) (+1)
79− (toReifiedClientApi endpoints (Proxy @endpoints) clientEnv)
80− where
81−
82− foo :: V.Curried (EndpointArgs endpoint) (ReaderT ClientEnv IO ResultType)
83− -> V.Curried (EndpointArgs endpoint) (IO ResultType)
84− foo = mapCurried @(EndpointArgs endpoint) @(ReaderT ClientEnv IO ResultType) (`runReaderT` clientEnv)
85−
86−mapCurried :: forall ts a b. V.RecordCurry' ts => (a -> b) -> V.Curried ts a -> V.Curried ts b
87−mapCurried f g = V.rcurry' @ts $ f . V.runcurry' g
88−
89−type ResultType = Either InteractionError (NonEmpty (Dynamic,Int))
90−-- runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
91−
92−
93−instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (ClientM x) where
94− 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
102−
103− where textual = T.pack $ show err
104−instance ToReifiedClientApi '[] where
105− toReifiedClientApi NoClientEndpoints _ _ = []
106−
107−
108−instance
109− ( FlattenClient api,
110− Endpoints endpoint ~ '[endpoint]
111− ) =>
112− FlattenClient (endpoint :<|> api)
113− where
114− flattenClient (endpoint :<|> c) = endpoint `AClientEndpoint` flattenClient @api c
115−
116−instance
117− (
118− Endpoints api ~ '[api]
119− ) =>
120− FlattenClient (x :> api)
121− where
122− flattenClient c = c `AClientEndpoint` NoClientEndpoints
123−
124−
125−instance FlattenClient (Verb method statusCode contentTypes responseType)
126− where
127− flattenClient c = c `AClientEndpoint` NoClientEndpoints
src/Roboservant/Direct.hs+14−17
@@ -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 (..),
@@ -65,7 +65,6 @@ import Roboservant.Types
6565 TypedF,
6666 )
6767import Roboservant.Types.Config
68−
6968import System.Random (Random (randomR), StdGen, mkStdGen)
7069import qualified Type.Reflection as R
7170
@@ -150,12 +149,12 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
150149 Report
151150 (unlines [show failureType, show exception])
152151 r
153− displayDiagnostics FuzzState {..} = liftIO $
152+ displayDiagnostics FuzzState {..} = liftIO $ do
154153 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)
154+ ["api endpoints covered"]
155+ <> (map show . Set.toList . Set.fromList $ map apiOffset path)
156+ <> ["", "types in stash"]
157+ <> DM.foldrWithKey (\_ v r -> (show . NEL.length . getStashValue $ v) : r) [] (getStash stash)
159158 -- <> (map (show . NEL.length . getStashValue ) $ DM.assocs (getStash stash))
160159 -- $ \_k v ->
161160 -- (show . NEL.length $ getStashValue v))
@@ -242,15 +241,12 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
242241 execute fuzzop func args = do
243242 (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
244243 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)) ->
244+ Left (e::InteractionError) -> throw e
245+ Right (dyn :: NEL.NonEmpty (Dynamic, Int)) -> do
250246 modify'
251− ( \fs@FuzzState {..} ->
252− fs {stash = addToStash (NEL.toList dyn) stash}
253− )
247+ ( \fs@FuzzState {..} ->
248+ fs {stash = addToStash (NEL.toList dyn) stash}
249+ )
254250 where
255251 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
256252 -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
@@ -262,7 +258,8 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
262258 (execute op func args)
263259 [ Handler (\(e :: SomeAsyncException) -> throw e),
264260 Handler
265− ( \(e :: SomeException) ->
261+ ( \(e :: SomeException) -> do
262+ -- displayDiagnostics =<< get
266263 throw . RoboservantException ServerCrashed (Just e) =<< get
267264 )
268265 ]
src/Roboservant/Server.hsdeleted−36
@@ -1,36 +0,0 @@
1−{-# LANGUAGE RankNTypes #-}
2−{-# LANGUAGE TypeApplications #-}
3−{-# LANGUAGE PolyKinds #-}
4−{-# LANGUAGE AllowAmbiguousTypes #-}
5−{-# LANGUAGE FlexibleContexts #-}
6−{-# LANGUAGE GADTs #-}
7−{-# LANGUAGE PolyKinds #-}
8−
9−{-# LANGUAGE ScopedTypeVariables #-}
10−{-# LANGUAGE TypeApplications #-}
11−{-# LANGUAGE TypeFamilies #-}
12−{-# LANGUAGE UndecidableInstances #-}
13−
14−module Roboservant.Server (fuzz, module Roboservant.Types ) where
15−
16−import Roboservant.Direct(fuzz',Report)
17−import Roboservant.Types
18− ( FlattenServer (..),
19− ReifiedApi,
20− )
21−import Roboservant.Types.ReifiedApi.Server(ToReifiedApi (..))
22−import Servant (Endpoints, Proxy (Proxy), Server)
23−import Roboservant.Types.Config
24−
25−fuzz :: forall api.
26− (FlattenServer api, ToReifiedApi (Endpoints api)) =>
27− Server api ->
28− Config ->
29− IO (Maybe Report)
30−fuzz s = fuzz' (reifyServer s)
31− -- todo: how do we pull reifyServer out?
32− where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api))
33− => Server api -> ReifiedApi
34− reifyServer server = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
35−-- reifyServer server = toReifiedApi server (Proxy @(Endpoints api))
36−
src/Roboservant/Types.hs+4−3
@@ -10,17 +10,18 @@
1010module Roboservant.Types
1111 ( module Roboservant.Types.Breakdown,
1212 module Roboservant.Types.BuildFrom,
13+ module Roboservant.Types.FlattenServer,
1314 module Roboservant.Types.ReifiedApi,
14− module Roboservant.Types.ReifiedApi.Server,
1515 module Roboservant.Types.Internal,
1616 module Roboservant.Types.Config,
17+ Atom, Compound
18+
1719 )
1820where
1921
2022import Roboservant.Types.Breakdown
2123import Roboservant.Types.BuildFrom
2224import Roboservant.Types.Config
25+import Roboservant.Types.FlattenServer
2326import Roboservant.Types.Internal
2427import Roboservant.Types.ReifiedApi
25−import 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/FlattenServer.hsnew file+40
@@ -0,0 +1,40 @@
1+{-# LANGUAGE AllowAmbiguousTypes #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE PolyKinds #-}
5+{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TypeApplications #-}
7+{-# LANGUAGE TypeOperators #-}
8+{-# LANGUAGE UndecidableInstances #-}
9+
10+-- | terrible name, this really just pulls stuff out where we can fiddle with it.
11+module Roboservant.Types.FlattenServer where
12+
13+import Servant
14+
15+data Bundled endpoints where
16+ AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
17+ NoEndpoints :: Bundled '[]
18+
19+class FlattenServer api where
20+ flattenServer :: Server api -> Bundled (Endpoints api)
21+
22+instance
23+ ( FlattenServer api,
24+ Endpoints endpoint ~ '[endpoint]
25+ ) =>
26+ FlattenServer (endpoint :<|> api)
27+ where
28+ flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
29+
30+instance
31+ (
32+ Endpoints api ~ '[api]
33+ ) =>
34+ FlattenServer (x :> api)
35+ where
36+ flattenServer server = server `AnEndpoint` NoEndpoints
37+
38+instance FlattenServer (Verb method statusCode contentTypes responseType)
39+ where
40+ flattenServer server = server `AnEndpoint` NoEndpoints
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+3−168
@@ -1,8 +1,3 @@
1−-- | Description: ways to build a reified api from a servant description.
2−--
3−-- arguably this could be more general and be abstracted away from even relying on servant
4−-- but that's future work.
5−
61{-# LANGUAGE AllowAmbiguousTypes #-}
72{-# LANGUAGE DataKinds #-}
83{-# LANGUAGE DerivingStrategies #-}
@@ -26,12 +21,7 @@ import Data.List.NonEmpty (NonEmpty)
2621import Data.Typeable (Typeable)
2722import GHC.Generics ((:*:)(..))
2823import Roboservant.Types.Internal
29−import Roboservant.Types.Breakdown
30−import Roboservant.Types.BuildFrom
31−import Data.Kind(Type)
32−import Servant
33−import Servant.API.Modifiers(FoldRequired,FoldLenient)
34−import GHC.TypeLits (Symbol)
24+
3525import qualified Data.Text as T
3626import qualified Data.Vinyl as V
3727import qualified Data.Vinyl.Curry as V
@@ -52,166 +42,11 @@ data ReifiedEndpoint = forall as. (V.RecordToList as, V.RMap as) => ReifiedEndpo
5242 , reEndpointFunc :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
5343 }
5444
55−instance Show ReifiedEndpoint where
56− show _ = "lol"
57−
58−class ( V.RecordToList (EndpointArgs endpoint)
59− , V.RMap (EndpointArgs endpoint)
60− ) => ToReifiedEndpoint (endpoint :: Type) where
61− type EndpointArgs endpoint :: [Type]
62− type EndpointRes endpoint :: Type
63−
64− reifiedEndpointArguments :: V.Rec (TypedF Argument) (EndpointArgs endpoint)
65−
45+type ReifiedApi = [(ApiOffset, ReifiedEndpoint )]
6646
6747tagType :: Typeable a => f a -> TypedF f a
6848tagType = (R.typeRep :*:)
6949
70−data InteractionError = InteractionError
71− { errorMessage :: T.Text
72− , fatalError :: Bool
73− }
50+newtype InteractionError = InteractionError T.Text
7451 deriving Show
7552instance Exception InteractionError
76−
77−
78−
79−instance
80− (Typeable responseType, Breakdown responseType) =>
81− ToReifiedEndpoint (Verb method statusCode contentTypes responseType)
82− where
83− type EndpointArgs (Verb method statusCode contentTypes responseType) = '[]
84− type EndpointRes (Verb method statusCode contentTypes responseType) = responseType
85− reifiedEndpointArguments = V.RNil
86−
87−instance ToReifiedEndpoint (NoContentVerb method)
88− where
89− type EndpointArgs (NoContentVerb method) = '[]
90− type EndpointRes (NoContentVerb method) = NoContent
91− reifiedEndpointArguments = V.RNil
92−
93−instance
94− (ToReifiedEndpoint endpoint) =>
95− ToReifiedEndpoint ((x :: Symbol) :> endpoint)
96− where
97− type EndpointArgs ((x :: Symbol) :> endpoint) = EndpointArgs endpoint
98− type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
99− reifiedEndpointArguments = reifiedEndpointArguments @endpoint
100−
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−
111−instance
112− (ToReifiedEndpoint endpoint) =>
113− ToReifiedEndpoint (Description s :> endpoint)
114− where
115− type EndpointArgs (Description s :> endpoint) = EndpointArgs endpoint
116− type EndpointRes (Description s :> endpoint) = EndpointRes endpoint
117− reifiedEndpointArguments = reifiedEndpointArguments @endpoint
118−
119−instance
120− (ToReifiedEndpoint endpoint) =>
121− ToReifiedEndpoint (Summary s :> endpoint)
122− where
123− type EndpointArgs (Summary s :> endpoint) = EndpointArgs endpoint
124− type EndpointRes (Summary s :> endpoint) = EndpointRes endpoint
125− reifiedEndpointArguments = reifiedEndpointArguments @endpoint
126−
127−instance
128− (Typeable requestType
129− ,BuildFrom requestType
130− ,ToReifiedEndpoint endpoint) =>
131− ToReifiedEndpoint (QueryFlag name :> endpoint)
132− where
133− type EndpointArgs (QueryFlag name :> endpoint) = Bool ': EndpointArgs endpoint
134− type EndpointRes (QueryFlag name :> endpoint) = EndpointRes endpoint
135− reifiedEndpointArguments = tagType (Argument (buildFrom @Bool)) V.:& reifiedEndpointArguments @endpoint
136−
137−type IfLenient s mods t = If (FoldLenient mods) (Either s t) t
138−type IfRequired mods t = If (FoldRequired mods) t (Maybe t)
139−type IfRequiredLenient s mods t = IfRequired mods (IfLenient s mods t)
140−
141−instance
142− ( BuildFrom (IfRequiredLenient T.Text mods paramType)
143− , ToReifiedEndpoint endpoint
144− ) =>
145− ToReifiedEndpoint (QueryParam' mods name paramType :> endpoint)
146− where
147− type EndpointArgs (QueryParam' mods name paramType :> endpoint) = IfRequiredLenient T.Text mods paramType ': EndpointArgs endpoint
148− type EndpointRes (QueryParam' mods name paramType :> endpoint) = EndpointRes endpoint
149− reifiedEndpointArguments =
150− tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
151− V.:& reifiedEndpointArguments @endpoint
152−
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−
171−instance
172− ( BuildFrom (IfRequiredLenient T.Text mods headerType)
173− , ToReifiedEndpoint endpoint
174− ) =>
175− ToReifiedEndpoint (Header' mods headerName headerType :> endpoint)
176− where
177− type EndpointArgs (Header' mods headerName headerType :> endpoint) = IfRequiredLenient T.Text mods headerType ': EndpointArgs endpoint
178− type EndpointRes (Header' mods headerName headerType :> endpoint) = EndpointRes endpoint
179− reifiedEndpointArguments =
180− tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods headerType)))
181− V.:& reifiedEndpointArguments @endpoint
182−
183−#if MIN_VERSION_servant(0,17,0)
184−instance
185− ( BuildFrom (IfLenient String mods captureType)
186− , ToReifiedEndpoint endpoint) =>
187− ToReifiedEndpoint (Capture' mods name captureType :> endpoint)
188− where
189− type EndpointArgs (Capture' mods name captureType :> endpoint) = IfLenient String mods captureType ': EndpointArgs endpoint
190− type EndpointRes (Capture' mods name captureType :> endpoint) = EndpointRes endpoint
191− reifiedEndpointArguments =
192− tagType (Argument (buildFrom @(IfLenient String mods captureType)))
193− V.:& reifiedEndpointArguments @endpoint
194−#else
195−instance
196− ( BuildFrom captureType
197− , ToReifiedEndpoint endpoint) =>
198− ToReifiedEndpoint (Capture' mods name captureType :> endpoint)
199− where
200− type EndpointArgs (Capture' mods name captureType :> endpoint) = captureType ': EndpointArgs endpoint
201− type EndpointRes (Capture' mods name captureType :> endpoint) = EndpointRes endpoint
202− reifiedEndpointArguments =
203− tagType (Argument (buildFrom @(captureType)))
204− V.:& reifiedEndpointArguments @endpoint
205−
206−#endif
207−
208−instance
209− ( BuildFrom (IfLenient String mods requestType)
210− , ToReifiedEndpoint endpoint) =>
211− ToReifiedEndpoint (ReqBody' mods contentTypes requestType :> endpoint)
212− where
213− type EndpointArgs (ReqBody' mods contentTypes requestType :> endpoint) = IfLenient String mods requestType ': EndpointArgs endpoint
214− type EndpointRes (ReqBody' mods contentTypes requestType :> endpoint) = EndpointRes endpoint
215− reifiedEndpointArguments =
216− tagType (Argument (buildFrom @(IfLenient String mods requestType)))
217− V.:& reifiedEndpointArguments @endpoint
src/Roboservant/Types/ReifiedApi/Server.hs+126−36
@@ -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 #-}
@@ -15,34 +17,47 @@
1517{-# LANGUAGE DataKinds #-}
1618{-# LANGUAGE TypeOperators #-}
1719{-# LANGUAGE FlexibleInstances #-}
18−{-# LANGUAGE MultiParamTypeClasses #-}
19−module Roboservant.Types.ReifiedApi.Server(module Roboservant.Types.ReifiedApi.Server) where
20+module Roboservant.Types.ReifiedApi.Server where
2021
2122import Servant
23+import Servant.API.Modifiers(FoldRequired,FoldLenient)
2224
2325import Control.Monad.Except (runExceptT)
2426import Data.Bifunctor
2527import Data.Dynamic (Dynamic)
28+import Data.Kind
2629import Data.List.NonEmpty (NonEmpty)
2730import Data.Typeable (Typeable)
31+import GHC.TypeLits (Symbol)
2832import Roboservant.Types.Breakdown
33+import Roboservant.Types.BuildFrom
34+import Roboservant.Types.FlattenServer
2935import Roboservant.Types.ReifiedApi
3036
3137import qualified Data.Text as T
38+import qualified Data.Vinyl as V
3239import qualified Data.Vinyl.Curry as V
3340import Data.Hashable(Hashable)
3441
35−type ReifiedApi = [(ApiOffset, ReifiedEndpoint )]
3642
37−
38−class ToReifiedApi endpoints where
43+class ToReifiedApi (endpoints ) where
3944 toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
4045
46+class ( V.RecordToList (EndpointArgs endpoint)
47+ , V.RMap (EndpointArgs endpoint)
48+ ) => ToReifiedEndpoint (endpoint :: *) where
49+ type EndpointArgs endpoint :: [Type]
50+ type EndpointRes endpoint :: Type
51+
52+ reifiedEndpointArguments :: V.Rec (TypedF Argument) (EndpointArgs endpoint)
53+
4154instance ToReifiedApi '[] where
4255 toReifiedApi NoEndpoints _ = []
4356
4457instance
45− ( NormalizeFunction (ServerT endpoint Handler)
58+ ( Typeable (EndpointRes endpoint)
59+
60+ , NormalizeFunction (ServerT endpoint Handler)
4661 , Normal (ServerT endpoint Handler) ~ V.Curried (EndpointArgs endpoint) (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
4762 , ToReifiedEndpoint endpoint
4863 , ToReifiedApi endpoints
@@ -58,6 +73,13 @@ instance
5873 : (map . first) (+1)
5974 (toReifiedApi endpoints (Proxy @endpoints))
6075
76+class NormalizeFunction m where
77+ type Normal m
78+ normalize :: m -> Normal m
79+
80+instance NormalizeFunction x => NormalizeFunction (r -> x) where
81+ type Normal (r -> x) = r -> Normal x
82+ normalize = fmap normalize
6183
6284instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x) where
6385 type Normal (Handler x) = IO (Either InteractionError (NonEmpty (Dynamic,Int)))
@@ -66,49 +88,117 @@ instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x)
6688 where
6789 -- | TODO improve this
6890 renderServerError :: ServerError -> InteractionError
69− renderServerError s = InteractionError (T.pack $ show s) (errHTTPCode serverError == 500)
70−
91+ renderServerError = InteractionError . T.pack . show
7192 Right x -> pure $ Right $ breakdown x
7293
94+instance
95+ (Typeable responseType, Breakdown responseType) =>
96+ ToReifiedEndpoint (Verb method statusCode contentTypes responseType)
97+ where
98+ type EndpointArgs (Verb method statusCode contentTypes responseType) = '[]
99+ type EndpointRes (Verb method statusCode contentTypes responseType) = responseType
100+ reifiedEndpointArguments = V.RNil
73101
74−-- case errHTTPCode serverError of
75−-- 500 -> throw serverError
76−-- _ ->
77−-- liftIO . logInfo . show $ ("ignoring non-500 error", serverError)
102+instance
103+ (ToReifiedEndpoint endpoint) =>
104+ ToReifiedEndpoint ((x :: Symbol) :> endpoint)
105+ where
106+ type EndpointArgs ((x :: Symbol) :> endpoint) = EndpointArgs endpoint
107+ type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
108+ reifiedEndpointArguments = reifiedEndpointArguments @endpoint
78109
110+instance
111+ (ToReifiedEndpoint endpoint) =>
112+ ToReifiedEndpoint (Description s :> endpoint)
113+ where
114+ type EndpointArgs (Description s :> endpoint) = EndpointArgs endpoint
115+ type EndpointRes (Description s :> endpoint) = EndpointRes endpoint
116+ reifiedEndpointArguments = reifiedEndpointArguments @endpoint
79117
80−data Bundled endpoints where
81− -- AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
82− AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
83− NoEndpoints :: Bundled '[]
118+instance
119+ (ToReifiedEndpoint endpoint) =>
120+ ToReifiedEndpoint (Summary s :> endpoint)
121+ where
122+ type EndpointArgs (Summary s :> endpoint) = EndpointArgs endpoint
123+ type EndpointRes (Summary s :> endpoint) = EndpointRes endpoint
124+ reifiedEndpointArguments = reifiedEndpointArguments @endpoint
125+
126+instance
127+ (Typeable requestType
128+ ,BuildFrom requestType
129+ ,ToReifiedEndpoint endpoint) =>
130+ ToReifiedEndpoint (QueryFlag name :> endpoint)
131+ where
132+ type EndpointArgs (QueryFlag name :> endpoint) = Bool ': EndpointArgs endpoint
133+ type EndpointRes (QueryFlag name :> endpoint) = EndpointRes endpoint
134+ reifiedEndpointArguments = tagType (Argument (buildFrom @Bool)) V.:& reifiedEndpointArguments @endpoint
84135
85−class FlattenServer api where
86− flattenServer :: Server api -> Bundled (Endpoints api)
136+type IfLenient s mods t = If (FoldLenient mods) (Either s t) t
137+type IfRequired mods t = If (FoldRequired mods) t (Maybe t)
138+type IfRequiredLenient s mods t = IfRequired mods (IfLenient s mods t)
87139
88140instance
89− ( FlattenServer api,
90− Endpoints endpoint ~ '[endpoint]
141+ ( BuildFrom (IfRequiredLenient T.Text mods paramType)
142+ , ToReifiedEndpoint endpoint
91143 ) =>
92− FlattenServer (endpoint :<|> api)
144+ ToReifiedEndpoint (QueryParam' mods name paramType :> endpoint)
93145 where
94− flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
146+ type EndpointArgs (QueryParam' mods name paramType :> endpoint) = IfRequiredLenient T.Text mods paramType ': EndpointArgs endpoint
147+ type EndpointRes (QueryParam' mods name paramType :> endpoint) = EndpointRes endpoint
148+ reifiedEndpointArguments =
149+ tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
150+ V.:& reifiedEndpointArguments @endpoint
95151
96152instance
97− (
98− Endpoints api ~ '[api]
99− ) =>
100− FlattenServer (x :> api)
153+ ( BuildFrom (IfRequiredLenient T.Text mods headerType)
154+ , ToReifiedEndpoint endpoint
155+ ) =>
156+ ToReifiedEndpoint (Header' mods headerName headerType :> endpoint)
101157 where
102− flattenServer server = server `AnEndpoint` NoEndpoints
158+ type EndpointArgs (Header' mods headerName headerType :> endpoint) = IfRequiredLenient T.Text mods headerType ': EndpointArgs endpoint
159+ type EndpointRes (Header' mods headerName headerType :> endpoint) = EndpointRes endpoint
160+ reifiedEndpointArguments =
161+ tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods headerType)))
162+ V.:& reifiedEndpointArguments @endpoint
103163
104−instance FlattenServer (Verb method statusCode contentTypes responseType)
164+#if MIN_VERSION_servant(0,17,0)
165+instance
166+ ( BuildFrom (IfLenient String mods captureType)
167+ , ToReifiedEndpoint endpoint) =>
168+ ToReifiedEndpoint (Capture' mods name captureType :> endpoint)
169+ where
170+ type EndpointArgs (Capture' mods name captureType :> endpoint) = IfLenient String mods captureType ': EndpointArgs endpoint
171+ type EndpointRes (Capture' mods name captureType :> endpoint) = EndpointRes endpoint
172+ reifiedEndpointArguments =
173+ tagType (Argument (buildFrom @(IfLenient String mods captureType)))
174+ V.:& reifiedEndpointArguments @endpoint
175+#else
176+instance
177+ ( BuildFrom captureType
178+ , ToReifiedEndpoint endpoint) =>
179+ ToReifiedEndpoint (Capture' mods name captureType :> endpoint)
105180 where
106− flattenServer server = server `AnEndpoint` NoEndpoints
181+ type EndpointArgs (Capture' mods name captureType :> endpoint) = captureType ': EndpointArgs endpoint
182+ type EndpointRes (Capture' mods name captureType :> endpoint) = EndpointRes endpoint
183+ reifiedEndpointArguments =
184+ tagType (Argument (buildFrom @(captureType)))
185+ V.:& reifiedEndpointArguments @endpoint
107186
108−class NormalizeFunction m where
109− type Normal m
110− normalize :: m -> Normal m
187+#endif
111188
112−instance NormalizeFunction x => NormalizeFunction (r -> x) where
113− type Normal (r -> x) = r -> Normal x
114− normalize = fmap normalize
189+instance
190+ ( BuildFrom (IfLenient String mods requestType)
191+ , ToReifiedEndpoint endpoint) =>
192+ ToReifiedEndpoint (ReqBody' mods contentTypes requestType :> endpoint)
193+ where
194+ type EndpointArgs (ReqBody' mods contentTypes requestType :> endpoint) = IfLenient String mods requestType ': EndpointArgs endpoint
195+ type EndpointRes (ReqBody' mods contentTypes requestType :> endpoint) = EndpointRes endpoint
196+ reifiedEndpointArguments =
197+ tagType (Argument (buildFrom @(IfLenient String mods requestType)))
198+ V.:& reifiedEndpointArguments @endpoint
199+
200+
201+-- case errHTTPCode serverError of
202+-- 500 -> throw serverError
203+-- _ ->
204+-- liftIO . logInfo . show $ ("ignoring non-500 error", serverError)
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+65−104
@@ -4,13 +4,7 @@
44{-# LANGUAGE RecordWildCards #-}
55{-# LANGUAGE StandaloneDeriving #-}
66{-# LANGUAGE TypeApplications #-}
7−{-# LANGUAGE RankNTypes #-}
8−{-# LANGUAGE FlexibleContexts #-}
9−{-# LANGUAGE AllowAmbiguousTypes #-}
10−{-# LANGUAGE ScopedTypeVariables #-}
11−{-# LANGUAGE DataKinds #-}
127{-# OPTIONS_GHC -fno-warn-orphans #-}
13−{-# LANGUAGE TypeFamilies #-}
148
159import qualified Breakdown
1610import Data.Dynamic (toDyn)
@@ -21,134 +15,101 @@ import qualified Foo
2115import qualified Headers
2216import qualified Nested
2317import qualified Post
24−import qualified Put
2518import qualified Product
26−import qualified QueryParams
27−import qualified Roboservant as R
19+import qualified Roboservant as RS
2820import qualified Roboservant.Server as RS
29−import qualified Roboservant.Client as RC
3021import qualified Seeded
3122import Test.Hspec
3223import Test.Hspec.Core.Spec (FailureReason (Reason), ResultStatus (Failure, Success), itemExample, mapSpecItem_, resultStatus)
3324import qualified Valid
34−import Servant ( Server, Proxy(..), serve, Endpoints, HasServer )
35−
36−import Servant.Client(ClientEnv, mkClientEnv, baseUrlPort, parseBaseUrl,HasClient,ClientM)
37−import Network.Wai(Application)
38−import qualified Network.Wai.Handler.Warp as Warp
39−import Network.HTTP.Client (newManager, defaultManagerSettings)
40−import Control.Monad((>=>))
4125
4226main :: IO ()
4327main = hspec spec
4428
45−fuzzBoth
46− :: forall a .
47− (R.ToReifiedApi (Endpoints a), HasServer a '[], RS.FlattenServer a, RC.ToReifiedClientApi (Endpoints a), RC.FlattenClient a,
48− HasClient ClientM a)
49− => String -> Server a -> R.Config -> (Maybe R.Report -> IO ()) -> Spec
50−fuzzBoth name server config condition = do
51− it (name <> " via server") $
52− RS.fuzz @a server config >>= condition
53−
54− around (withServer (serve (Proxy :: Proxy a) server)) $
55− it (name <> " via client") $ \(clientEnv::ClientEnv) -> do
56− RC.fuzz @a clientEnv config >>= condition
57−
58−withServer :: Application -> ActionWith ClientEnv -> IO ()
59−withServer app action = Warp.testWithApplication (pure app) (genClientEnv >=> action)
60− where genClientEnv port = do
61− baseUrl <- parseBaseUrl "http://localhost"
62− manager <- newManager defaultManagerSettings
63− pure $ mkClientEnv manager (baseUrl { baseUrlPort = port })
64−
6529spec :: Spec
6630spec = do
6731 describe "Basic usage" $ do
6832 describe "noError" $ do
69− fuzzBoth @Valid.Api "find no error in a basic app" Valid.server R.defaultConfig (`shouldSatisfy` isNothing)
70− fuzzBoth @Valid.RoutedApi "finds no error in a valid generic app" Valid.routedServer R.defaultConfig (`shouldSatisfy` isNothing)
71− fuzzBoth @Valid.Api "fails coverage check" Valid.server R.defaultConfig {R.coverageThreshold = 0.6}
72− (\r ->
73− fmap (R.failureReason . R.rsException) r
74− `shouldSatisfy` ( \case
75− Just (R.InsufficientCoverage _) -> True
76− _ -> False
77− ))
78− describe "posted body" $
79− 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−
33+ it "finds no error in a valid app" $ do
34+ RS.fuzz @Valid.Api Valid.server RS.defaultConfig
35+ >>= (`shouldSatisfy` isNothing)
36+ it "finds no error in a valid generic app" $ do
37+ RS.fuzz @Valid.RoutedApi Valid.routedServer RS.defaultConfig
38+ >>= (`shouldSatisfy` isNothing)
39+ it "does fail coverage check" $ do
40+ r <- RS.fuzz @Valid.Api Valid.server RS.defaultConfig {RS.coverageThreshold = 0.6}
41+ fmap (RS.failureReason . RS.rsException) r
42+ `shouldSatisfy` ( \case
43+ Just (RS.InsufficientCoverage _) -> True
44+ _ -> False
45+ )
46+ describe "posted body" $ do
47+ it "passes a coverage check using a posted body" $ do
48+ RS.fuzz @Post.Api Post.server RS.defaultConfig {RS.coverageThreshold = 0.99}
49+ >>= (`shouldSatisfy` isNothing)
8850 describe "seeded" $ do
89− let res = Seeded.Seed 1
90− shouldFail $ fuzzBoth @Seeded.Api "finds an error using information passed in" Seeded.server
91− (R.defaultConfig {R.seed = [(toDyn res, hash res)]})
92− (`shouldSatisfy` isNothing)
93−
94− describe "Foo" $
95− fuzzBoth @Foo.Api "finds an error in a basic app" Foo.server R.defaultConfig (`shouldSatisfy` serverFailure)
96−
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−
51+ shouldFail
52+ $ it "finds an error using information passed in"
53+ $ let res = Seeded.Seed 1
54+ in RS.fuzz @Seeded.Api Seeded.server (RS.defaultConfig {RS.seed = [(toDyn res, hash res)]})
55+ >>= (`shouldSatisfy` isNothing)
56+ describe "Foo" $ do
57+ it "finds an error in a basic app" $
58+ RS.fuzz @Foo.Api Foo.server RS.defaultConfig
59+ >>= (`shouldSatisfy` serverFailure)
10160 describe "BuildFrom" $ do
102− describe "headers (and sum types)" $
103− 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)
61+ describe "headers (and sum types)" $ do
62+ it "should find a failure that's dependent on using header info" $ do
63+ RS.fuzz @Headers.Api Headers.server RS.defaultConfig
64+ >>= (`shouldSatisfy` serverFailure)
65+ describe "product types" $ do
66+ it "should find a failure that's dependent on creating a product" $ do
67+ RS.fuzz @Product.Api Product.server RS.defaultConfig {RS.seed = [RS.hashedDyn 'a', RS.hashedDyn (1 :: Int)]}
68+ >>= (`shouldSatisfy` serverFailure)
10969 describe "Breakdown" $ do
110− fuzzBoth @Breakdown.ProductApi "handles products" Breakdown.productServer R.defaultConfig
111− (`shouldSatisfy` serverFailure)
112− fuzzBoth @Breakdown.SumApi "handles sums" Breakdown.sumServer R.defaultConfig
113− (`shouldSatisfy` serverFailure)
114− describe "flattening" $
115− fuzzBoth @Nested.FlatApi "can handle nested apis" Nested.server R.defaultConfig {R.coverageThreshold = 0.99}
116− (`shouldSatisfy` isNothing)
117−
118−serverFailure :: Maybe R.Report -> Bool
70+ it "handles products" $ do
71+ RS.fuzz @Breakdown.ProductApi Breakdown.productServer RS.defaultConfig
72+ >>= (`shouldSatisfy` serverFailure)
73+ it "handles sums" $ do
74+ RS.fuzz @Breakdown.SumApi Breakdown.sumServer RS.defaultConfig
75+ >>= (`shouldSatisfy` serverFailure)
76+ describe "flattening" $ do
77+ -- we don't actually do much here, this is just here to document the appropriate response
78+ -- if you get a type error with a nested api.
79+ it "can handle nested apis" $ do
80+ RS.fuzz @(Nested.FlatApi) Nested.server RS.defaultConfig {RS.coverageThreshold = 0.99}
81+ >>= (`shouldSatisfy` isNothing)
82+
83+serverFailure :: Maybe RS.Report -> Bool
11984serverFailure = \case
120− Just R.Report {..} ->
121− let R.RoboservantException {..} = rsException
122− in failureReason /= R.NoPossibleMoves
85+ Just RS.Report {..} ->
86+ let RS.RoboservantException {..} = rsException
87+ in failureReason /= RS.NoPossibleMoves
12388 _ -> False
12489
125−deriving via (R.Atom Foo.Foo) instance R.Breakdown Foo.Foo
126−
127−deriving via (R.Atom Foo.Foo) instance R.BuildFrom Foo.Foo
128−
129−deriving via (R.Atom Headers.Foo) instance R.Breakdown Headers.Foo
90+deriving via (RS.Atom Foo.Foo) instance RS.Breakdown Foo.Foo
13091
131−deriving via (R.Atom Headers.Foo) instance R.BuildFrom Headers.Foo
92+deriving via (RS.Atom Foo.Foo) instance RS.BuildFrom Foo.Foo
13293
133−deriving via (R.Atom Seeded.Seed) instance R.Breakdown Seeded.Seed
94+deriving via (RS.Atom Headers.Foo) instance RS.Breakdown Headers.Foo
13495
135−deriving via (R.Atom Seeded.Seed) instance R.BuildFrom Seeded.Seed
96+deriving via (RS.Atom Headers.Foo) instance RS.BuildFrom Headers.Foo
13697
137−deriving via (R.Atom Void) instance R.BuildFrom Void
98+deriving via (RS.Atom Seeded.Seed) instance RS.Breakdown Seeded.Seed
13899
139−deriving via (R.Atom Post.FooPost) instance R.Breakdown Post.FooPost
140−deriving via (R.Atom Post.FooPost) instance R.BuildFrom Post.FooPost
100+deriving via (RS.Atom Seeded.Seed) instance RS.BuildFrom Seeded.Seed
141101
142−deriving via (R.Atom Put.Foo) instance R.Breakdown Put.Foo
143−deriving via (R.Atom Put.Foo) instance R.BuildFrom Put.Foo
102+deriving via (RS.Atom Void) instance RS.BuildFrom Void
144103
104+deriving via (RS.Atom Post.FooPost) instance RS.Breakdown Post.FooPost
145105
106+deriving via (RS.Atom Post.FooPost) instance RS.BuildFrom Post.FooPost
146107
147−deriving via (R.Compound Breakdown.Foo) instance R.Breakdown Breakdown.Foo
108+deriving via (RS.Compound Breakdown.Foo) instance RS.Breakdown Breakdown.Foo
148109
149−deriving via (R.Compound Product.Foo) instance R.BuildFrom Product.Foo
110+deriving via (RS.Compound Product.Foo) instance RS.BuildFrom Product.Foo
150111
151−deriving via (R.Compound Breakdown.SomeSum) instance R.Breakdown Breakdown.SomeSum
112+deriving via (RS.Compound Breakdown.SomeSum) instance RS.Breakdown Breakdown.SomeSum
152113
153114-- | `shouldFail` allows you to assert that a given `Spec` should contain at least one failing test.
154115-- this is often useful when testing tests.
@@ -158,7 +119,7 @@ shouldFail =
158119 ( \i ->
159120 i
160121 { itemExample = \p a cb -> do
161− r <- itemExample i p a cb
122+ r <- (itemExample i) p a cb
162123 pure
163124 r
164125 { 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] ()