on master help sign in
32 files changed+756−317
since splitsnapshots
unifiedsplit
.github/workflows/cabal.yml+64 .github/workflows/ci.yml+2−2 EXAMPLE.md+41−4 Makefile+1−1 README.md+8−1 benchmarks/genFile.sh+15−2 benchmarks/prelude+7−16 benchmarks/runTiming.sh+2−2 package.yaml+10−2 roboservant.cabal+22−6 src/Roboservant/Client.hs+127 src/Roboservant/Direct.hs+33−31 src/Roboservant/Server.hs+36 src/Roboservant/Types.hs+3−2 src/Roboservant/Types/Breakdown.hs+15−2 src/Roboservant/Types/BuildFrom.hs+12−5 src/Roboservant/Types/FlattenServer.hs−44 src/Roboservant/Types/Internal.hs+2−2 src/Roboservant/Types/Orphans.hs+7 src/Roboservant/Types/ReifiedApi.hs+56−49 src/Roboservant/Types/ReifiedApi/Server.hs+114 stack.yaml+5−10 stack.yaml.lock+9−58 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+105−65 test/UnsafeIO.hs+3−3 test/Valid.hs+2−3
.github/workflows/cabal.ymlnew file+64
@@ -0,0 +1,64 @@
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: CI
1+name: Stack 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.8.3"]
15+ ghc: ["8.10.4"]
1616 os: [ubuntu-latest]
1717 steps:
1818 - uses: actions/checkout@v2
EXAMPLE.md+41−4
@@ -13,23 +13,37 @@ Our api under test:
1313{-# LANGUAGE DeriveGeneric #-}
1414{-# LANGUAGE GeneralizedNewtypeDeriving #-}
1515{-# LANGUAGE RecordWildCards #-}
16+{-# LANGUAGE ScopedTypeVariables #-}
17+{-# LANGUAGE TypeApplications #-}
1618
17−import Roboservant
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
1824import Test.Hspec
1925import Servant
2026import GHC.Generics
2127import Data.Typeable
2228import Data.Hashable
2329import Data.Maybe(isNothing, isJust)
30+import qualified Network.Wai.Handler.Warp as Warp
31+import Data.Aeson(FromJSON,ToJSON)
2432
2533newtype A = A Int
2634 deriving (Generic, Eq, Show, Typeable)
2735 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
2836
37+instance FromJSON A
38+instance ToJSON A
39+
2940newtype B = B Int
3041 deriving (Generic, Eq, Show, Typeable)
3142 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
3243
44+instance FromJSON B
45+instance ToJSON B
46+
3347type Api =
3448 "item" :> Get '[JSON] A
3549 :<|> "itemAdd" :> Capture "one" B :> Capture "two" B :> Get '[JSON] B
@@ -45,7 +59,7 @@ server introduce = introduce :<|> combine :<|> eliminate
4559```
4660
4761We have a "good" server, that never generates anything other than a 0. This means repeated application of
48−the combination/addition rule can never bring us to the dangerous state of numbers larger than 100.
62+the combination/addition rule can never bring us to the dangerous state of numbers larger than 10.
4963
5064``` haskell
5165goodServer, badServer :: Server Api
@@ -62,13 +76,24 @@ main = hspec spec
6276spec :: Spec
6377spec = describe "example" $ do
6478 it "good server should not fail" $ do
65− fuzz @Api goodServer config
79+ RS.fuzz @Api goodServer config
6680 >>= (`shouldSatisfy` isNothing)
6781 it "bad server should fail" $ do
68− fuzz @Api badServer config
82+ RS.fuzz @Api badServer config
6983 >>= (`shouldSatisfy` isJust)
7084```
7185
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.)
7297
7398We expect to be able to cover the whole api from our starting point, so let's set the coverage to 0.99.
7499There are other tweakable things in the config, like maximum runtime, reps,
@@ -104,3 +129,15 @@ build it up from components.
104129deriving via (Compound B) instance BuildFrom B
105130deriving via (Atom B) instance Breakdown B
106131```
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+```
Makefile+1−1
@@ -1,4 +1,4 @@
11testwatch:
2− ghcid -T main -c 'stack repl --test --ghc-options=-fobject-code' --allow-eval --restart="stack.yaml" --restart="package.yaml" -W
2+ ghcid -T main -c 'stack repl roboservant:lib roboservant:test:roboservant-test --test --ghc-options=-fobject-code' --allow-eval --restart="stack.yaml" --restart="package.yaml" -W
33pedanticwatch:
44 ghcid -c 'stack repl --test --ghc-options=-fobject-code' --allow-eval --restart="stack.yaml" --restart="package.yaml" -W
README.md+8−1
@@ -2,7 +2,8 @@
22
33Automatically fuzz your servant apis in a contextually-aware way.
44
5−![CI](https://github.com/mwotton/roboservant/workflows/CI/badge.svg)
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)
67
78## example
89
@@ -102,6 +103,12 @@ we would want to show that distinction in a visible way: it's possible
102103that other calls that don't have direct data dependencies were
103104important, but we definitely know we need the direct data dependencies.
104105
106+The provenance stuff is a bit underbaked. It should at least pull a
107+representation of the route chosen rather than just an integer index.
108+
105109It would also be nice to have a robust strategy for deriving recursive
106110datatypes, or at least rejecting attempts to generate them that don't
107111end in an infinite loop.
112+
113+Currently the `FlattenServer` instance for `:>` is quadratic. It would
114+be nice to fix this but I lack the art.
benchmarks/genFile.sh+15−2
@@ -1,9 +1,11 @@
11n=$1
2+k=$2
23
34cat prelude
4−echo -n "type Api = \"route1\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
5+echo -n "type SubApi = \"route1\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
56for i in $(seq 2 $n); do
6− echo " :<|> \"route${i}\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
7+ echo -n " :<|> \"route${i}\" :> "
8+ echo "ReqBody '[JSON] Input :> Post '[JSON] Output"
79done
810
911echo -n "server = handler "
@@ -11,3 +13,14 @@ echo -n "server = handler "
1113for i in $(seq 2 $n); do
1214 echo -n ":<|> handler "
1315done
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+7−16
@@ -12,6 +12,7 @@ import Servant
1212import Data.Hashable
1313import GHC.Generics
1414import Data.Aeson
15+import Servant.API.Flatten
1516
1617data Input = Input1 { foo :: String }
1718 | Input2 { bar :: Int }
@@ -52,19 +53,9 @@ handler = pure . \case
5253 Input1 foo -> Output1 foo
5354 Input2 bar -> Output2 bar
5455
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− }
56+main =
57+ -- fuzz @Api server defaultConfig
58+ pure ()
59+
60+f = flatten (Proxy @Api)
61+
benchmarks/runTiming.sh+2−2
@@ -1,8 +1,8 @@
11lo=$1
22hi=$2
3−
3+k=$3
44for i in $(seq $lo 5 $hi); do
5− ./genFile.sh $i > main.hs;
5+ ./genFile.sh $i $k > 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+10−2
@@ -1,6 +1,6 @@
11
22name: roboservant
3−version: 0.1.0.2
3+version: 0.1.0.3
44github: "mwotton/roboservant"
55license: BSD3
66copyright: "2020 Mark Wotton, Samuel Schlesinger"
@@ -19,6 +19,7 @@ dependencies:
1919- containers
2020- random
2121- hashable
22+- http-types
2223- lifted-base
2324- monad-control
2425- mtl
@@ -55,7 +56,10 @@ tests:
5556 - hspec
5657 - hspec-core
5758 - http-api-data
58−
59+ - http-client
60+ - hspec-wai
61+ - wai
62+ - warp
5963 example:
6064 main: Example.lhs
6165 source-dirs: .
@@ -63,6 +67,10 @@ tests:
6367 dependencies:
6468 - roboservant
6569 - hspec
70+ - warp
71+ - http-client
72+ - aeson
73+ - hspec-core
6674 ghc-options: -pgmL markdown-unlit
6775
6876 build-tools: markdown-unlit
roboservant.cabal+22−6
@@ -1,13 +1,13 @@
11cabal-version: 1.12
22
3−-- This file has been generated from package.yaml by hpack version 0.33.0.
3+-- This file has been generated from package.yaml by hpack version 0.35.0.
44--
55-- see: https://github.com/sol/hpack
66--
7−-- hash: 48773018ec618e9eba02a126e3aeb93781a1d68ce28e2c405c35e9536bf94f70
7+-- hash: 0c0c35e1dca1d6c52e5bc61591cbd8c0da9d811b83341bbc676c5f8b93b0b20d
88
99name: roboservant
10−version: 0.1.0.2
10+version: 0.1.0.3
1111synopsis: Automatic session-aware servant testing
1212description: Please see the README on GitHub at <https://github.com/mwotton/roboservant#readme>
1313category: Web
@@ -28,14 +28,17 @@ source-repository head
2828library
2929 exposed-modules:
3030 Roboservant
31+ Roboservant.Client
3132 Roboservant.Direct
33+ Roboservant.Server
3234 Roboservant.Types
3335 Roboservant.Types.Breakdown
3436 Roboservant.Types.BuildFrom
3537 Roboservant.Types.Config
36− Roboservant.Types.FlattenServer
3738 Roboservant.Types.Internal
39+ Roboservant.Types.Orphans
3840 Roboservant.Types.ReifiedApi
41+ Roboservant.Types.ReifiedApi.Server
3942 other-modules:
4043 Paths_roboservant
4144 hs-source-dirs:
@@ -48,6 +51,7 @@ library
4851 , dependent-map
4952 , dependent-sum
5053 , hashable
54+ , http-types
5155 , lifted-base
5256 , monad-control
5357 , mtl
@@ -69,18 +73,22 @@ test-suite example
6973 other-modules:
7074 Paths_roboservant
7175 hs-source-dirs:
72− ./.
76+ ./
7377 ghc-options: -Wall -fwrite-ide-info -hiedir=.hie -pgmL markdown-unlit
7478 build-tool-depends:
7579 markdown-unlit:markdown-unlit
7680 build-depends:
77− base >=4.7 && <5
81+ aeson
82+ , base >=4.7 && <5
7883 , bytestring
7984 , containers
8085 , dependent-map
8186 , dependent-sum
8287 , hashable
8388 , hspec
89+ , hspec-core
90+ , http-client
91+ , http-types
8492 , lifted-base
8593 , monad-control
8694 , mtl
@@ -95,6 +103,7 @@ test-suite example
95103 , time
96104 , unordered-containers
97105 , vinyl
106+ , warp
98107 default-language: Haskell2010
99108
100109test-suite roboservant-test
@@ -107,6 +116,8 @@ test-suite roboservant-test
107116 Nested
108117 Post
109118 Product
119+ Put
120+ QueryParams
110121 Seeded
111122 UnsafeIO
112123 Valid
@@ -124,7 +135,10 @@ test-suite roboservant-test
124135 , hashable
125136 , hspec
126137 , hspec-core
138+ , hspec-wai
127139 , http-api-data
140+ , http-client
141+ , http-types
128142 , lifted-base
129143 , monad-control
130144 , mtl
@@ -139,4 +153,6 @@ test-suite roboservant-test
139153 , time
140154 , unordered-containers
141155 , vinyl
156+ , wai
157+ , warp
142158 default-language: Haskell2010
src/Roboservant/Client.hsnew file+127
@@ -0,0 +1,127 @@
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+33−31
@@ -7,12 +7,12 @@
77{-# LANGUAGE RecordWildCards #-}
88{-# LANGUAGE ScopedTypeVariables #-}
99{-# LANGUAGE TupleSections #-}
10−{-# LANGUAGE TypeApplications #-}
10+
1111{-# LANGUAGE TypeFamilies #-}
1212{-# LANGUAGE UndecidableInstances #-}
1313
1414module Roboservant.Direct
15− ( fuzz,
15+ ( fuzz',
1616 Config (..),
1717 -- TODO come up with something smarter than exporting all this, we should
1818 -- have some nice error-display functions
@@ -56,16 +56,16 @@ import GHC.Generics ((:*:) (..))
5656import Roboservant.Types
5757 ( ApiOffset (..),
5858 Argument (..),
59− FlattenServer (..),
59+ InteractionError(..),
6060 Provenance (..),
61+ ReifiedApi,
6162 ReifiedEndpoint (..),
6263 Stash (..),
6364 StashValue (..),
64− ToReifiedApi (..),
6565 TypedF,
6666 )
6767import Roboservant.Types.Config
68−import Servant (Endpoints, Proxy (Proxy), Server, ServerError (..))
68+
6969import System.Random (Random (randomR), StdGen, mkStdGen)
7070import qualified Type.Reflection as R
7171
@@ -105,7 +105,7 @@ data EndpointOption
105105 = forall as.
106106 (V.RecordToList as, V.RMap as) =>
107107 EndpointOption
108− { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))),
108+ { eoCall :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))),
109109 eoArgs :: V.Rec (TypedF StashValue) as
110110 }
111111
@@ -121,13 +121,18 @@ data Report
121121 }
122122 deriving (Show)
123123
124−fuzz ::
125− forall api.
126− (FlattenServer api, ToReifiedApi (Endpoints api)) =>
127− Server api ->
124+
125+
126+-- fuzzClient :: Client api -> Config -> IO (Maybe Report)
127+-- fuzzClient = undefined
128+
129+
130+
131+fuzz' ::
132+ ReifiedApi ->
128133 Config ->
129134 IO (Maybe Report)
130−fuzz server Config {..} = handle (pure . Just . formatException) $ do
135+fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
131136 let path = []
132137 stash = addToStash seed mempty
133138 currentRng = mkStdGen rngSeed
@@ -145,12 +150,12 @@ fuzz server Config {..} = handle (pure . Just . formatException) $ do
145150 Report
146151 (unlines [show failureType, show exception])
147152 r
148− displayDiagnostics FuzzState {..} = liftIO $ do
153+ displayDiagnostics FuzzState {..} = liftIO $
149154 logInfo $ unlines $
150− ["api endpoints covered"]
151− <> (map show . Set.toList . Set.fromList $ map apiOffset path)
152− <> ["", "types in stash"]
153− <> 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)
154159 -- <> (map (show . NEL.length . getStashValue ) $ DM.assocs (getStash stash))
155160 -- $ \_k v ->
156161 -- (show . NEL.length $ getStashValue v))
@@ -173,7 +178,7 @@ fuzz server Config {..} = handle (pure . Just . formatException) $ do
173178 else do
174179 _ <- action
175180 untilDone (n -1, deadline) action
176− reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
181+
177182 routeCount = length reifiedApi
178183 elementOrFail ::
179184 (MonadState FuzzState m, MonadIO m) =>
@@ -190,7 +195,7 @@ fuzz server Config {..} = handle (pure . Just . formatException) $ do
190195 ( forall as.
191196 (V.RecordToList as, V.RMap as) =>
192197 FuzzOp ->
193− V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))) ->
198+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
194199 V.Rec (TypedF V.Identity) as ->
195200 m r
196201 ) ->
@@ -231,23 +236,21 @@ fuzz server Config {..} = handle (pure . Just . formatException) $ do
231236 execute ::
232237 (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
233238 FuzzOp ->
234− V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))) ->
239+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
235240 V.Rec (TypedF V.Identity) as ->
236241 m ()
237242 execute fuzzop func args = do
238243 (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
239244 liftIO (V.runcurry' func argVals) >>= \case
240− -- parameterise this
241− Left (serverError :: ServerError) ->
242− case errHTTPCode serverError of
243− 500 -> throw serverError
244− _ ->
245− liftIO . logInfo . show $ ("ignoring non-500 error", serverError)
246− Right (dyn :: NEL.NonEmpty (Dynamic, Int)) -> do
245+ Left (e::InteractionError) ->
246+ if fatalError e
247+ then throw e
248+ else pure ()
249+ Right (dyn :: NEL.NonEmpty (Dynamic, Int)) ->
247250 modify'
248− ( \fs@FuzzState {..} ->
249− fs {stash = addToStash (NEL.toList dyn) stash}
250− )
251+ ( \fs@FuzzState {..} ->
252+ fs {stash = addToStash (NEL.toList dyn) stash}
253+ )
251254 where
252255 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
253256 -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
@@ -259,8 +262,7 @@ fuzz server Config {..} = handle (pure . Just . formatException) $ do
259262 (execute op func args)
260263 [ Handler (\(e :: SomeAsyncException) -> throw e),
261264 Handler
262− ( \(e :: SomeException) -> do
263− -- displayDiagnostics =<< get
265+ ( \(e :: SomeException) ->
264266 throw . RoboservantException ServerCrashed (Just e) =<< get
265267 )
266268 ]
src/Roboservant/Server.hsnew file+36
@@ -0,0 +1,36 @@
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+3−2
@@ -10,8 +10,8 @@
1010module Roboservant.Types
1111 ( module Roboservant.Types.Breakdown,
1212 module Roboservant.Types.BuildFrom,
13− module Roboservant.Types.FlattenServer,
1413 module Roboservant.Types.ReifiedApi,
14+ module Roboservant.Types.ReifiedApi.Server,
1515 module Roboservant.Types.Internal,
1616 module Roboservant.Types.Config,
1717 )
@@ -20,6 +20,7 @@ where
2020import Roboservant.Types.Breakdown
2121import Roboservant.Types.BuildFrom
2222import Roboservant.Types.Config
23−import Roboservant.Types.FlattenServer
2423import Roboservant.Types.Internal
2524import Roboservant.Types.ReifiedApi
25+import Roboservant.Types.ReifiedApi.Server
26+import Roboservant.Types.Orphans()
src/Roboservant/Types/Breakdown.hs+15−2
@@ -1,5 +1,6 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
22{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE FlexibleContexts #-}
34{-# LANGUAGE DerivingVia #-}
45{-# LANGUAGE FlexibleInstances #-}
56{-# LANGUAGE GADTs #-}
@@ -22,6 +23,8 @@ import qualified Data.List.NonEmpty as NEL
2223import Data.Typeable (Typeable)
2324import GHC.Generics
2425import Roboservant.Types.Internal
26+import Servant
27+import Roboservant.Types.Orphans()
2528
2629breakdown ::
2730 (Hashable x, Typeable x, Breakdown x) =>
@@ -38,10 +41,15 @@ instance (Hashable x, Typeable x) => Breakdown (Atom x) where
3841deriving via (Atom ()) instance Breakdown ()
3942
4043deriving via (Atom Int) instance Breakdown Int
44+deriving via (Atom Char) instance Breakdown Char
4145
42−deriving via (Atom [Char]) instance Breakdown [Char]
46+deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, Breakdown x) => Breakdown (Maybe x)
4347
44−class GBreakdown (f :: k -> *) where
48+instance (Hashable x, Typeable x, Breakdown x) => Breakdown [x] where
49+ breakdownExtras stash = concatMap (NEL.toList . breakdown) stash
50+
51+
52+class GBreakdown (f :: k -> Type) where
4553 gBreakdownExtras :: f a -> [(Dynamic, Int)]
4654
4755instance (Hashable x, Typeable x, Generic x, GBreakdown (Rep x)) => Breakdown (Compound (x :: Type)) where
@@ -66,3 +74,8 @@ instance (GBreakdown a, GBreakdown b) => GBreakdown (a :+: b) where
6674
6775instance (Hashable a, Typeable a, Breakdown a) => GBreakdown (K1 R a) where
6876 gBreakdownExtras (K1 c) = NEL.toList $ breakdown c
77+
78+instance GBreakdown U1 where
79+ gBreakdownExtras U1 = []
80+
81+deriving via (Atom NoContent) instance Breakdown NoContent
src/Roboservant/Types/BuildFrom.hs+12−5
@@ -12,7 +12,7 @@
1212
1313module Roboservant.Types.BuildFrom where
1414
15−import Control.Monad (filterM)
15+import Data.List(nub)
1616import qualified Data.Dependent.Map as DM
1717import Data.Hashable
1818import qualified Data.IntSet as IntSet
@@ -23,6 +23,8 @@ import Data.Typeable (Typeable)
2323import GHC.Generics
2424import Roboservant.Types.Internal
2525import qualified Type.Reflection as R
26+import Servant(NoContent)
27+import Roboservant.Types.Orphans()
2628
2729buildFrom :: forall x. (Hashable x, BuildFrom x, Typeable x) => Stash -> Maybe (StashValue x)
2830buildFrom = buildStash . buildFrom'
@@ -60,10 +62,13 @@ deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, BuildFrom x)
6062
6163-- this isn't wonderful, but we need a hand-rolled instance for recursive datatypes right now.
6264-- with an arbitrary-ish interface, we could use a size parameter, rng access etc.
63−instance (BuildFrom x) => BuildFrom [x] where
64− extras stash = map (\xs -> (concatMap fst xs, map snd xs)) $ powerset $ extras @x stash
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
6568 where
66− powerset xs = filterM (const [True, False]) xs
69+ -- powerset creates way too much stuff. something better here eventually.
70+ notpowerset xs = []:xs:map pure xs
71+
6772
6873instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x :: Type)) where
6974 extras stash = fmap (Compound . to) <$> gExtras stash
@@ -72,7 +77,7 @@ deriving via (Atom Int) instance BuildFrom Int
7277
7378deriving via (Atom Char) instance BuildFrom Char
7479
75−class GBuildFrom (f :: k -> *) where
80+class GBuildFrom (f :: k -> Type) where
7681 gExtras :: Stash -> [([Provenance], f a)]
7782
7883instance GBuildFrom b => GBuildFrom (M1 D a b) where
@@ -98,3 +103,5 @@ instance BuildFrom a => GBuildFrom (K1 i a) where
98103
99104instance GBuildFrom U1 where
100105 gExtras _ = [([], U1)]
106+
107+deriving via (Atom NoContent) instance BuildFrom NoContent
src/Roboservant/Types/FlattenServer.hsdeleted−44
@@ -1,44 +0,0 @@
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− ( HasServer (x :> api) '[],
32− Endpoints (x :> api) ~ '[x :> api]
33− ) =>
34− FlattenServer (x :> api)
35− where
36− flattenServer server = server `AnEndpoint` NoEndpoints
37−
38−instance
39− ( Endpoints (Verb method statusCode contentTypes responseType) ~ '[Verb method statusCode contentTypes responseType],
40− HasServer (Verb method statusCode contentTypes responseType) '[]
41− ) =>
42− FlattenServer (Verb method statusCode contentTypes responseType)
43− where
44− 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)
46+ deriving newtype (Hashable, Typeable,Eq)
4747
4848-- | can be broken down and built up from generic pieces
4949newtype Compound x = Compound {unCompound :: x}
50− deriving newtype (Hashable, Typeable)
50+ deriving newtype (Hashable, Typeable, Eq)
5151
5252hashedDyn :: (Hashable a, Typeable a) => a -> (Dynamic, Int)
5353hashedDyn a = (toDyn a, hash a)
src/Roboservant/Types/Orphans.hsnew file+7
@@ -0,0 +1,7 @@
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+56−49
@@ -1,3 +1,8 @@
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+
16{-# LANGUAGE AllowAmbiguousTypes #-}
27{-# LANGUAGE DataKinds #-}
38{-# LANGUAGE DerivingStrategies #-}
@@ -15,25 +20,22 @@
1520{-# LANGUAGE CPP #-}
1621module Roboservant.Types.ReifiedApi where
1722
18−import Control.Monad.Except (runExceptT)
19−import Data.Bifunctor
2023import Data.Dynamic (Dynamic)
21−import Data.Kind
24+import Control.Exception(Exception)
2225import Data.List.NonEmpty (NonEmpty)
2326import Data.Typeable (Typeable)
2427import GHC.Generics ((:*:)(..))
25−import GHC.TypeLits (Symbol)
2628import Roboservant.Types.Internal
2729import Roboservant.Types.Breakdown
2830import Roboservant.Types.BuildFrom
29−import Roboservant.Types.FlattenServer
31+import Data.Kind(Type)
3032import Servant
3133import Servant.API.Modifiers(FoldRequired,FoldLenient)
34+import GHC.TypeLits (Symbol)
3235import qualified Data.Text as T
3336import qualified Data.Vinyl as V
3437import qualified Data.Vinyl.Curry as V
3538import qualified Type.Reflection as R
36−import Data.Hashable(Hashable)
3739
3840newtype ApiOffset = ApiOffset Int
3941 deriving (Eq, Show, Ord)
@@ -47,61 +49,32 @@ newtype Argument a = Argument
4749
4850data ReifiedEndpoint = forall as. (V.RecordToList as, V.RMap as) => ReifiedEndpoint
4951 { reArguments :: V.Rec (TypedF Argument) as
50− , reEndpointFunc :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int))))
52+ , reEndpointFunc :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
5153 }
5254
53−type ReifiedApi = [(ApiOffset, ReifiedEndpoint)]
54−
55−tagType :: Typeable a => f a -> TypedF f a
56−tagType = (R.typeRep :*:)
57−
58−class ToReifiedApi (endpoints ) where
59−
60− toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
55+instance Show ReifiedEndpoint where
56+ show _ = "lol"
6157
6258class ( V.RecordToList (EndpointArgs endpoint)
6359 , V.RMap (EndpointArgs endpoint)
64− ) => ToReifiedEndpoint (endpoint :: *) where
60+ ) => ToReifiedEndpoint (endpoint :: Type) where
6561 type EndpointArgs endpoint :: [Type]
6662 type EndpointRes endpoint :: Type
6763
6864 reifiedEndpointArguments :: V.Rec (TypedF Argument) (EndpointArgs endpoint)
6965
70−instance ToReifiedApi '[] where
71− toReifiedApi NoEndpoints _ = []
7266
73−instance
74− ( Typeable (EndpointRes endpoint)
67+tagType :: Typeable a => f a -> TypedF f a
68+tagType = (R.typeRep :*:)
69+
70+data InteractionError = InteractionError
71+ { errorMessage :: T.Text
72+ , fatalError :: Bool
73+ }
74+ deriving Show
75+instance Exception InteractionError
76+
7577
76− , NormalizeFunction (ServerT endpoint Handler)
77− , Normal (ServerT endpoint Handler) ~ V.Curried (EndpointArgs endpoint) (IO (Either ServerError (NonEmpty (Dynamic,Int))))
78− , ToReifiedEndpoint endpoint
79− , ToReifiedApi endpoints
80− ) =>
81− ToReifiedApi (endpoint : endpoints)
82− where
83− toReifiedApi (endpoint `AnEndpoint` endpoints) _ =
84− (0, ReifiedEndpoint
85− { reArguments = reifiedEndpointArguments @endpoint
86− , reEndpointFunc = normalize endpoint
87− }
88− )
89− : (map . first) (+1)
90− (toReifiedApi endpoints (Proxy @endpoints))
91−
92−class NormalizeFunction m where
93− type Normal m
94− normalize :: m -> Normal m
95−
96−instance NormalizeFunction x => NormalizeFunction (r -> x) where
97− type Normal (r -> x) = r -> Normal x
98− normalize = fmap normalize
99−
100−instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x) where
101− type Normal (Handler x) = IO (Either ServerError (NonEmpty (Dynamic,Int)))
102− normalize handler = (runExceptT . runHandler') handler >>= \case
103− Left serverError -> pure (Left serverError)
104− Right x -> pure $ Right $ breakdown x
10578
10679instance
10780 (Typeable responseType, Breakdown responseType) =>
@@ -111,6 +84,12 @@ instance
11184 type EndpointRes (Verb method statusCode contentTypes responseType) = responseType
11285 reifiedEndpointArguments = V.RNil
11386
87+instance ToReifiedEndpoint (NoContentVerb method)
88+ where
89+ type EndpointArgs (NoContentVerb method) = '[]
90+ type EndpointRes (NoContentVerb method) = NoContent
91+ reifiedEndpointArguments = V.RNil
92+
11493instance
11594 (ToReifiedEndpoint endpoint) =>
11695 ToReifiedEndpoint ((x :: Symbol) :> endpoint)
@@ -119,6 +98,16 @@ instance
11998 type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
12099 reifiedEndpointArguments = reifiedEndpointArguments @endpoint
121100
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+
122111instance
123112 (ToReifiedEndpoint endpoint) =>
124113 ToReifiedEndpoint (Description s :> endpoint)
@@ -161,6 +150,24 @@ instance
161150 tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
162151 V.:& reifiedEndpointArguments @endpoint
163152
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+
164171instance
165172 ( BuildFrom (IfRequiredLenient T.Text mods headerType)
166173 , ToReifiedEndpoint endpoint
src/Roboservant/Types/ReifiedApi/Server.hsnew file+114
@@ -0,0 +1,114 @@
1+{-# LANGUAGE RankNTypes #-}
2+{-# LANGUAGE TypeApplications #-}
3+{-# LANGUAGE PolyKinds #-}
4+{-# LANGUAGE AllowAmbiguousTypes #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE GADTs #-}
7+{-# LANGUAGE LambdaCase #-}
8+{-# LANGUAGE PolyKinds #-}
9+
10+{-# LANGUAGE ScopedTypeVariables #-}
11+{-# LANGUAGE TypeApplications #-}
12+{-# LANGUAGE TypeFamilies #-}
13+{-# LANGUAGE UndecidableInstances #-}
14+{-# LANGUAGE CPP #-}
15+{-# LANGUAGE DataKinds #-}
16+{-# LANGUAGE TypeOperators #-}
17+{-# LANGUAGE FlexibleInstances #-}
18+{-# LANGUAGE MultiParamTypeClasses #-}
19+module Roboservant.Types.ReifiedApi.Server(module Roboservant.Types.ReifiedApi.Server) where
20+
21+import Servant
22+
23+import Control.Monad.Except (runExceptT)
24+import Data.Bifunctor
25+import Data.Dynamic (Dynamic)
26+import Data.List.NonEmpty (NonEmpty)
27+import Data.Typeable (Typeable)
28+import Roboservant.Types.Breakdown
29+import Roboservant.Types.ReifiedApi
30+
31+import qualified Data.Text as T
32+import qualified Data.Vinyl.Curry as V
33+import Data.Hashable(Hashable)
34+
35+type ReifiedApi = [(ApiOffset, ReifiedEndpoint )]
36+
37+
38+class ToReifiedApi endpoints where
39+ toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
40+
41+instance ToReifiedApi '[] where
42+ toReifiedApi NoEndpoints _ = []
43+
44+instance
45+ ( NormalizeFunction (ServerT endpoint Handler)
46+ , Normal (ServerT endpoint Handler) ~ V.Curried (EndpointArgs endpoint) (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
47+ , ToReifiedEndpoint endpoint
48+ , ToReifiedApi endpoints
49+ ) =>
50+ ToReifiedApi (endpoint : endpoints)
51+ where
52+ toReifiedApi (endpoint `AnEndpoint` endpoints) _ =
53+ (0, ReifiedEndpoint
54+ { reArguments = reifiedEndpointArguments @endpoint
55+ , reEndpointFunc = normalize endpoint
56+ }
57+ )
58+ : (map . first) (+1)
59+ (toReifiedApi endpoints (Proxy @endpoints))
60+
61+
62+instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x) where
63+ type Normal (Handler x) = IO (Either InteractionError (NonEmpty (Dynamic,Int)))
64+ normalize handler = (runExceptT . runHandler') handler >>= \case
65+ Left serverError -> pure (Left (renderServerError serverError))
66+ where
67+ -- | TODO improve this
68+ renderServerError :: ServerError -> InteractionError
69+ renderServerError s = InteractionError (T.pack $ show s) (errHTTPCode serverError == 500)
70+
71+ Right x -> pure $ Right $ breakdown x
72+
73+
74+-- case errHTTPCode serverError of
75+-- 500 -> throw serverError
76+-- _ ->
77+-- liftIO . logInfo . show $ ("ignoring non-500 error", serverError)
78+
79+
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 '[]
84+
85+class FlattenServer api where
86+ flattenServer :: Server api -> Bundled (Endpoints api)
87+
88+instance
89+ ( FlattenServer api,
90+ Endpoints endpoint ~ '[endpoint]
91+ ) =>
92+ FlattenServer (endpoint :<|> api)
93+ where
94+ flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
95+
96+instance
97+ (
98+ Endpoints api ~ '[api]
99+ ) =>
100+ FlattenServer (x :> api)
101+ where
102+ flattenServer server = server `AnEndpoint` NoEndpoints
103+
104+instance FlattenServer (Verb method statusCode contentTypes responseType)
105+ where
106+ flattenServer server = server `AnEndpoint` NoEndpoints
107+
108+class NormalizeFunction m where
109+ type Normal m
110+ normalize :: m -> Normal m
111+
112+instance NormalizeFunction x => NormalizeFunction (r -> x) where
113+ type Normal (r -> x) = r -> Normal x
114+ normalize = fmap normalize
stack.yaml+5−10
@@ -1,15 +1,10 @@
1−resolver: lts-16.27
1+resolver: lts-20.4
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
68- servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
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
9+# - constraints-extras-0.3.0.2@sha256:013b8d0392582c6ca068e226718a4fe8be8e22321cc0634f6115505bf377ad26,1853
10+- some-1.0.3
stack.yaml.lock+9−58
@@ -7,69 +7,20 @@ packages:
77- completed:
88 hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
99 pantry-tree:
10− size: 325
1110 sha256: 04f12c7bef2c3f9a25d94eb9489752ed498db8e243069fe95838dbb51df1dcb3
11+ size: 325
1212 original:
1313 hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
1414- completed:
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
15+ hackage: some-1.0.3@sha256:938cec5fc0d3a08dde34cfc0acd5a0aa2e9fd489c9dbb64417f16588dfb47af0,2399
6516 pantry-tree:
66− size: 2614
67− sha256: 3ac7430134439e4b67f0f5333f63b89d0cb7de5e2e07f0af7801c8e223942b9c
17+ sha256: b88dddc6b991cd570c6750f8d309a9feb76938b29a690abdabc4ababb8c7b2f8
18+ size: 709
6819 original:
69− hackage: servant-server-0.18.2
20+ hackage: some-1.0.3
7021snapshots:
7122- completed:
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
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
test/Breakdown.hs+1−1
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
test/Nested.hs+3−3
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE DeriveGeneric #-}
2+
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
@@ -11,7 +11,7 @@ import Servant
1111import Servant.API.Flatten
1212
1313type Api =
14− ( "one" :> Post '[JSON] Int
14+ ( "one" :> Summary "foo" :> 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−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5−{-# LANGUAGE OverloadedStrings #-}
4+
5+
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−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
test/Put.hsnew file+33
@@ -0,0 +1,33 @@
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.hsnew file+15
@@ -0,0 +1,15 @@
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+105−65
@@ -4,7 +4,13 @@
44{-# LANGUAGE RecordWildCards #-}
55{-# LANGUAGE StandaloneDeriving #-}
66{-# LANGUAGE TypeApplications #-}
7+{-# LANGUAGE RankNTypes #-}
8+{-# LANGUAGE FlexibleContexts #-}
9+{-# LANGUAGE AllowAmbiguousTypes #-}
10+{-# LANGUAGE ScopedTypeVariables #-}
11+{-# LANGUAGE DataKinds #-}
712{-# OPTIONS_GHC -fno-warn-orphans #-}
13+{-# LANGUAGE TypeFamilies #-}
814
915import qualified Breakdown
1016import Data.Dynamic (toDyn)
@@ -15,100 +21,134 @@ import qualified Foo
1521import qualified Headers
1622import qualified Nested
1723import qualified Post
24+import qualified Put
1825import qualified Product
19−import qualified Roboservant as RS
26+import qualified QueryParams
27+import qualified Roboservant as R
28+import qualified Roboservant.Server as RS
29+import qualified Roboservant.Client as RC
2030import qualified Seeded
2131import Test.Hspec
2232import Test.Hspec.Core.Spec (FailureReason (Reason), ResultStatus (Failure, Success), itemExample, mapSpecItem_, resultStatus)
2333import 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((>=>))
2441
2542main :: IO ()
2643main = hspec spec
2744
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+
2865spec :: Spec
2966spec = do
3067 describe "Basic usage" $ do
3168 describe "noError" $ do
32− it "finds no error in a valid app" $ do
33− RS.fuzz @Valid.Api Valid.server RS.defaultConfig
34− >>= (`shouldSatisfy` isNothing)
35− it "finds no error in a valid generic app" $ do
36− RS.fuzz @Valid.RoutedApi Valid.routedServer RS.defaultConfig
37− >>= (`shouldSatisfy` isNothing)
38− it "does fail coverage check" $ do
39− r <- RS.fuzz @Valid.Api Valid.server RS.defaultConfig {RS.coverageThreshold = 0.6}
40− fmap (RS.failureReason . RS.rsException) r
41− `shouldSatisfy` ( \case
42− Just (RS.InsufficientCoverage _) -> True
43− _ -> False
44− )
45− describe "posted body" $ do
46− it "passes a coverage check using a posted body" $ do
47− RS.fuzz @Post.Api Post.server RS.defaultConfig {RS.coverageThreshold = 0.99}
48− >>= (`shouldSatisfy` isNothing)
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+
4988 describe "seeded" $ do
50− shouldFail
51− $ it "finds an error using information passed in"
52− $ let res = Seeded.Seed 1
53− in RS.fuzz @Seeded.Api Seeded.server (RS.defaultConfig {RS.seed = [(toDyn res, hash res)]})
54− >>= (`shouldSatisfy` isNothing)
55− describe "Foo" $ do
56− it "finds an error in a basic app" $
57− RS.fuzz @Foo.Api Foo.server RS.defaultConfig
58− >>= (`shouldSatisfy` serverFailure)
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+
59101 describe "BuildFrom" $ do
60− describe "headers (and sum types)" $ do
61− it "should find a failure that's dependent on using header info" $ do
62− RS.fuzz @Headers.Api Headers.server RS.defaultConfig
63− >>= (`shouldSatisfy` serverFailure)
64− describe "product types" $ do
65− it "should find a failure that's dependent on creating a product" $ do
66− RS.fuzz @Product.Api Product.server RS.defaultConfig {RS.seed = [RS.hashedDyn 'a', RS.hashedDyn (1 :: Int)]}
67− >>= (`shouldSatisfy` serverFailure)
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)
68109 describe "Breakdown" $ do
69− it "handles products" $ do
70− RS.fuzz @Breakdown.ProductApi Breakdown.productServer RS.defaultConfig
71− >>= (`shouldSatisfy` serverFailure)
72− it "handles sums" $ do
73− RS.fuzz @Breakdown.SumApi Breakdown.sumServer RS.defaultConfig
74− >>= (`shouldSatisfy` serverFailure)
75− describe "flattening" $ do
76− -- we don't actually do much here, this is just here to document the appropriate response
77− -- if you get a type error with a nested api.
78− it "can handle nested apis" $ do
79− RS.fuzz @(Nested.FlatApi) Nested.server RS.defaultConfig {RS.coverageThreshold = 0.99}
80− >>= (`shouldSatisfy` isNothing)
81−
82−serverFailure :: Maybe RS.Report -> Bool
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
83119serverFailure = \case
84− Just RS.Report {..} ->
85− let RS.RoboservantException {..} = rsException
86− in failureReason /= RS.NoPossibleMoves
120+ Just R.Report {..} ->
121+ let R.RoboservantException {..} = rsException
122+ in failureReason /= R.NoPossibleMoves
87123 _ -> False
88124
89−deriving via (RS.Atom Foo.Foo) instance RS.Breakdown Foo.Foo
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
90130
91−deriving via (RS.Atom Foo.Foo) instance RS.BuildFrom Foo.Foo
131+deriving via (R.Atom Headers.Foo) instance R.BuildFrom Headers.Foo
92132
93−deriving via (RS.Atom Headers.Foo) instance RS.Breakdown Headers.Foo
133+deriving via (R.Atom Seeded.Seed) instance R.Breakdown Seeded.Seed
94134
95−deriving via (RS.Atom Headers.Foo) instance RS.BuildFrom Headers.Foo
135+deriving via (R.Atom Seeded.Seed) instance R.BuildFrom Seeded.Seed
96136
97−deriving via (RS.Atom Seeded.Seed) instance RS.Breakdown Seeded.Seed
137+deriving via (R.Atom Void) instance R.BuildFrom Void
98138
99−deriving via (RS.Atom Seeded.Seed) instance RS.BuildFrom Seeded.Seed
139+deriving via (R.Atom Post.FooPost) instance R.Breakdown Post.FooPost
140+deriving via (R.Atom Post.FooPost) instance R.BuildFrom Post.FooPost
100141
101−deriving via (RS.Atom Void) instance RS.BuildFrom Void
142+deriving via (R.Atom Put.Foo) instance R.Breakdown Put.Foo
143+deriving via (R.Atom Put.Foo) instance R.BuildFrom Put.Foo
102144
103−deriving via (RS.Atom Post.FooPost) instance RS.Breakdown Post.FooPost
104145
105−deriving via (RS.Atom Post.FooPost) instance RS.BuildFrom Post.FooPost
106146
107−deriving via (RS.Compound Breakdown.Foo) instance RS.Breakdown Breakdown.Foo
147+deriving via (R.Compound Breakdown.Foo) instance R.Breakdown Breakdown.Foo
108148
109−deriving via (RS.Compound Product.Foo) instance RS.BuildFrom Product.Foo
149+deriving via (R.Compound Product.Foo) instance R.BuildFrom Product.Foo
110150
111−deriving via (RS.Compound Breakdown.SomeSum) instance RS.Breakdown Breakdown.SomeSum
151+deriving via (R.Compound Breakdown.SomeSum) instance R.Breakdown Breakdown.SomeSum
112152
113153-- | `shouldFail` allows you to assert that a given `Spec` should contain at least one failing test.
114154-- this is often useful when testing tests.
@@ -118,7 +158,7 @@ shouldFail =
118158 ( \i ->
119159 i
120160 { itemExample = \p a cb -> do
121− r <- (itemExample i) p a cb
161+ r <- itemExample i p a cb
122162 pure
123163 r
124164 { resultStatus = case resultStatus r of
test/UnsafeIO.hs+3−3
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE DeriveGeneric #-}
2+
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
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+2−3
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
@@ -10,7 +10,6 @@ module Valid where
1010import Data.Void
1111import GHC.Generics
1212import Servant
13−import Servant.API.Generic
1413import Servant.Server.Generic
1514
1615type Api =
@@ -21,7 +20,7 @@ data Routes route
2120 = Routes
2221 { getInt ::
2322 route
24− :- Get '[JSON] Int,
23+ :- Summary "i'm a summary!" :> Get '[JSON] Int,
2524 captureIt ::
2625 route
2726 :- Capture "void" Void :> Get '[JSON] ()