on 3deefd6 help sign in
39 files changed+520−1244
since splitsnapshots
unifiedsplit
.github/workflows/cabal.yml−64 .github/workflows/ci.yml+2−2 EXAMPLE.md+42−71 Makefile+1−1 README.md+1−90 benchmarks/README.md−30 benchmarks/genFile.sh−26 benchmarks/prelude−61 benchmarks/runTiming.sh−10 benchmarks/runTimingNoisy.sh−10 package.yaml+3−13 roboservant.cabal+6−26 src/Roboservant.hs+2−5 src/Roboservant/Client.hs−127 src/Roboservant/Direct.hs+60−60 src/Roboservant/Server.hs−36 src/Roboservant/Types.hs+8−10 src/Roboservant/Types/Breakdown.hs+14−31 src/Roboservant/Types/BuildFrom.hs+43−49 src/Roboservant/Types/Config.hs−29 src/Roboservant/Types/FlattenServer.hs+44 src/Roboservant/Types/Internal.hs+20−24 src/Roboservant/Types/Orphans.hs−7 src/Roboservant/Types/ReifiedApi.hs+50−58 src/Roboservant/Types/ReifiedApi/Server.hs−114 stack.yaml+11−5 stack.yaml.lock+58−9 test/Breakdown.hs+11−11 test/Foo.hs+1−3 test/Headers.hs+3−4 test/Nested.hs−23 test/Post.hs+9−13 test/Product.hs+2−4 test/Put.hs−33 test/QueryParams.hs−15 test/Seeded.hs+5−9 test/Spec.hs+95−130 test/UnsafeIO.hs+15−13 test/Valid.hs+14−18
.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+42−71
@@ -2,7 +2,7 @@
22
33Our api under test:
44
5−``` haskell
5+```haskell
66-- Obligatory fancy-types pragma tax
77{-# LANGUAGE DataKinds #-}
88{-# LANGUAGE TypeOperators #-}
@@ -13,43 +13,30 @@ 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
29−import Data.Maybe(isNothing, isJust)
30−import qualified Network.Wai.Handler.Warp as Warp
31−import Data.Aeson(FromJSON,ToJSON)
3223
3324newtype A = A Int
3425 deriving (Generic, Eq, Show, Typeable)
3526 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
3627
37−instance FromJSON A
38−instance ToJSON A
39−
4028newtype B = B Int
4129 deriving (Generic, Eq, Show, Typeable)
4230 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
4331
44−instance FromJSON B
45−instance ToJSON B
46−
4732type Api =
4833 "item" :> Get '[JSON] A
4934 :<|> "itemAdd" :> Capture "one" B :> Capture "two" B :> Get '[JSON] B
5035 :<|> "item" :> Capture "itemId" B :> Get '[JSON] ()
5136
52−server :: Handler A -> Server Api
37+goodServer = server (pure $ A 0)
38+badServer = server (pure $ A 1)
39+
5340server introduce = introduce :<|> combine :<|> eliminate
5441 where
5542 combine (B i) (B j) = pure $ B (i + j)
@@ -58,66 +45,45 @@ server introduce = introduce :<|> combine :<|> eliminate
5845 | otherwise = pure ()
5946```
6047
61−We have a "good" server, that never generates anything other than a 0. This means repeated application of
62−the combination/addition rule can never bring us to the dangerous state of numbers larger than 10.
48+In the test file, we first define the configuration:
49+
50+```haskell
51+defaultConfig :: Config
52+defaultConfig = Config {
53+ -- you can pass extra values in using the seed argument. This can be useful
54+ -- for things that might not be produceable within the api, like auth tokens.
55+ seed = [hashedDyn "blah"]
56+ , maxRuntime = 0.5
57+ -- if we get to 1000 interactions with the api, call it quits.
58+ , maxReps = 1000
59+ -- if you're using this inside quickcheck or hedgehog, you might want to set this
60+ -- from their seed to make sure it stays deterministic
61+ , rngSeed = 0
62+ -- 0 to 100: fail tests if we hit less than this percentage of endpoints.
63+ , coverageThreshold = 0
64+ }
6365
64−``` haskell
65−goodServer, badServer :: Server Api
66−goodServer = server (pure $ A 0)
67−badServer = server (pure $ A 1)
6866```
6967
70−In the test file, we first define the tests: the faulty server should fail and the good server should pass.
68+and the tests: the faulty server should fail and the good server should pass.
7169
7270```haskell
73−main :: IO ()
74−main = hspec spec
75−
76−spec :: Spec
7771spec = describe "example" $ do
7872 it "good server should not fail" $ do
79− RS.fuzz @Api goodServer config
73+ fuzz @Api goodServer defaultConfig { coverageThreshold = 0.99 } (pure ())
8074 >>= (`shouldSatisfy` isNothing)
8175 it "bad server should fail" $ do
82− RS.fuzz @Api badServer config
83− >>= (`shouldSatisfy` isJust)
84−```
85−
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.)
97−
98−We expect to be able to cover the whole api from our starting point, so let's set the coverage to 0.99.
99−There are other tweakable things in the config, like maximum runtime, reps,
100−per-request healthchecks, seeds, and verbose logging. Have a look at
101−Roboservant.Types.Config for details.
102−
103−``` haskell
104−config :: Config
105−config = defaultConfig
106− { coverageThreshold = 0.99
107− }
76+ fuzz @Api badServer defaultConfig { coverageThreshold = 0.99 } (pure ())
77+ >>= (`shouldSatisfy` serverFailure)
10878```
10979
110−Unless we want to ship roboservant and all its dependencies to production, we also need
80+And unless we want to ship roboservant and all its dependencies to production, we also need
11181some orphan instances: because As are the only value we can get without
11282an input, we need to be able to break them down.
11383
114−``` haskell
84+```haskell
11585deriving via (Compound A) instance Breakdown A
116−```
117−
118−if we wanted to assemble As from parts as well, we'd derive using Compound, but in this case we don't care.
119−
120−``` haskell
86+-- if we wanted to assemble As from parts as well, we'd derive using Compound
12187deriving via (Atom A) instance BuildFrom A
12288
12389```
@@ -130,14 +96,19 @@ deriving via (Compound B) instance BuildFrom B
13096deriving via (Atom B) instance Breakdown B
13197```
13298
99+finally some uninteresting utilities and the entrypoint
100+
101+```haskell
102+main = hspec spec
133103
134−test utilities:
104+serverFailure :: Maybe Report -> Bool
105+serverFailure c = case c of
106+ Just Report{..} ->
107+ let RoboservantException{..} = rsException
108+ in failureReason /= NoPossibleMoves
109+ _ -> False
135110
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 })
111+isNothing x = case x of
112+ Nothing -> True
113+ _ -> False
143114```
Makefile+1−1
@@ -1,4 +1,4 @@
11testwatch:
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
2+ ghcid -T main -c 'stack repl --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+1−90
@@ -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
@@ -15,71 +14,6 @@ Servant gives us a lot of information about what a server can do. We
1514use this information to generate arbitrarily long request/response
1615sessions and verify properties that should hold over them.
1716
18−## how?
19−
20−In essence, ```fuzz @Api yourServer config``` will make a bunch of
21−calls to your API, and record the results in a type-indexed
22−dictionary. This means that they are now available for the
23−prerequisites of other calls, so as you proceed, more and more api
24−calls become possible.
25−
26−We explicitly do not try to come up with plausible values that haven't
27−somehow come back from the API. That's straying into QC/Hedgehog
28−territory: if you want that, come up with the values on that side, and
29−set them as seeds in the configuration.
30−
31−### what does it mean to be "available"?
32−
33−In a simple API, you may make a call and get back a `Foo`, which will
34−allow you to make another call that requires a `Foo`. In a more
35−complicated app, it's likely that you'll send a request body that
36−includes many subcomponents, and it's likely you'll get a response
37−that needs to be broken down into pieces before it's useful.
38−
39−To cope with this, we have the typeclasses `BuildFrom` and
40−`Breakdown`. You can write instances for them if you feel like it, and
41−indeed it's currently required for recursive datatypes if you don't
42−want the fuzzer to hang, but for the majority of your types it should
43−be sufficient to derive them generically. (Sensible instances are
44−provided for lists.)
45−
46−There are two basic strategies here. In some cases, you want to regard
47−a type as indivisible: that's why we like newtypes, right? In this
48−case, we can derive using the `Atom` strategy.
49−
50−``` haskell
51−deriving via (Atom NewtypedKey) instance Breakdown NewtypedKey
52−deriving via (Atom NewtypedKey) instance BuildFrom NewtypedKey
53−```
54−
55−This is saying "A can neither be built from components or broken down
56−for spare parts. Hands off!". This is a good strategy for key types,
57−for instance.
58−
59−If instead it's a big complicated thing with lots of juicy
60−subcomponents, we want to rip it apart using Generics and feast on
61−its succulent headmeats:
62−
63−``` haskell
64−deriving via (Compound Payload) instance Breakdown Payload
65−deriving via (Compound Payload) instance BuildFrom Payload
66−```
67−
68−### priming the pump
69−
70−Sometimes there are values we'd like to smuggle into the API that are
71−not derivable from within the API itself: sometimes this is a warning
72−sign that your API is incomplete, but it can be quite reasonable to
73−require identifying credentials within an API and not provide a way to
74−get them. It might also be reasonable to have some sample values that
75−the user is expected to come up with.
76−
77−For those cases, override the `seed` in the `Config` with a
78−list of seed values, suitably hashed:
79−
80−``` haskell
81−defaultConfig { seed = [hashedDyn creds, hashedDyn userJwt]}
82−```
8317
8418## why not servant-quickcheck?
8519
@@ -89,26 +23,3 @@ there's a lot of the state space you just can't explore without context: modern
8923full of pointer-like structures, whether they're URLs or database
9024keys/uuids, and servant-quickcheck requires that you be able to generate
9125these without context via Arbitrary.
92−
93−## limitations and future work
94−
95−Currently, the display of failing traces is pretty tragic, both in the
96−formatting and in its non-minimality. This is pretty ticklish:
97−arguably the right way to do this is to return a trace that we can
98−also rerun, and let quickcheck or hedgehog a level up shrink it until
99−it's satisfactorily short. In the interest of being useful earlier
100−rather than later, I'm releasing v1.0 before I crack this particular
101−nut. We do know which calls we made that led to the failing case, so
102−we would want to show that distinction in a visible way: it's possible
103−that other calls that don't have direct data dependencies were
104−important, but we definitely know we need the direct data dependencies.
105−
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−
109−It would also be nice to have a robust strategy for deriving recursive
110−datatypes, or at least rejecting attempts to generate them that don't
111−end 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/README.mddeleted−30
@@ -1,30 +0,0 @@
1−# Benchmarks
2−
3−I've noticed compilation can be a little slow on realistic apps so I set up a test.
4−
5−## components
6−
7−1. codegen
8−
9− given an integer input n, create an api with n endpoints. they should take a reasonably
10− complicated input and return a reasonably complicated output
11−
12−2. runner
13−
14− build dependencies
15− for i from 1 to 100
16− generate api with i endpoints
17− compile it, but timed
18− print (i,time)
19−
20−3. plot to see linearity/quadraticity?
21−
22−## possible gotchas
23−
24−- we usually compile these in different modules rather than together: if we don't get slow
25− compilation in one file, try out multiple files.
26−
27−## notes
28−
29−- flat profile if you don't have the call to `fuzz`
30−- no difference between deriving via Atom vs Compound, so not a Generics thing.
benchmarks/genFile.shdeleted−26
@@ -1,26 +0,0 @@
1−n=$1
2−k=$2
3−
4−cat prelude
5−echo -n "type SubApi = \"route1\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
6−for i in $(seq 2 $n); do
7− echo -n " :<|> \"route${i}\" :> "
8− echo "ReqBody '[JSON] Input :> Post '[JSON] Output"
9−done
10−
11−echo -n "server = handler "
12−
13−for i in $(seq 2 $n); do
14− echo -n ":<|> handler "
15−done
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/preludedeleted−61
@@ -1,61 +0,0 @@
1−{-# LANGUAGE LambdaCase #-}
2−{-# LANGUAGE TypeOperators #-}
3−{-# LANGUAGE DataKinds #-}
4−{-# LANGUAGE TypeApplications #-}
5−{-# LANGUAGE DerivingVia #-}
6−{-# LANGUAGE StandaloneDeriving #-}
7−{-# LANGUAGE DeriveGeneric #-}
8−
9−
10−import Roboservant
11−import Servant
12−import Data.Hashable
13−import GHC.Generics
14−import Data.Aeson
15−import Servant.API.Flatten
16−
17−data Input = Input1 { foo :: String }
18− | Input2 { bar :: Int }
19− deriving (Generic, Eq)
20−
21−instance FromJSON Input
22−instance ToJSON Input
23−
24−
25−deriving via (Compound Input) instance Breakdown Input
26−deriving via (Compound Input) instance BuildFrom Input
27−
28−
29−-- deriving via (Atom Input) instance Breakdown Input
30−-- deriving via (Atom Input) instance BuildFrom Input
31−
32−instance Hashable Input
33−
34−
35−data Output = Output1 { ofoo :: String }
36− | Output2 { obar :: Int }
37− deriving (Generic, Eq)
38−
39−instance FromJSON Output
40−instance ToJSON Output
41−
42−instance Hashable Output
43−
44−deriving via (Compound Output) instance Breakdown Output
45−deriving via (Compound Output) instance BuildFrom Output
46−
47−-- deriving via (Atom Output) instance Breakdown Output
48−-- deriving via (Atom Output) instance BuildFrom Output
49−
50−
51−handler :: Input -> Handler Output
52−handler = pure . \case
53− Input1 foo -> Output1 foo
54− Input2 bar -> Output2 bar
55−
56−main =
57− -- fuzz @Api server defaultConfig
58− pure ()
59−
60−f = flatten (Proxy @Api)
61−
benchmarks/runTiming.shdeleted−10
@@ -1,10 +0,0 @@
1−lo=$1
2−hi=$2
3−k=$3
4−for i in $(seq $lo 5 $hi); do
5− ./genFile.sh $i $k > main.hs;
6− echo "sievedn: $i,";
7− /usr/bin/time -f "%e" stack ghc main.hs 2>&1;
8−# /usr/bin/time -f "%e" stack ghc -- -fomit-interface-pragmas main.hs 2>&1;
9−
10−done | grep -v Compiling | grep -v Linking | awk '/,$/ { printf("%s", $0); next } 1' | sed 's/sievedn: //'
benchmarks/runTimingNoisy.shdeleted−10
@@ -1,10 +0,0 @@
1−lo=$1
2−hi=$2
3−
4−for i in $(seq $lo 5 $hi); do
5− ./genFile.sh $i > main.hs;
6− echo "sievedn: $i,";
7− /usr/bin/time -f "%e" stack ghc main.hs 2>&1;
8−# /usr/bin/time -f "%e" stack ghc -- -fomit-interface-pragmas main.hs 2>&1;
9−
10−done
package.yaml+3−13
@@ -1,6 +1,5 @@
1−
21name: roboservant
3−version: 0.1.0.3
2+version: 0.1.0.2
43github: "mwotton/roboservant"
54license: BSD3
65copyright: "2020 Mark Wotton, Samuel Schlesinger"
@@ -19,7 +18,6 @@ dependencies:
1918- containers
2019- random
2120- hashable
22−- http-types
2321- lifted-base
2422- monad-control
2523- mtl
@@ -56,10 +54,7 @@ tests:
5654 - hspec
5755 - hspec-core
5856 - http-api-data
59− - http-client
60− - hspec-wai
61− - wai
62− - warp
57+
6358 example:
6459 main: Example.lhs
6560 source-dirs: .
@@ -67,10 +62,5 @@ tests:
6762 dependencies:
6863 - roboservant
6964 - hspec
70− - warp
71− - http-client
72− - aeson
73− - hspec-core
65+ build-depends: base, markdown-unlit
7466 ghc-options: -pgmL markdown-unlit
75−
76− build-tools: markdown-unlit
roboservant.cabal+6−26
@@ -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: 8de468df6e784f3723af9155bba9da9f43659b7d4e8e465fdac26eafcb0d4306
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,17 +28,13 @@ source-repository head
2828library
2929 exposed-modules:
3030 Roboservant
31− Roboservant.Client
3231 Roboservant.Direct
33− Roboservant.Server
3432 Roboservant.Types
3533 Roboservant.Types.Breakdown
3634 Roboservant.Types.BuildFrom
37− Roboservant.Types.Config
35+ Roboservant.Types.FlattenServer
3836 Roboservant.Types.Internal
39− Roboservant.Types.Orphans
4037 Roboservant.Types.ReifiedApi
41− Roboservant.Types.ReifiedApi.Server
4238 other-modules:
4339 Paths_roboservant
4440 hs-source-dirs:
@@ -51,7 +47,6 @@ library
5147 , dependent-map
5248 , dependent-sum
5349 , hashable
54− , http-types
5550 , lifted-base
5651 , monad-control
5752 , mtl
@@ -73,22 +68,16 @@ test-suite example
7368 other-modules:
7469 Paths_roboservant
7570 hs-source-dirs:
76− ./
71+ ./.
7772 ghc-options: -Wall -fwrite-ide-info -hiedir=.hie -pgmL markdown-unlit
78− build-tool-depends:
79− markdown-unlit:markdown-unlit
8073 build-depends:
81− aeson
82− , base >=4.7 && <5
74+ base >=4.7 && <5
8375 , bytestring
8476 , containers
8577 , dependent-map
8678 , dependent-sum
8779 , hashable
8880 , hspec
89− , hspec-core
90− , http-client
91− , http-types
9281 , lifted-base
9382 , monad-control
9483 , mtl
@@ -103,7 +92,6 @@ test-suite example
10392 , time
10493 , unordered-containers
10594 , vinyl
106− , warp
10795 default-language: Haskell2010
10896
10997test-suite roboservant-test
@@ -113,11 +101,8 @@ test-suite roboservant-test
113101 Breakdown
114102 Foo
115103 Headers
116− Nested
117104 Post
118105 Product
119− Put
120− QueryParams
121106 Seeded
122107 UnsafeIO
123108 Valid
@@ -135,10 +120,7 @@ test-suite roboservant-test
135120 , hashable
136121 , hspec
137122 , hspec-core
138− , hspec-wai
139123 , http-api-data
140− , http-client
141− , http-types
142124 , lifted-base
143125 , monad-control
144126 , mtl
@@ -153,6 +135,4 @@ test-suite roboservant-test
153135 , time
154136 , unordered-containers
155137 , vinyl
156− , wai
157− , warp
158138 default-language: Haskell2010
src/Roboservant.hs+2−5
@@ -1,8 +1,5 @@
1−module Roboservant
2− ( module Roboservant.Direct,
3− module Roboservant.Types,
4− )
5−where
1+module Roboservant (module Roboservant.Direct
2+ ,module Roboservant.Types) where
63
74import Roboservant.Direct
85import Roboservant.Types
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+60−60
@@ -2,17 +2,16 @@
22{-# LANGUAGE FlexibleContexts #-}
33{-# LANGUAGE GADTs #-}
44{-# LANGUAGE LambdaCase #-}
5−{-# LANGUAGE PolyKinds #-}
65{-# LANGUAGE RankNTypes #-}
76{-# LANGUAGE RecordWildCards #-}
87{-# LANGUAGE ScopedTypeVariables #-}
98{-# LANGUAGE TupleSections #-}
10−
9+{-# LANGUAGE TypeApplications #-}
1110{-# LANGUAGE TypeFamilies #-}
1211{-# LANGUAGE UndecidableInstances #-}
1312
1413module Roboservant.Direct
15− ( fuzz',
14+ ( fuzz,
1615 Config (..),
1716 -- TODO come up with something smarter than exporting all this, we should
1817 -- have some nice error-display functions
@@ -25,30 +24,25 @@ module Roboservant.Direct
2524where
2625
2726import Control.Exception.Lifted
28− ( Exception,
29− Handler (Handler),
30− SomeAsyncException,
31− SomeException,
32− catch,
33− catches,
34− handle,
35− throw,
36− )
27+ ( SomeException,
28+ throw,
29+ Exception,
30+ SomeAsyncException,
31+ catch,
32+ catches,
33+ handle,
34+ Handler(Handler) )
3735import Control.Monad.State.Strict
38− ( MonadIO (..),
39− MonadState (get),
40− StateT (runStateT),
41− modify',
42− )
36+ ( MonadIO(..), StateT(runStateT), modify', MonadState(get) )
4337import Control.Monad.Trans.Control (MonadBaseControl)
4438import qualified Data.Dependent.Map as DM
45−import Data.Dynamic (Dynamic (..))
39+import Data.Dynamic ( Dynamic(..) )
4640import qualified Data.IntSet as IntSet
4741import Data.List.NonEmpty (NonEmpty (..))
4842import qualified Data.List.NonEmpty as NEL
4943import Data.Maybe (mapMaybe)
5044import qualified Data.Set as Set
51−import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
45+import Data.Time.Clock ( addUTCTime, getCurrentTime, UTCTime )
5246import qualified Data.Vinyl as V
5347import qualified Data.Vinyl.Curry as V
5448import qualified Data.Vinyl.Functor as V
@@ -56,17 +50,16 @@ import GHC.Generics ((:*:) (..))
5650import Roboservant.Types
5751 ( ApiOffset (..),
5852 Argument (..),
59− InteractionError(..),
53+ FlattenServer (..),
6054 Provenance (..),
61− ReifiedApi,
6255 ReifiedEndpoint (..),
6356 Stash (..),
6457 StashValue (..),
58+ ToReifiedApi (..),
6559 TypedF,
6660 )
67−import Roboservant.Types.Config
68−
69−import System.Random (Random (randomR), StdGen, mkStdGen)
61+import Servant (Endpoints, Proxy (Proxy), Server, ServerError (..))
62+import System.Random ( mkStdGen, Random(randomR), StdGen )
7063import qualified Type.Reflection as R
7164
7265data RoboservantException
@@ -93,6 +86,15 @@ data FuzzOp
9386 }
9487 deriving (Show, Eq)
9588
89+data Config
90+ = Config
91+ { seed :: [(Dynamic, Int)],
92+ maxRuntime :: Double, -- seconds to test for
93+ maxReps :: Integer,
94+ rngSeed :: Int,
95+ coverageThreshold :: Double
96+ }
97+
9698data FuzzState
9799 = FuzzState
98100 { path :: [FuzzOp],
@@ -105,7 +107,7 @@ data EndpointOption
105107 = forall as.
106108 (V.RecordToList as, V.RMap as) =>
107109 EndpointOption
108− { eoCall :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))),
110+ { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))),
109111 eoArgs :: V.Rec (TypedF StashValue) as
110112 }
111113
@@ -121,18 +123,14 @@ data Report
121123 }
122124 deriving (Show)
123125
124−
125−
126−-- fuzzClient :: Client api -> Config -> IO (Maybe Report)
127−-- fuzzClient = undefined
128−
129−
130−
131−fuzz' ::
132− ReifiedApi ->
126+fuzz ::
127+ forall api.
128+ (FlattenServer api, ToReifiedApi (Endpoints api)) =>
129+ Server api ->
133130 Config ->
131+ IO () ->
134132 IO (Maybe Report)
135−fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
133+fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
136134 let path = []
137135 stash = addToStash seed mempty
138136 currentRng = mkStdGen rngSeed
@@ -141,7 +139,7 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
141139 runStateT
142140 (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get))
143141 FuzzState {..}
144− logInfo $ show stopreason
142+ print stopreason
145143 pure Nothing
146144 where
147145 -- something less terrible later
@@ -150,15 +148,13 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
150148 Report
151149 (unlines [show failureType, show exception])
152150 r
153− displayDiagnostics FuzzState {..} = liftIO $
154− 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)
159− -- <> (map (show . NEL.length . getStashValue ) $ DM.assocs (getStash stash))
160− -- $ \_k v ->
161− -- (show . NEL.length $ getStashValue v))
151+ displayDiagnostics FuzzState {..} = liftIO $ do
152+ putStrLn "api endpoints covered"
153+ mapM_ print (Set.toList $ Set.fromList $ map apiOffset path)
154+ putStrLn ""
155+ putStrLn "types in stash"
156+ DM.forWithKey_ (getStash stash) $ \_k v ->
157+ print (NEL.length $ getStashValue v)
162158
163159 evaluateCoverage f@FuzzState {..}
164160 | coverage > coverageThreshold = pure ()
@@ -178,7 +174,7 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
178174 else do
179175 _ <- action
180176 untilDone (n -1, deadline) action
181−
177+ reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
182178 routeCount = length reifiedApi
183179 elementOrFail ::
184180 (MonadState FuzzState m, MonadIO m) =>
@@ -195,7 +191,7 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
195191 ( forall as.
196192 (V.RecordToList as, V.RMap as) =>
197193 FuzzOp ->
198− V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
194+ V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))) ->
199195 V.Rec (TypedF V.Identity) as ->
200196 m r
201197 ) ->
@@ -236,24 +232,27 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
236232 execute ::
237233 (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
238234 FuzzOp ->
239− V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
235+ V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))) ->
240236 V.Rec (TypedF V.Identity) as ->
241237 m ()
242238 execute fuzzop func args = do
243− (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
239+ (liftIO . print . (fuzzop,) . stash) =<< get
244240 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)) ->
241+ -- parameterise this
242+ Left (serverError :: ServerError) ->
243+ case errHTTPCode serverError of
244+ 500 -> throw serverError
245+ _ ->
246+ liftIO $ print ("ignoring non-500 error", serverError)
247+ Right (dyn :: NEL.NonEmpty (Dynamic, Int)) -> do
248+ -- liftIO $ print ("storing", fmap dynTypeRep dyn)
250249 modify'
251− ( \fs@FuzzState {..} ->
252− fs {stash = addToStash (NEL.toList dyn) stash}
253− )
250+ ( \fs@FuzzState {..} ->
251+ fs {stash = addToStash (NEL.toList dyn) stash}
252+ )
254253 where
255254 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
256− -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
255+ -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
257256 go ::
258257 (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m) =>
259258 m ()
@@ -262,12 +261,13 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
262261 (execute op func args)
263262 [ Handler (\(e :: SomeAsyncException) -> throw e),
264263 Handler
265− ( \(e :: SomeException) ->
264+ ( \(e :: SomeException) -> do
265+ displayDiagnostics =<< get
266266 throw . RoboservantException ServerCrashed (Just e) =<< get
267267 )
268268 ]
269269 catch
270− (liftIO healthCheck)
270+ (liftIO checker)
271271 (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
272272
273273addToStash ::
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+8−10
@@ -8,19 +8,17 @@
88{-# LANGUAGE UndecidableInstances #-}
99
1010module Roboservant.Types
11− ( module Roboservant.Types.Breakdown,
12− module Roboservant.Types.BuildFrom,
13− module Roboservant.Types.ReifiedApi,
14− module Roboservant.Types.ReifiedApi.Server,
15− module Roboservant.Types.Internal,
16− module Roboservant.Types.Config,
11+ ( module Roboservant.Types.Breakdown
12+ , module Roboservant.Types.BuildFrom
13+ , module Roboservant.Types.FlattenServer
14+ , module Roboservant.Types.ReifiedApi
15+ , module Roboservant.Types.Internal
16+
1717 )
1818where
1919
2020import Roboservant.Types.Breakdown
2121import Roboservant.Types.BuildFrom
22−import Roboservant.Types.Config
23−import Roboservant.Types.Internal
22+import Roboservant.Types.FlattenServer
2423import Roboservant.Types.ReifiedApi
25−import Roboservant.Types.ReifiedApi.Server
26−import Roboservant.Types.Orphans()
24+import Roboservant.Types.Internal
src/Roboservant/Types/Breakdown.hs+14−31
@@ -1,6 +1,5 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
22{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE FlexibleContexts #-}
43{-# LANGUAGE DerivingVia #-}
54{-# LANGUAGE FlexibleInstances #-}
65{-# LANGUAGE GADTs #-}
@@ -15,47 +14,36 @@
1514
1615module Roboservant.Types.Breakdown where
1716
18−import Data.Dynamic (Dynamic)
19−import Data.Hashable
20−import Data.Kind
17+import Data.Dynamic (Dynamic, toDyn)
2118import Data.List.NonEmpty (NonEmpty (..))
22−import qualified Data.List.NonEmpty as NEL
23−import Data.Typeable (Typeable)
2419import GHC.Generics
20+import Data.Typeable (Typeable)
2521import Roboservant.Types.Internal
26−import Servant
27−import Roboservant.Types.Orphans()
22+import Data.Hashable
23+import Data.Kind
24+import qualified Data.List.NonEmpty as NEL
2825
29−breakdown ::
30− (Hashable x, Typeable x, Breakdown x) =>
31− x ->
32− NonEmpty (Dynamic, Int)
26+breakdown :: (Hashable x, Typeable x, Breakdown x)
27+ => x -> NonEmpty (Dynamic, Int)
3328breakdown x = hashedDyn x :| breakdownExtras x
3429
3530class Breakdown x where
36− breakdownExtras :: x -> [(Dynamic, Int)]
31+ breakdownExtras :: x -> [(Dynamic,Int)]
3732
3833instance (Hashable x, Typeable x) => Breakdown (Atom x) where
3934 breakdownExtras _ = []
4035
4136deriving via (Atom ()) instance Breakdown ()
42−
4337deriving via (Atom Int) instance Breakdown Int
44−deriving via (Atom Char) instance Breakdown Char
45−
46−deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, Breakdown x) => Breakdown (Maybe x)
47−
48−instance (Hashable x, Typeable x, Breakdown x) => Breakdown [x] where
49− breakdownExtras stash = concatMap (NEL.toList . breakdown) stash
38+deriving via (Atom [Char]) instance Breakdown [Char]
5039
40+class GBreakdown (f :: k -> *) where
41+ gBreakdownExtras :: f a -> [(Dynamic,Int)]
5142
52−class GBreakdown (f :: k -> Type) where
53− gBreakdownExtras :: f a -> [(Dynamic, Int)]
54−
55−instance (Hashable x, Typeable x, Generic x, GBreakdown (Rep x)) => Breakdown (Compound (x :: Type)) where
43+instance (Hashable x, Typeable x, Generic x, GBreakdown (Rep x)) => Breakdown (Compound (x::Type)) where
5644 breakdownExtras = gBreakdownExtras . from . unCompound
5745
58−instance GBreakdown f => GBreakdown (M1 S c f) where
46+instance GBreakdown f => GBreakdown (M1 S c f ) where
5947 gBreakdownExtras (M1 f) = gBreakdownExtras f
6048
6149instance GBreakdown b => GBreakdown (M1 D a b) where
@@ -72,10 +60,5 @@ instance (GBreakdown a, GBreakdown b) => GBreakdown (a :+: b) where
7260 L1 a -> gBreakdownExtras a
7361 R1 a -> gBreakdownExtras a
7462
75−instance (Hashable a, Typeable a, Breakdown a) => GBreakdown (K1 R a) where
63+instance (Hashable a, Typeable a, Breakdown a) => GBreakdown (K1 R a) where
7664 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+43−49
@@ -1,54 +1,55 @@
1−{-# LANGUAGE AllowAmbiguousTypes #-}
2−{-# LANGUAGE DerivingVia #-}
3−{-# LANGUAGE FlexibleContexts #-}
4−{-# LANGUAGE FlexibleInstances #-}
5−{-# LANGUAGE PolyKinds #-}
61{-# LANGUAGE ScopedTypeVariables #-}
2+{-# LANGUAGE TypeFamilies #-}
73{-# LANGUAGE StandaloneDeriving #-}
4+{-# LANGUAGE DerivingVia #-}
85{-# LANGUAGE TypeApplications #-}
9−{-# LANGUAGE TypeFamilies #-}
10−{-# LANGUAGE TypeOperators #-}
6+{-# LANGUAGE FlexibleContexts #-}
117{-# LANGUAGE UndecidableInstances #-}
12−
8+{-# LANGUAGE PolyKinds #-}
9+{-# LANGUAGE TypeOperators #-}
10+{-# LANGUAGE FlexibleInstances #-}
11+{-# LANGUAGE AllowAmbiguousTypes #-}
1312module Roboservant.Types.BuildFrom where
1413
15−import Data.List(nub)
14+import Control.Monad(filterM)
15+import Data.List.NonEmpty (NonEmpty(..))
16+import qualified Data.List.NonEmpty as NEL
17+import Data.Typeable (Typeable)
1618import qualified Data.Dependent.Map as DM
19+import qualified Type.Reflection as R
20+import Data.Kind
21+import Roboservant.Types.Internal
1722import Data.Hashable
1823import qualified Data.IntSet as IntSet
19−import Data.Kind
20−import Data.List.NonEmpty (NonEmpty (..))
21−import qualified Data.List.NonEmpty as NEL
22−import Data.Typeable (Typeable)
2324import GHC.Generics
24−import Roboservant.Types.Internal
25−import qualified Type.Reflection as R
26−import Servant(NoContent)
27−import Roboservant.Types.Orphans()
2825
29−buildFrom :: forall x. (Hashable x, BuildFrom x, Typeable x) => Stash -> Maybe (StashValue x)
26+buildFrom :: forall x . (Hashable x, BuildFrom x, Typeable x) => Stash -> Maybe (StashValue x)
3027buildFrom = buildStash . buildFrom'
3128 where
3229 buildStash :: [([Provenance], x)] -> Maybe (StashValue x)
33− buildStash = fmap (foldr1 addStash . fmap promoteToStash) . NEL.nonEmpty
30+ buildStash = fmap (foldr1 addStash . fmap promoteToStash) . NEL.nonEmpty
31+
3432 promoteToStash :: ([Provenance], x) -> StashValue x
35− promoteToStash (p, x) =
36− StashValue
37− (pure (p, x))
38− (IntSet.singleton (hash x))
33+ promoteToStash (p,x) = StashValue (pure (p,x))
34+ (IntSet.singleton (hash x))
35+
36+ -- | sorta fiddly. looks like (<>), but in general it's not safe to add arbitrary StashValues, because
37+ -- there's no guarantee that they came from the same run, which would invalidate all the provenance stuff.
38+ -- in this context it's safe because we have no access to anything else.
39+ --
40+ -- we _do_ have to check here that the new elements are not already contained, however.
3941 addStash :: StashValue x -> StashValue x -> StashValue x
40− addStash old (StashValue newVal _) =
41− let insertableVals = NEL.filter ((`IntSet.notMember` stashHash old) . hash) newVal
42− in StashValue
43− (addListToNE (getStashValue old) insertableVals)
44− (IntSet.union (IntSet.fromList . map hash . fmap snd . NEL.toList $ newVal) (stashHash old))
42+ addStash old (StashValue newVal _)
43+ = let insertableVals = NEL.filter ((`IntSet.notMember` stashHash old) . hash) newVal
44+ in StashValue ( addListToNE (getStashValue old) insertableVals )
45+ (IntSet.union (IntSet.fromList . map hash . fmap snd . NEL.toList $ newVal) (stashHash old))
46+
4547 addListToNE :: NonEmpty a -> [a] -> NonEmpty a
4648 addListToNE ne l = NEL.fromList (NEL.toList ne <> l)
4749
48−buildFrom' :: forall x. (Hashable x, BuildFrom x, Typeable x) => Stash -> [([Provenance], x)]
49−buildFrom' stash =
50− maybe [] (NEL.toList . getStashValue) (DM.lookup R.typeRep (getStash stash))
51− <> extras stash
50+buildFrom' :: forall x . (Hashable x, BuildFrom x, Typeable x) => Stash -> [([Provenance], x)]
51+buildFrom' stash = maybe [] (NEL.toList . getStashValue) (DM.lookup R.typeRep (getStash stash))
52+ <> extras stash
5253
5354class (Hashable x, Typeable x) => BuildFrom (x :: Type) where
5455 extras :: Stash -> [([Provenance], x)]
@@ -62,22 +63,18 @@ deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, BuildFrom x)
6263
6364-- this isn't wonderful, but we need a hand-rolled instance for recursive datatypes right now.
6465-- 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
66+instance (BuildFrom x) => BuildFrom [x] where
67+ extras stash = map (\xs -> (concatMap fst xs,map snd xs)) $ powerset $ extras @x stash
6868 where
69− -- powerset creates way too much stuff. something better here eventually.
70− notpowerset xs = []:xs:map pure xs
71−
69+ powerset xs = filterM (const [True, False]) xs
7270
73−instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x :: Type)) where
71+instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x::Type)) where
7472 extras stash = fmap (Compound . to) <$> gExtras stash
7573
7674deriving via (Atom Int) instance BuildFrom Int
77−
7875deriving via (Atom Char) instance BuildFrom Char
7976
80−class GBuildFrom (f :: k -> Type) where
77+class GBuildFrom (f :: k -> *) where
8178 gExtras :: Stash -> [([Provenance], f a)]
8279
8380instance GBuildFrom b => GBuildFrom (M1 D a b) where
@@ -85,15 +82,14 @@ instance GBuildFrom b => GBuildFrom (M1 D a b) where
8582
8683-- not recursion safe!
8784instance (GBuildFrom a, GBuildFrom b) => GBuildFrom (a :+: b) where
88− gExtras stash =
89− (fmap L1 <$> gExtras stash)
90− <> (fmap R1 <$> gExtras stash)
85+ gExtras stash = (fmap L1 <$> gExtras stash)
86+ <> (fmap R1 <$> gExtras stash)
9187
9288instance (GBuildFrom a, GBuildFrom b) => GBuildFrom (a :*: b) where
93− gExtras stash = [(pa <> pb, a' :*: b') | (pa, a') <- gExtras stash, (pb, b') <- gExtras stash]
89+ gExtras stash = [ (pa<>pb, a' :*: b') | (pa,a') <- gExtras stash , (pb,b') <- gExtras stash]
9490
9591instance GBuildFrom b => GBuildFrom (M1 C a b) where
96− gExtras = fmap (fmap M1) . gExtras
92+ gExtras =fmap (fmap M1) . gExtras
9793
9894instance GBuildFrom b => GBuildFrom (M1 S a b) where
9995 gExtras = fmap (fmap M1) . gExtras
@@ -102,6 +98,4 @@ instance BuildFrom a => GBuildFrom (K1 i a) where
10298 gExtras = fmap (fmap K1) . buildFrom'
10399
104100instance GBuildFrom U1 where
105− gExtras _ = [([], U1)]
106−
107−deriving via (Atom NoContent) instance BuildFrom NoContent
101+ gExtras _ = [([],U1)]
src/Roboservant/Types/Config.hsdeleted−29
@@ -1,29 +0,0 @@
1−module Roboservant.Types.Config where
2−
3−import Data.Dynamic
4−
5−data Config
6− = Config
7− { seed :: [(Dynamic, Int)],
8− maxRuntime :: Double, -- seconds to test for
9− maxReps :: Integer,
10− rngSeed :: Int,
11− coverageThreshold :: Double,
12− logInfo :: String -> IO (),
13− healthCheck :: IO ()
14− }
15−
16−defaultConfig :: Config
17−defaultConfig =
18− Config
19− { seed = [],
20− maxRuntime = 0.5,
21− maxReps = 1000,
22− rngSeed = 0,
23− coverageThreshold = 0,
24− logInfo = const (pure ()),
25− healthCheck = pure ()
26− }
27−
28−noisyConfig :: Config
29−noisyConfig = defaultConfig {logInfo = print}
src/Roboservant/Types/FlattenServer.hsnew file+44
@@ -0,0 +1,44 @@
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+module Roboservant.Types.FlattenServer where
11+
12+import Servant
13+
14+data Bundled endpoints where
15+ AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
16+ NoEndpoints :: Bundled '[]
17+
18+class FlattenServer api where
19+ flattenServer :: Server api -> Bundled (Endpoints api)
20+
21+instance
22+ ( Endpoints (endpoint :<|> api) ~ (endpoint ': Endpoints api),
23+ Server (endpoint :<|> api) ~ (Server endpoint :<|> Server api),
24+ FlattenServer api
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+ ( HasServer (Verb method statusCode contentTypes responseType) '[],
40+ Endpoints (Verb method statusCode contentTypes responseType) ~ '[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+20−24
@@ -1,53 +1,49 @@
11{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
23{-# LANGUAGE DeriveGeneric #-}
34{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55
66module Roboservant.Types.Internal where
77
8+import Data.List.NonEmpty (NonEmpty(..))
9+import qualified Data.Map.Strict as Map
810import qualified Data.Dependent.Map as DM
911import Data.Dependent.Map (DMap)
10−import Data.Dependent.Sum
11−import Data.Dynamic (Dynamic, toDyn)
12−import Data.Hashable (Hashable, hash)
13−import Data.IntSet (IntSet)
14−import Data.List.NonEmpty (NonEmpty (..))
15−import qualified Data.Map.Strict as Map
16−import Data.Typeable (Typeable)
17−import GHC.Generics (Generic)
1812import qualified Type.Reflection as R
13+import Data.Dependent.Sum
14+import Data.IntSet(IntSet)
15+import Data.Hashable(Hashable,hash)
16+import GHC.Generics(Generic)
17+import Data.Typeable(Typeable)
18+import Data.Dynamic(Dynamic,toDyn)
1919
2020data Provenance
2121 = Provenance R.SomeTypeRep Int
22− deriving (Show, Eq, Generic)
22+ deriving (Show,Eq,Generic)
2323
2424instance Hashable Provenance
2525
26−data StashValue a
27− = StashValue
28− { getStashValue :: NonEmpty ([Provenance], a),
29− stashHash :: IntSet
30− }
26+data StashValue a = StashValue { getStashValue :: NonEmpty ([Provenance], a)
27+ , stashHash :: IntSet
28+ }
3129 deriving (Functor, Show)
3230
3331-- wrap in newtype to give a custom Show instance, since the normal
3432-- instance for DMap is not happy since StashValue needs Show a to show
35−newtype Stash = Stash {getStash :: DMap R.TypeRep StashValue}
33+newtype Stash = Stash { getStash :: DMap R.TypeRep StashValue }
3634 deriving (Semigroup, Monoid)
3735
3836instance Show Stash where
39− showsPrec i (Stash x) =
40− showsPrec i
41− $ Map.fromList . map (\(tr :=> StashValue vs _) -> (R.SomeTypeRep tr, fmap fst vs))
42− $ DM.toList x
37+ showsPrec i (Stash x) = showsPrec i $
38+ Map.fromList . map (\(tr :=> StashValue vs _) -> (R.SomeTypeRep tr, fmap fst vs)) $ DM.toList x
4339
4440-- | Can't be built up from parts, can't be broken down further.
45−newtype Atom x = Atom {unAtom :: x}
46− deriving newtype (Hashable, Typeable,Eq)
41+newtype Atom x = Atom { unAtom :: x }
42+ deriving newtype (Hashable,Typeable)
4743
4844-- | can be broken down and built up from generic pieces
49−newtype Compound x = Compound {unCompound :: x}
50− deriving newtype (Hashable, Typeable, Eq)
45+newtype Compound x = Compound { unCompound :: x }
46+ deriving newtype (Hashable,Typeable)
5147
5248hashedDyn :: (Hashable a, Typeable a) => a -> (Dynamic, Int)
5349hashedDyn 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+50−58
@@ -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 #-}
@@ -20,22 +15,25 @@
2015{-# LANGUAGE CPP #-}
2116module Roboservant.Types.ReifiedApi where
2217
18+import Control.Monad.Except (runExceptT)
19+import Data.Bifunctor
2320import Data.Dynamic (Dynamic)
24−import Control.Exception(Exception)
21+import Data.Kind
2522import Data.List.NonEmpty (NonEmpty)
2623import Data.Typeable (Typeable)
2724import GHC.Generics ((:*:)(..))
25+import GHC.TypeLits (Symbol)
2826import Roboservant.Types.Internal
2927import Roboservant.Types.Breakdown
3028import Roboservant.Types.BuildFrom
31−import Data.Kind(Type)
29+import Roboservant.Types.FlattenServer
3230import Servant
3331import Servant.API.Modifiers(FoldRequired,FoldLenient)
34−import GHC.TypeLits (Symbol)
3532import qualified Data.Text as T
3633import qualified Data.Vinyl as V
3734import qualified Data.Vinyl.Curry as V
3835import qualified Type.Reflection as R
36+import Data.Hashable(Hashable)
3937
4038newtype ApiOffset = ApiOffset Int
4139 deriving (Eq, Show, Ord)
@@ -49,32 +47,60 @@ newtype Argument a = Argument
4947
5048data ReifiedEndpoint = forall as. (V.RecordToList as, V.RMap as) => ReifiedEndpoint
5149 { reArguments :: V.Rec (TypedF Argument) as
52− , reEndpointFunc :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
50+ , reEndpointFunc :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int))))
5351 }
5452
55−instance Show ReifiedEndpoint where
56− show _ = "lol"
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+ toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
5760
58−class ( V.RecordToList (EndpointArgs endpoint)
61+class ( V.Curried (EndpointArgs endpoint) (Handler (EndpointRes endpoint)) ~ Server endpoint
62+ , V.RecordToList (EndpointArgs endpoint)
5963 , V.RMap (EndpointArgs endpoint)
60− ) => ToReifiedEndpoint (endpoint :: Type) where
64+ ) => ToReifiedEndpoint (endpoint :: *) where
6165 type EndpointArgs endpoint :: [Type]
6266 type EndpointRes endpoint :: Type
6367
6468 reifiedEndpointArguments :: V.Rec (TypedF Argument) (EndpointArgs endpoint)
6569
70+instance ToReifiedApi '[] where
71+ toReifiedApi NoEndpoints _ = []
6672
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−
77−
73+instance
74+ ( Typeable (EndpointRes endpoint)
75+ , NormalizeFunction (ServerT endpoint Handler)
76+ , Normal (ServerT endpoint Handler) ~ V.Curried (EndpointArgs endpoint) (IO (Either ServerError (NonEmpty (Dynamic,Int))))
77+ , ToReifiedEndpoint endpoint
78+ , ToReifiedApi endpoints, Typeable (ServerT endpoint Handler)
79+ ) =>
80+ ToReifiedApi (endpoint : endpoints)
81+ where
82+ toReifiedApi (endpoint `AnEndpoint` endpoints) _ =
83+ (0, ReifiedEndpoint
84+ { reArguments = reifiedEndpointArguments @endpoint
85+ , reEndpointFunc = normalize endpoint
86+ }
87+ )
88+ : (map . first) (+1)
89+ (toReifiedApi endpoints (Proxy @endpoints))
90+
91+class NormalizeFunction m where
92+ type Normal m
93+ normalize :: m -> Normal m
94+
95+instance NormalizeFunction x => NormalizeFunction (r -> x) where
96+ type Normal (r -> x) = r -> Normal x
97+ normalize = fmap normalize
98+
99+instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x) where
100+ type Normal (Handler x) = IO (Either ServerError (NonEmpty (Dynamic,Int)))
101+ normalize handler = (runExceptT . runHandler') handler >>= \case
102+ Left serverError -> pure (Left serverError)
103+ Right x -> pure $ Right $ breakdown x
78104
79105instance
80106 (Typeable responseType, Breakdown responseType) =>
@@ -84,12 +110,6 @@ instance
84110 type EndpointRes (Verb method statusCode contentTypes responseType) = responseType
85111 reifiedEndpointArguments = V.RNil
86112
87−instance ToReifiedEndpoint (NoContentVerb method)
88− where
89− type EndpointArgs (NoContentVerb method) = '[]
90− type EndpointRes (NoContentVerb method) = NoContent
91− reifiedEndpointArguments = V.RNil
92−
93113instance
94114 (ToReifiedEndpoint endpoint) =>
95115 ToReifiedEndpoint ((x :: Symbol) :> endpoint)
@@ -98,16 +118,6 @@ instance
98118 type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
99119 reifiedEndpointArguments = reifiedEndpointArguments @endpoint
100120
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−
111121instance
112122 (ToReifiedEndpoint endpoint) =>
113123 ToReifiedEndpoint (Description s :> endpoint)
@@ -150,24 +160,6 @@ instance
150160 tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
151161 V.:& reifiedEndpointArguments @endpoint
152162
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−
171163instance
172164 ( BuildFrom (IfRequiredLenient T.Text mods headerType)
173165 , ToReifiedEndpoint endpoint
src/Roboservant/Types/ReifiedApi/Server.hsdeleted−114
@@ -1,114 +0,0 @@
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+11−5
@@ -1,10 +1,16 @@
1−resolver: lts-20.4
1+# nb: 16.8 doesn't work because the version of servant is too old to have FoldLenient', fixme.
2+resolver: lts-16.16
23packages:
34- .
45
56extra-deps:
6−# - dependent-map-0.4.0.0@sha256:ca2b131046f4340a1c35d138c5a003fe4a5be96b14efc26291ed35fd08c62221,1657
7−# - dependent-sum-0.7.1.0@sha256:5599aa89637db434431b1dd3fa7c34bc3d565ee44f0519bfbc877be1927c2531,2068
87- servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
9−# - constraints-extras-0.3.0.2@sha256:013b8d0392582c6ca068e226718a4fe8be8e22321cc0634f6115505bf377ad26,1853
10−- some-1.0.3
8+- vinyl-0.13.0
9+- dependent-sum-0.7.1.0
10+- constraints-extras-0.3.0.2
11+- dependent-map-0.4.0.0
12+
13+- servant-0.18.2
14+- servant-client-0.18.2
15+- servant-client-core-0.18.2
16+- 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: 532380
73+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/16.yaml
74+ sha256: d6b004b095fe2a0b8b14fbc30014ee97e58843b9c9362ddb9244273dda62649e
75+ original: lts-16.16
test/Breakdown.hs+11−11
@@ -1,39 +1,36 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
88module Breakdown where
99
1010import Data.Aeson
11−import Data.Hashable
1211import Data.Typeable (Typeable)
1312import GHC.Generics (Generic)
1413import Servant
14+import Data.Hashable
1515
1616data Foo = Foo Int String
1717 deriving (Generic, Eq, Show, Typeable)
1818
1919instance Hashable Foo
20−
2120instance ToJSON Foo
22−
2321instance FromJSON Foo
2422
23+
2524data SomeSum = A Int | B String
2625 deriving (Generic, Eq, Show, Typeable)
2726
2827instance Hashable SomeSum
29−
3028instance ToJSON SomeSum
31−
3229instance FromJSON SomeSum
3330
34−type ProductApi =
35− "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
36− :<|> "getFoo" :> Get '[JSON] Foo
31+
32+type ProductApi = "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
33+ :<|> "getFoo" :> Get '[JSON] Foo
3734
3835eliminate :: Int -> Handler ()
3936eliminate _ = throwError $ err500 {errBody = "eliminate blew up, oh no!"}
@@ -41,10 +38,13 @@ eliminate _ = throwError $ err500 {errBody = "eliminate blew up, oh no!"}
4138productServer :: Server ProductApi
4239productServer = eliminate :<|> pure (Foo 12 "abc")
4340
44−type SumApi =
45− "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
41+
42+type SumApi
43+ = "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
4644 :<|> "getFoo1" :> Get '[JSON] SomeSum
4745 :<|> "getFoo2" :> Get '[JSON] SomeSum
4846
47+
48+
4949sumServer :: Server SumApi
5050sumServer = eliminate :<|> pure (B "hi") :<|> pure (A 3)
test/Foo.hs+1−3
@@ -8,19 +8,17 @@
88module Foo where
99
1010import Data.Aeson
11−import Data.Hashable
1211import Data.Typeable (Typeable)
1312import GHC.Generics (Generic)
1413import Servant
14+import Data.Hashable
1515
1616newtype Foo = Foo Int
1717 deriving (Generic, Eq, Show, Typeable)
1818 deriving newtype (FromHttpApiData, ToHttpApiData)
1919
2020instance Hashable Foo
21−
2221instance ToJSON Foo
23−
2422instance FromJSON Foo
2523
2624type Api =
test/Headers.hs+3−4
@@ -8,19 +8,17 @@
88module Headers where
99
1010import Data.Aeson
11−import Data.Hashable
1211import Data.Typeable (Typeable)
1312import GHC.Generics (Generic)
1413import Servant
14+import Data.Hashable
1515
1616newtype Foo = Foo Int
1717 deriving (Generic, Eq, Show, Typeable)
1818 deriving newtype (FromHttpApiData, ToHttpApiData)
1919
2020instance Hashable Foo
21−
2221instance ToJSON Foo
23−
2422instance FromJSON Foo
2523
2624type Api =
@@ -35,7 +33,8 @@ combine :: Maybe Foo -> Maybe Foo -> Handler Foo
3533combine (Just (Foo a)) (Just (Foo b)) = pure (Foo (a + b))
3634combine (Just a) Nothing = pure a
3735combine Nothing (Just a) = pure a
38−combine Nothing Nothing = pure (Foo 1)
36+combine Nothing Nothing = pure (Foo 1)
37+
3938
4039eliminate :: Foo -> Handler ()
4140eliminate (Foo a)
test/Nested.hsdeleted−23
@@ -1,23 +0,0 @@
1−{-# LANGUAGE DataKinds #-}
2−
3−{-# LANGUAGE DerivingStrategies #-}
4−
5−{-# LANGUAGE OverloadedStrings #-}
6−{-# LANGUAGE TypeOperators #-}
7−
8−module Nested where
9−
10−import Servant
11−import Servant.API.Flatten
12−
13−type Api =
14− ( "one" :> Summary "foo" :> Post '[JSON] Int
15− :<|> "two" :> Post '[JSON] Int
16− )
17− :<|> ( "three" :> Post '[JSON] Int
18− )
19−
20−type FlatApi = Flat Api
21−
22−server :: Server FlatApi
23−server = pure 1 :<|> pure 2 :<|> pure 3
test/Post.hs+9−13
@@ -1,31 +1,27 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
5−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
88module Post where
99
10−import Data.Aeson
11−import Data.Hashable
1210import GHC.Generics (Generic)
1311import Servant
12+import Data.Aeson
13+import Data.Hashable
1414
15−type Api =
16− Get '[JSON] FooPost
17− :<|> ReqBody '[JSON] FooPost :> Post '[JSON] ()
15+type Api = Get '[JSON] FooPost
16+ :<|> ReqBody '[JSON] FooPost :> Post '[JSON] ()
1817
1918data FooPost = FooPost
20− deriving (Eq, Show, Generic)
19+ deriving (Eq,Show,Generic)
2120
2221instance Hashable FooPost
23−
2422instance ToJSON FooPost
25−
2623instance FromJSON FooPost
2724
2825server :: Server Api
29−server =
30− pure FooPost
31− :<|> const (pure ())
26+server = pure FooPost
27+ :<|> const (pure ())
test/Product.hs+2−4
@@ -1,25 +1,23 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
88module Product where
99
1010import Data.Aeson
11−import Data.Hashable
1211import Data.Typeable (Typeable)
1312import GHC.Generics (Generic)
1413import Servant
14+import Data.Hashable
1515
1616data Foo = Foo Int String
1717 deriving (Generic, Eq, Show, Typeable)
1818
1919instance Hashable Foo
20−
2120instance ToJSON Foo
22−
2321instance FromJSON Foo
2422
2523type Api = "item" :> ReqBody '[JSON] Foo :> Post '[JSON] ()
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/Seeded.hs+5−9
@@ -8,26 +8,22 @@
88module Seeded where
99
1010import Data.Aeson
11−import Data.Hashable
1211import Data.Typeable (Typeable)
1312import GHC.Generics (Generic)
1413import Servant
14+import Data.Hashable
1515
1616newtype Seed = Seed Int
1717 deriving (Generic, Eq, Show, Typeable)
1818 deriving newtype (FromHttpApiData, ToHttpApiData)
1919
2020instance ToJSON Seed
21−
2221instance FromJSON Seed
23−
2422instance Hashable Seed
2523
26−type Api =
27− Capture "seed" Seed :> Get '[JSON] ()
28− :<|> Get '[JSON] ()
24+type Api = Capture "seed" Seed :> Get '[JSON] ()
25+ :<|> Get '[JSON] ()
2926
3027server :: Server Api
31−server =
32− (\(Seed _) -> error "we blow up if we get here")
33− :<|> pure ()
28+server = (\(Seed _) -> error "we blow up if we get here")
29+ :<|> pure ()
test/Spec.hs+95−130
@@ -1,170 +1,135 @@
1−{-# LANGUAGE DerivingVia #-}
2−{-# LANGUAGE LambdaCase #-}
31{-# LANGUAGE OverloadedStrings #-}
4−{-# LANGUAGE RecordWildCards #-}
52{-# LANGUAGE StandaloneDeriving #-}
3+{-# LANGUAGE RecordWildCards #-}
64{-# LANGUAGE TypeApplications #-}
7−{-# LANGUAGE RankNTypes #-}
8−{-# LANGUAGE FlexibleContexts #-}
9−{-# LANGUAGE AllowAmbiguousTypes #-}
10−{-# LANGUAGE ScopedTypeVariables #-}
11−{-# LANGUAGE DataKinds #-}
12−{-# OPTIONS_GHC -fno-warn-orphans #-}
13−{-# LANGUAGE TypeFamilies #-}
5+{-# LANGUAGE LambdaCase #-}
6+{-# LANGUAGE DerivingVia #-}
147
15−import qualified Breakdown
16−import Data.Dynamic (toDyn)
17−import Data.Hashable (Hashable (hash))
18−import Data.Maybe (isNothing)
19−import Data.Void (Void)
8+{-# OPTIONS_GHC -fno-warn-orphans #-}
209import qualified Foo
10+import qualified Seeded
11+import qualified Valid
2112import qualified Headers
22−import qualified Nested
2313import qualified Post
24−import qualified Put
2514import qualified Product
26−import qualified QueryParams
27−import qualified Roboservant as R
28−import qualified Roboservant.Server as RS
29−import qualified Roboservant.Client as RC
30−import qualified Seeded
31−import Test.Hspec
32−import Test.Hspec.Core.Spec (FailureReason (Reason), ResultStatus (Failure, Success), itemExample, mapSpecItem_, resultStatus)
33−import qualified Valid
34−import Servant ( Server, Proxy(..), serve, Endpoints, HasServer )
15+import qualified Breakdown
3516
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((>=>))
17+import Test.Hspec.Core.Spec(ResultStatus(Failure,Success),resultStatus,itemExample,FailureReason(Reason),mapSpecItem_)
18+import Data.Dynamic(toDyn)
19+import qualified Roboservant as RS
20+import Test.Hspec
21+import Data.Void ( Void )
22+import Data.Maybe ( isNothing )
23+import Data.Hashable ( Hashable(hash) )
4124
4225main :: IO ()
4326main = hspec spec
4427
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 })
28+noCheck :: IO ()
29+noCheck = pure ()
30+
31+defaultConfig :: RS.Config
32+defaultConfig = RS.Config
33+ { RS.seed = []
34+ , RS.maxRuntime = 0.5
35+ , RS.maxReps = 1000
36+ , RS.rngSeed = 0
37+ , RS.coverageThreshold = 0
38+ }
6439
6540spec :: Spec
6641spec = do
6742 describe "Basic usage" $ do
6843 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−
44+ it "finds no error in a valid app" $ do
45+ RS.fuzz @Valid.Api Valid.server defaultConfig noCheck
46+ >>= (`shouldSatisfy` isNothing)
47+ it "finds no error in a valid generic app" $ do
48+ RS.fuzz @Valid.RoutedApi Valid.routedServer defaultConfig noCheck
49+ >>= (`shouldSatisfy` isNothing)
50+
51+ it "does fail coverage check" $ do
52+ r <- RS.fuzz @Valid.Api Valid.server defaultConfig { RS.coverageThreshold = 0.6 } noCheck
53+ fmap (RS.failureReason . RS.rsException) r `shouldSatisfy`
54+ (\case
55+ Just (RS.InsufficientCoverage _) -> True
56+ _ -> False)
57+
58+ describe "posted body" $ do
59+ it "passes a coverage check using a posted body" $ do
60+ RS.fuzz @Post.Api Post.server defaultConfig { RS.coverageThreshold = 0.99 } noCheck
61+ >>= (`shouldSatisfy` isNothing)
8762
8863 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)
64+ shouldFail $
65+ it "finds an error using information passed in" $
66+ let res = Seeded.Seed 1 in
67+ RS.fuzz @Seeded.Api Seeded.server (defaultConfig{ RS.seed = [(toDyn res,hash res) ] }) noCheck
68+ >>= (`shouldSatisfy` isNothing)
69+ describe "Foo" $ do
70+ it "finds an error in a basic app" $
71+ RS.fuzz @Foo.Api Foo.server defaultConfig noCheck
72+ >>= (`shouldSatisfy` serverFailure)
10073
10174 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)
109− 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
119−serverFailure = \case
120− Just R.Report {..} ->
121− let R.RoboservantException {..} = rsException
122− in failureReason /= R.NoPossibleMoves
123− _ -> False
75+ describe "headers (and sum types)" $ do
76+ it "should find a failure that's dependent on using header info" $ do
77+ RS.fuzz @Headers.Api Headers.server defaultConfig noCheck
78+ >>= (`shouldSatisfy` serverFailure)
12479
125−deriving via (R.Atom Foo.Foo) instance R.Breakdown Foo.Foo
80+ describe "product types" $ do
81+ it "should find a failure that's dependent on creating a product" $ do
82+ RS.fuzz @Product.Api Product.server defaultConfig { RS.seed = [RS.hashedDyn 'a', RS.hashedDyn (1::Int)]} noCheck
83+ >>= (`shouldSatisfy` serverFailure)
12684
127−deriving via (R.Atom Foo.Foo) instance R.BuildFrom Foo.Foo
85+ describe "Breakdown" $ do
86+ it "handles products" $ do
87+ RS.fuzz @Breakdown.ProductApi Breakdown.productServer defaultConfig noCheck
88+ >>= (`shouldSatisfy` serverFailure)
12889
129−deriving via (R.Atom Headers.Foo) instance R.Breakdown Headers.Foo
90+ it "handles sums" $ do
91+ RS.fuzz @Breakdown.SumApi Breakdown.sumServer defaultConfig noCheck
92+ >>= (`shouldSatisfy` serverFailure)
13093
131−deriving via (R.Atom Headers.Foo) instance R.BuildFrom Headers.Foo
13294
133−deriving via (R.Atom Seeded.Seed) instance R.Breakdown Seeded.Seed
95+serverFailure :: Maybe RS.Report -> Bool
96+serverFailure = \case
97+ Just RS.Report{..} ->
98+ let RS.RoboservantException{..} = rsException
99+ in failureReason /= RS.NoPossibleMoves
100+ _ -> False
134101
135−deriving via (R.Atom Seeded.Seed) instance R.BuildFrom Seeded.Seed
136102
137−deriving via (R.Atom Void) instance R.BuildFrom Void
103+deriving via (RS.Atom Foo.Foo) instance RS.Breakdown Foo.Foo
104+deriving via (RS.Atom Foo.Foo) instance RS.BuildFrom Foo.Foo
138105
139−deriving via (R.Atom Post.FooPost) instance R.Breakdown Post.FooPost
140−deriving via (R.Atom Post.FooPost) instance R.BuildFrom Post.FooPost
106+deriving via (RS.Atom Headers.Foo) instance RS.Breakdown Headers.Foo
107+deriving via (RS.Atom Headers.Foo) instance RS.BuildFrom Headers.Foo
141108
142−deriving via (R.Atom Put.Foo) instance R.Breakdown Put.Foo
143−deriving via (R.Atom Put.Foo) instance R.BuildFrom Put.Foo
109+deriving via (RS.Atom Seeded.Seed) instance RS.Breakdown Seeded.Seed
110+deriving via (RS.Atom Seeded.Seed) instance RS.BuildFrom Seeded.Seed
144111
145112
113+-- instance RS.BuildFrom Seeded.Seed
146114
147−deriving via (R.Compound Breakdown.Foo) instance R.Breakdown Breakdown.Foo
115+deriving via (RS.Atom Void) instance RS.BuildFrom Void
148116
149−deriving via (R.Compound Product.Foo) instance R.BuildFrom Product.Foo
117+deriving via (RS.Atom Post.FooPost) instance RS.BuildFrom Post.FooPost
118+deriving via (RS.Atom Post.FooPost) instance RS.Breakdown Post.FooPost
150119
151−deriving via (R.Compound Breakdown.SomeSum) instance R.Breakdown Breakdown.SomeSum
120+deriving via (RS.Compound Product.Foo) instance RS.BuildFrom Product.Foo
121+deriving via (RS.Compound Breakdown.Foo) instance RS.Breakdown Breakdown.Foo
122+deriving via (RS.Compound Breakdown.SomeSum) instance RS.Breakdown Breakdown.SomeSum
152123
153124-- | `shouldFail` allows you to assert that a given `Spec` should contain at least one failing test.
154125-- this is often useful when testing tests.
155126shouldFail :: SpecWith a -> SpecWith a
156−shouldFail =
157− mapSpecItem_
158− ( \i ->
159− i
160− { itemExample = \p a cb -> do
161− r <- itemExample i p a cb
162− pure
163− r
164− { resultStatus = case resultStatus r of
165− Success -> Failure Nothing (Reason "Unexpected success")
166− Failure _ _ -> Success
167− x -> x
168− }
169− }
170− )
127+shouldFail = mapSpecItem_ (\i -> i {
128+ itemExample = \p a cb -> do
129+ r <- (itemExample i) p a cb
130+ pure r {resultStatus = case resultStatus r of
131+ Success -> Failure Nothing (Reason "Unexpected success")
132+ Failure _ _ -> Success
133+ x -> x
134+ }
135+ })
test/UnsafeIO.hs+15−13
@@ -1,39 +1,41 @@
11{-# LANGUAGE DataKinds #-}
2−
2+{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
88module UnsafeIO where
99
10−import Control.Monad.Trans (MonadIO (liftIO))
11−import Data.Aeson ()
12−import qualified Data.ByteString.Lazy.Char8 as BL8
13−import Data.IORef (IORef, newIORef, readIORef, writeIORef)
10+import Data.Aeson()
1411import Servant
12+import Data.IORef (writeIORef, IORef, readIORef, newIORef)
13+import Control.Monad.Trans (MonadIO(liftIO))
14+import qualified Data.ByteString.Lazy.Char8 as BL8
15+
1516
1617type UnsafeApi =
1718 "add" :> Get '[JSON] ()
18− :<|> "healthcheck" :> Get '[JSON] ()
19+ :<|> "healthcheck" :> Get '[JSON] ()
1920
2021healthcheck :: IORef Int -> Handler ()
2122healthcheck ref = do
2223 t <- liftIO $ readIORef ref
2324 case t of
2425 0 -> pure ()
25− n -> throwError $ err500 {errBody = "observed inconsistency: " <> BL8.pack (show n)}
26+ n -> throwError $ err500 {errBody = "observed inconsistency: " <> (BL8.pack $ show n)}
27+
28+
2629
2730makeServer :: IO (Server UnsafeApi)
2831makeServer = do
2932 ref <- newIORef 0
30− pure $
31− unsafeMunge ref
32− :<|> healthcheck ref
33+ pure $ unsafeMunge ref
34+ :<|> healthcheck ref
3335
3436unsafeMunge :: IORef Int -> Handler ()
3537unsafeMunge ref = liftIO $ do
3638 t <- readIORef ref
37− writeIORef ref (t + 1)
39+ writeIORef ref (t+1)
3840 t2 <- readIORef ref
39− writeIORef ref (t2 -1)
41+ writeIORef ref (t2-1)
test/Valid.hs+14−18
@@ -1,31 +1,28 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
88module Valid where
99
10+import Servant
1011import Data.Void
1112import GHC.Generics
12−import Servant
13+import Servant.API.Generic
1314import Servant.Server.Generic
1415
15−type Api =
16− Get '[JSON] Int
17− :<|> Capture "void" Void :> Get '[JSON] ()
16+type Api = Get '[JSON] Int
17+ :<|> Capture "void" Void :> Get '[JSON] ()
1818
1919data Routes route
2020 = Routes
21− { getInt ::
22− route
23− :- Summary "i'm a summary!" :> Get '[JSON] Int,
24− captureIt ::
25− route
26− :- Capture "void" Void :> Get '[JSON] ()
27− }
28− deriving (Generic)
21+ { getInt :: route :-
22+ Get '[JSON] Int
23+ , captureIt :: route :-
24+ Capture "void" Void :> Get '[JSON] ()
25+ } deriving Generic
2926
3027type RoutedApi = ToServantApi Routes
3128
@@ -34,11 +31,10 @@ routedServer :: Server RoutedApi
3431routedServer = genericServer routes
3532
3633routes :: Routes AsServer
37−routes =
38− Routes
39− { getInt = pure 7,
40− captureIt = const (pure ())
41− }
34+routes = Routes
35+ { getInt = pure 7
36+ , captureIt = const (pure ())
37+ }
4238
4339server :: Server Api
4440server = pure 7 :<|> const (pure ())