on master help sign in
39 files changed+1244−520
since splitsnapshots
unifiedsplit
.github/workflows/cabal.yml+64 .github/workflows/ci.yml+2−2 EXAMPLE.md+71−42 Makefile+1−1 README.md+90−1 benchmarks/README.md+30 benchmarks/genFile.sh+26 benchmarks/prelude+61 benchmarks/runTiming.sh+10 benchmarks/runTimingNoisy.sh+10 package.yaml+13−3 roboservant.cabal+26−6 src/Roboservant.hs+5−2 src/Roboservant/Client.hs+127 src/Roboservant/Direct.hs+60−60 src/Roboservant/Server.hs+36 src/Roboservant/Types.hs+10−8 src/Roboservant/Types/Breakdown.hs+31−14 src/Roboservant/Types/BuildFrom.hs+49−43 src/Roboservant/Types/Config.hs+29 src/Roboservant/Types/FlattenServer.hs−44 src/Roboservant/Types/Internal.hs+24−20 src/Roboservant/Types/Orphans.hs+7 src/Roboservant/Types/ReifiedApi.hs+58−50 src/Roboservant/Types/ReifiedApi/Server.hs+114 stack.yaml+5−11 stack.yaml.lock+9−58 test/Breakdown.hs+11−11 test/Foo.hs+3−1 test/Headers.hs+4−3 test/Nested.hs+23 test/Post.hs+13−9 test/Product.hs+4−2 test/Put.hs+33 test/QueryParams.hs+15 test/Seeded.hs+9−5 test/Spec.hs+130−95 test/UnsafeIO.hs+13−15 test/Valid.hs+18−14
.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+71−42
@@ -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,30 +13,43 @@ 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
29+import Data.Maybe(isNothing, isJust)
30+import qualified Network.Wai.Handler.Warp as Warp
31+import Data.Aeson(FromJSON,ToJSON)
2332
2433newtype A = A Int
2534 deriving (Generic, Eq, Show, Typeable)
2635 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
2736
37+instance FromJSON A
38+instance ToJSON A
39+
2840newtype B = B Int
2941 deriving (Generic, Eq, Show, Typeable)
3042 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
3143
44+instance FromJSON B
45+instance ToJSON B
46+
3247type Api =
3348 "item" :> Get '[JSON] A
3449 :<|> "itemAdd" :> Capture "one" B :> Capture "two" B :> Get '[JSON] B
3550 :<|> "item" :> Capture "itemId" B :> Get '[JSON] ()
3651
37−goodServer = server (pure $ A 0)
38−badServer = server (pure $ A 1)
39−
52+server :: Handler A -> Server Api
4053server introduce = introduce :<|> combine :<|> eliminate
4154 where
4255 combine (B i) (B j) = pure $ B (i + j)
@@ -45,45 +58,66 @@ server introduce = introduce :<|> combine :<|> eliminate
4558 | otherwise = pure ()
4659```
4760
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− }
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.
6563
64+``` haskell
65+goodServer, badServer :: Server Api
66+goodServer = server (pure $ A 0)
67+badServer = server (pure $ A 1)
6668```
6769
68−and the tests: the faulty server should fail and the good server should pass.
70+In the test file, we first define the tests: the faulty server should fail and the good server should pass.
6971
7072```haskell
73+main :: IO ()
74+main = hspec spec
75+
76+spec :: Spec
7177spec = describe "example" $ do
7278 it "good server should not fail" $ do
73− fuzz @Api goodServer defaultConfig { coverageThreshold = 0.99 } (pure ())
79+ RS.fuzz @Api goodServer config
7480 >>= (`shouldSatisfy` isNothing)
7581 it "bad server should fail" $ do
76− fuzz @Api badServer defaultConfig { coverageThreshold = 0.99 } (pure ())
77− >>= (`shouldSatisfy` serverFailure)
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+ }
78108```
79109
80−And unless we want to ship roboservant and all its dependencies to production, we also need
110+Unless we want to ship roboservant and all its dependencies to production, we also need
81111some orphan instances: because As are the only value we can get without
82112an input, we need to be able to break them down.
83113
84−```haskell
114+``` haskell
85115deriving via (Compound A) instance Breakdown A
86−-- if we wanted to assemble As from parts as well, we'd derive using Compound
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
87121deriving via (Atom A) instance BuildFrom A
88122
89123```
@@ -96,19 +130,14 @@ deriving via (Compound B) instance BuildFrom B
96130deriving via (Atom B) instance Breakdown B
97131```
98132
99−finally some uninteresting utilities and the entrypoint
100−
101−```haskell
102−main = hspec spec
103133
104−serverFailure :: Maybe Report -> Bool
105−serverFailure c = case c of
106− Just Report{..} ->
107− let RoboservantException{..} = rsException
108− in failureReason /= NoPossibleMoves
109− _ -> False
134+test utilities:
110135
111−isNothing x = case x of
112− Nothing -> True
113− _ -> False
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 })
114143```
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+90−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
@@ -14,6 +15,71 @@ Servant gives us a lot of information about what a server can do. We
1415use this information to generate arbitrarily long request/response
1516sessions and verify properties that should hold over them.
1617
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+```
1783
1884## why not servant-quickcheck?
1985
@@ -23,3 +89,26 @@ there's a lot of the state space you just can't explore without context: modern
2389full of pointer-like structures, whether they're URLs or database
2490keys/uuids, and servant-quickcheck requires that you be able to generate
2591these 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.mdnew file+30
@@ -0,0 +1,30 @@
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.shnew file+26
@@ -0,0 +1,26 @@
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/preludenew file+61
@@ -0,0 +1,61 @@
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.shnew file+10
@@ -0,0 +1,10 @@
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.shnew file+10
@@ -0,0 +1,10 @@
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+13−3
@@ -1,5 +1,6 @@
1+
12name: roboservant
2−version: 0.1.0.2
3+version: 0.1.0.3
34github: "mwotton/roboservant"
45license: BSD3
56copyright: "2020 Mark Wotton, Samuel Schlesinger"
@@ -18,6 +19,7 @@ dependencies:
1819- containers
1920- random
2021- hashable
22+- http-types
2123- lifted-base
2224- monad-control
2325- mtl
@@ -54,7 +56,10 @@ tests:
5456 - hspec
5557 - hspec-core
5658 - http-api-data
57−
59+ - http-client
60+ - hspec-wai
61+ - wai
62+ - warp
5863 example:
5964 main: Example.lhs
6065 source-dirs: .
@@ -62,5 +67,10 @@ tests:
6267 dependencies:
6368 - roboservant
6469 - hspec
65− build-depends: base, markdown-unlit
70+ - warp
71+ - http-client
72+ - aeson
73+ - hspec-core
6674 ghc-options: -pgmL markdown-unlit
75+
76+ build-tools: markdown-unlit
roboservant.cabal+26−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: 8de468df6e784f3723af9155bba9da9f43659b7d4e8e465fdac26eafcb0d4306
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,13 +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
35− Roboservant.Types.FlattenServer
37+ Roboservant.Types.Config
3638 Roboservant.Types.Internal
39+ Roboservant.Types.Orphans
3740 Roboservant.Types.ReifiedApi
41+ Roboservant.Types.ReifiedApi.Server
3842 other-modules:
3943 Paths_roboservant
4044 hs-source-dirs:
@@ -47,6 +51,7 @@ library
4751 , dependent-map
4852 , dependent-sum
4953 , hashable
54+ , http-types
5055 , lifted-base
5156 , monad-control
5257 , mtl
@@ -68,16 +73,22 @@ test-suite example
6873 other-modules:
6974 Paths_roboservant
7075 hs-source-dirs:
71− ./.
76+ ./
7277 ghc-options: -Wall -fwrite-ide-info -hiedir=.hie -pgmL markdown-unlit
78+ build-tool-depends:
79+ markdown-unlit:markdown-unlit
7380 build-depends:
74− base >=4.7 && <5
81+ aeson
82+ , base >=4.7 && <5
7583 , bytestring
7684 , containers
7785 , dependent-map
7886 , dependent-sum
7987 , hashable
8088 , hspec
89+ , hspec-core
90+ , http-client
91+ , http-types
8192 , lifted-base
8293 , monad-control
8394 , mtl
@@ -92,6 +103,7 @@ test-suite example
92103 , time
93104 , unordered-containers
94105 , vinyl
106+ , warp
95107 default-language: Haskell2010
96108
97109test-suite roboservant-test
@@ -101,8 +113,11 @@ test-suite roboservant-test
101113 Breakdown
102114 Foo
103115 Headers
116+ Nested
104117 Post
105118 Product
119+ Put
120+ QueryParams
106121 Seeded
107122 UnsafeIO
108123 Valid
@@ -120,7 +135,10 @@ test-suite roboservant-test
120135 , hashable
121136 , hspec
122137 , hspec-core
138+ , hspec-wai
123139 , http-api-data
140+ , http-client
141+ , http-types
124142 , lifted-base
125143 , monad-control
126144 , mtl
@@ -135,4 +153,6 @@ test-suite roboservant-test
135153 , time
136154 , unordered-containers
137155 , vinyl
156+ , wai
157+ , warp
138158 default-language: Haskell2010
src/Roboservant.hs+5−2
@@ -1,5 +1,8 @@
1−module Roboservant (module Roboservant.Direct
2− ,module Roboservant.Types) where
1+module Roboservant
2+ ( module Roboservant.Direct,
3+ module Roboservant.Types,
4+ )
5+where
36
47import Roboservant.Direct
58import Roboservant.Types
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+60−60
@@ -2,16 +2,17 @@
22{-# LANGUAGE FlexibleContexts #-}
33{-# LANGUAGE GADTs #-}
44{-# LANGUAGE LambdaCase #-}
5+{-# LANGUAGE PolyKinds #-}
56{-# LANGUAGE RankNTypes #-}
67{-# LANGUAGE RecordWildCards #-}
78{-# LANGUAGE ScopedTypeVariables #-}
89{-# LANGUAGE TupleSections #-}
9−{-# LANGUAGE TypeApplications #-}
10+
1011{-# LANGUAGE TypeFamilies #-}
1112{-# LANGUAGE UndecidableInstances #-}
1213
1314module Roboservant.Direct
14− ( fuzz,
15+ ( fuzz',
1516 Config (..),
1617 -- TODO come up with something smarter than exporting all this, we should
1718 -- have some nice error-display functions
@@ -24,25 +25,30 @@ module Roboservant.Direct
2425where
2526
2627import Control.Exception.Lifted
27− ( SomeException,
28− throw,
29− Exception,
30− SomeAsyncException,
31− catch,
32− catches,
33− handle,
34− Handler(Handler) )
28+ ( Exception,
29+ Handler (Handler),
30+ SomeAsyncException,
31+ SomeException,
32+ catch,
33+ catches,
34+ handle,
35+ throw,
36+ )
3537import Control.Monad.State.Strict
36− ( MonadIO(..), StateT(runStateT), modify', MonadState(get) )
38+ ( MonadIO (..),
39+ MonadState (get),
40+ StateT (runStateT),
41+ modify',
42+ )
3743import Control.Monad.Trans.Control (MonadBaseControl)
3844import qualified Data.Dependent.Map as DM
39−import Data.Dynamic ( Dynamic(..) )
45+import Data.Dynamic (Dynamic (..))
4046import qualified Data.IntSet as IntSet
4147import Data.List.NonEmpty (NonEmpty (..))
4248import qualified Data.List.NonEmpty as NEL
4349import Data.Maybe (mapMaybe)
4450import qualified Data.Set as Set
45−import Data.Time.Clock ( addUTCTime, getCurrentTime, UTCTime )
51+import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
4652import qualified Data.Vinyl as V
4753import qualified Data.Vinyl.Curry as V
4854import qualified Data.Vinyl.Functor as V
@@ -50,16 +56,17 @@ import GHC.Generics ((:*:) (..))
5056import Roboservant.Types
5157 ( ApiOffset (..),
5258 Argument (..),
53− FlattenServer (..),
59+ InteractionError(..),
5460 Provenance (..),
61+ ReifiedApi,
5562 ReifiedEndpoint (..),
5663 Stash (..),
5764 StashValue (..),
58− ToReifiedApi (..),
5965 TypedF,
6066 )
61−import Servant (Endpoints, Proxy (Proxy), Server, ServerError (..))
62−import System.Random ( mkStdGen, Random(randomR), StdGen )
67+import Roboservant.Types.Config
68+
69+import System.Random (Random (randomR), StdGen, mkStdGen)
6370import qualified Type.Reflection as R
6471
6572data RoboservantException
@@ -86,15 +93,6 @@ data FuzzOp
8693 }
8794 deriving (Show, Eq)
8895
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−
9896data FuzzState
9997 = FuzzState
10098 { path :: [FuzzOp],
@@ -107,7 +105,7 @@ data EndpointOption
107105 = forall as.
108106 (V.RecordToList as, V.RMap as) =>
109107 EndpointOption
110− { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))),
108+ { eoCall :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))),
111109 eoArgs :: V.Rec (TypedF StashValue) as
112110 }
113111
@@ -123,14 +121,18 @@ data Report
123121 }
124122 deriving (Show)
125123
126−fuzz ::
127− forall api.
128− (FlattenServer api, ToReifiedApi (Endpoints api)) =>
129− Server api ->
124+
125+
126+-- fuzzClient :: Client api -> Config -> IO (Maybe Report)
127+-- fuzzClient = undefined
128+
129+
130+
131+fuzz' ::
132+ ReifiedApi ->
130133 Config ->
131− IO () ->
132134 IO (Maybe Report)
133−fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
135+fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
134136 let path = []
135137 stash = addToStash seed mempty
136138 currentRng = mkStdGen rngSeed
@@ -139,7 +141,7 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
139141 runStateT
140142 (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get))
141143 FuzzState {..}
142− print stopreason
144+ logInfo $ show stopreason
143145 pure Nothing
144146 where
145147 -- something less terrible later
@@ -148,13 +150,15 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
148150 Report
149151 (unlines [show failureType, show exception])
150152 r
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)
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))
158162
159163 evaluateCoverage f@FuzzState {..}
160164 | coverage > coverageThreshold = pure ()
@@ -174,7 +178,7 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
174178 else do
175179 _ <- action
176180 untilDone (n -1, deadline) action
177− reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
181+
178182 routeCount = length reifiedApi
179183 elementOrFail ::
180184 (MonadState FuzzState m, MonadIO m) =>
@@ -191,7 +195,7 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
191195 ( forall as.
192196 (V.RecordToList as, V.RMap as) =>
193197 FuzzOp ->
194− V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))) ->
198+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
195199 V.Rec (TypedF V.Identity) as ->
196200 m r
197201 ) ->
@@ -232,27 +236,24 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
232236 execute ::
233237 (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
234238 FuzzOp ->
235− V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))) ->
239+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
236240 V.Rec (TypedF V.Identity) as ->
237241 m ()
238242 execute fuzzop func args = do
239− (liftIO . print . (fuzzop,) . stash) =<< get
243+ (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
240244 liftIO (V.runcurry' func argVals) >>= \case
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)
245+ Left (e::InteractionError) ->
246+ if fatalError e
247+ then throw e
248+ else pure ()
249+ Right (dyn :: NEL.NonEmpty (Dynamic, Int)) ->
249250 modify'
250− ( \fs@FuzzState {..} ->
251− fs {stash = addToStash (NEL.toList dyn) stash}
252− )
251+ ( \fs@FuzzState {..} ->
252+ fs {stash = addToStash (NEL.toList dyn) stash}
253+ )
253254 where
254255 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
255− -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
256+ -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
256257 go ::
257258 (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m) =>
258259 m ()
@@ -261,13 +262,12 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
261262 (execute op func args)
262263 [ Handler (\(e :: SomeAsyncException) -> throw e),
263264 Handler
264− ( \(e :: SomeException) -> do
265− displayDiagnostics =<< get
265+ ( \(e :: SomeException) ->
266266 throw . RoboservantException ServerCrashed (Just e) =<< get
267267 )
268268 ]
269269 catch
270− (liftIO checker)
270+ (liftIO healthCheck)
271271 (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
272272
273273addToStash ::
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+10−8
@@ -8,17 +8,19 @@
88{-# LANGUAGE UndecidableInstances #-}
99
1010module Roboservant.Types
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−
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,
1717 )
1818where
1919
2020import Roboservant.Types.Breakdown
2121import Roboservant.Types.BuildFrom
22−import Roboservant.Types.FlattenServer
23−import Roboservant.Types.ReifiedApi
22+import Roboservant.Types.Config
2423import Roboservant.Types.Internal
24+import Roboservant.Types.ReifiedApi
25+import Roboservant.Types.ReifiedApi.Server
26+import Roboservant.Types.Orphans()
src/Roboservant/Types/Breakdown.hs+31−14
@@ -1,5 +1,6 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
22{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE FlexibleContexts #-}
34{-# LANGUAGE DerivingVia #-}
45{-# LANGUAGE FlexibleInstances #-}
56{-# LANGUAGE GADTs #-}
@@ -14,36 +15,47 @@
1415
1516module Roboservant.Types.Breakdown where
1617
17−import Data.Dynamic (Dynamic, toDyn)
18−import Data.List.NonEmpty (NonEmpty (..))
19−import GHC.Generics
20−import Data.Typeable (Typeable)
21−import Roboservant.Types.Internal
18+import Data.Dynamic (Dynamic)
2219import Data.Hashable
2320import Data.Kind
21+import Data.List.NonEmpty (NonEmpty (..))
2422import qualified Data.List.NonEmpty as NEL
23+import Data.Typeable (Typeable)
24+import GHC.Generics
25+import Roboservant.Types.Internal
26+import Servant
27+import Roboservant.Types.Orphans()
2528
26−breakdown :: (Hashable x, Typeable x, Breakdown x)
27− => x -> NonEmpty (Dynamic, Int)
29+breakdown ::
30+ (Hashable x, Typeable x, Breakdown x) =>
31+ x ->
32+ NonEmpty (Dynamic, Int)
2833breakdown x = hashedDyn x :| breakdownExtras x
2934
3035class Breakdown x where
31− breakdownExtras :: x -> [(Dynamic,Int)]
36+ breakdownExtras :: x -> [(Dynamic, Int)]
3237
3338instance (Hashable x, Typeable x) => Breakdown (Atom x) where
3439 breakdownExtras _ = []
3540
3641deriving via (Atom ()) instance Breakdown ()
42+
3743deriving via (Atom Int) instance Breakdown Int
38−deriving via (Atom [Char]) instance Breakdown [Char]
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
3950
40−class GBreakdown (f :: k -> *) where
41− gBreakdownExtras :: f a -> [(Dynamic,Int)]
4251
43−instance (Hashable x, Typeable x, Generic x, GBreakdown (Rep x)) => Breakdown (Compound (x::Type)) where
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
4456 breakdownExtras = gBreakdownExtras . from . unCompound
4557
46−instance GBreakdown f => GBreakdown (M1 S c f ) where
58+instance GBreakdown f => GBreakdown (M1 S c f) where
4759 gBreakdownExtras (M1 f) = gBreakdownExtras f
4860
4961instance GBreakdown b => GBreakdown (M1 D a b) where
@@ -60,5 +72,10 @@ instance (GBreakdown a, GBreakdown b) => GBreakdown (a :+: b) where
6072 L1 a -> gBreakdownExtras a
6173 R1 a -> gBreakdownExtras a
6274
63−instance (Hashable a, Typeable a, Breakdown a) => GBreakdown (K1 R a) where
75+instance (Hashable a, Typeable a, Breakdown a) => GBreakdown (K1 R a) where
6476 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+49−43
@@ -1,55 +1,54 @@
1−{-# LANGUAGE ScopedTypeVariables #-}
2−{-# LANGUAGE TypeFamilies #-}
3−{-# LANGUAGE StandaloneDeriving #-}
1+{-# LANGUAGE AllowAmbiguousTypes #-}
42{-# LANGUAGE DerivingVia #-}
5−{-# LANGUAGE TypeApplications #-}
63{-# LANGUAGE FlexibleContexts #-}
7−{-# LANGUAGE UndecidableInstances #-}
4+{-# LANGUAGE FlexibleInstances #-}
85{-# LANGUAGE PolyKinds #-}
6+{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE StandaloneDeriving #-}
8+{-# LANGUAGE TypeApplications #-}
9+{-# LANGUAGE TypeFamilies #-}
910{-# LANGUAGE TypeOperators #-}
10−{-# LANGUAGE FlexibleInstances #-}
11−{-# LANGUAGE AllowAmbiguousTypes #-}
11+{-# LANGUAGE UndecidableInstances #-}
12+
1213module Roboservant.Types.BuildFrom where
1314
14−import Control.Monad(filterM)
15−import Data.List.NonEmpty (NonEmpty(..))
16−import qualified Data.List.NonEmpty as NEL
17−import Data.Typeable (Typeable)
15+import Data.List(nub)
1816import qualified Data.Dependent.Map as DM
19−import qualified Type.Reflection as R
20−import Data.Kind
21−import Roboservant.Types.Internal
2217import Data.Hashable
2318import 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)
2423import GHC.Generics
24+import Roboservant.Types.Internal
25+import qualified Type.Reflection as R
26+import Servant(NoContent)
27+import Roboservant.Types.Orphans()
2528
26−buildFrom :: forall x . (Hashable x, BuildFrom x, Typeable x) => Stash -> Maybe (StashValue x)
29+buildFrom :: forall x. (Hashable x, BuildFrom x, Typeable x) => Stash -> Maybe (StashValue x)
2730buildFrom = buildStash . buildFrom'
2831 where
2932 buildStash :: [([Provenance], x)] -> Maybe (StashValue x)
30− buildStash = fmap (foldr1 addStash . fmap promoteToStash) . NEL.nonEmpty
31−
33+ buildStash = fmap (foldr1 addStash . fmap promoteToStash) . NEL.nonEmpty
3234 promoteToStash :: ([Provenance], x) -> StashValue 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.
35+ promoteToStash (p, x) =
36+ StashValue
37+ (pure (p, x))
38+ (IntSet.singleton (hash x))
4139 addStash :: StashValue x -> StashValue x -> StashValue x
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−
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))
4745 addListToNE :: NonEmpty a -> [a] -> NonEmpty a
4846 addListToNE ne l = NEL.fromList (NEL.toList ne <> l)
4947
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
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
5352
5453class (Hashable x, Typeable x) => BuildFrom (x :: Type) where
5554 extras :: Stash -> [([Provenance], x)]
@@ -63,18 +62,22 @@ deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, BuildFrom x)
6362
6463-- this isn't wonderful, but we need a hand-rolled instance for recursive datatypes right now.
6564-- with an arbitrary-ish interface, we could use a size parameter, rng access etc.
66−instance (BuildFrom x) => BuildFrom [x] where
67− 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
6868 where
69− 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+
7072
71−instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x::Type)) where
73+instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x :: Type)) where
7274 extras stash = fmap (Compound . to) <$> gExtras stash
7375
7476deriving via (Atom Int) instance BuildFrom Int
77+
7578deriving via (Atom Char) instance BuildFrom Char
7679
77−class GBuildFrom (f :: k -> *) where
80+class GBuildFrom (f :: k -> Type) where
7881 gExtras :: Stash -> [([Provenance], f a)]
7982
8083instance GBuildFrom b => GBuildFrom (M1 D a b) where
@@ -82,14 +85,15 @@ instance GBuildFrom b => GBuildFrom (M1 D a b) where
8285
8386-- not recursion safe!
8487instance (GBuildFrom a, GBuildFrom b) => GBuildFrom (a :+: b) where
85− gExtras stash = (fmap L1 <$> gExtras stash)
86− <> (fmap R1 <$> gExtras stash)
88+ gExtras stash =
89+ (fmap L1 <$> gExtras stash)
90+ <> (fmap R1 <$> gExtras stash)
8791
8892instance (GBuildFrom a, GBuildFrom b) => GBuildFrom (a :*: b) where
89− gExtras stash = [ (pa<>pb, a' :*: b') | (pa,a') <- gExtras stash , (pb,b') <- gExtras stash]
93+ gExtras stash = [(pa <> pb, a' :*: b') | (pa, a') <- gExtras stash, (pb, b') <- gExtras stash]
9094
9195instance GBuildFrom b => GBuildFrom (M1 C a b) where
92− gExtras =fmap (fmap M1) . gExtras
96+ gExtras = fmap (fmap M1) . gExtras
9397
9498instance GBuildFrom b => GBuildFrom (M1 S a b) where
9599 gExtras = fmap (fmap M1) . gExtras
@@ -98,4 +102,6 @@ instance BuildFrom a => GBuildFrom (K1 i a) where
98102 gExtras = fmap (fmap K1) . buildFrom'
99103
100104instance GBuildFrom U1 where
101− gExtras _ = [([],U1)]
105+ gExtras _ = [([], U1)]
106+
107+deriving via (Atom NoContent) instance BuildFrom NoContent
src/Roboservant/Types/Config.hsnew file+29
@@ -0,0 +1,29 @@
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.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−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+24−20
@@ -1,49 +1,53 @@
11{-# LANGUAGE DeriveFunctor #-}
2−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
32{-# LANGUAGE DeriveGeneric #-}
43{-# 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
108import qualified Data.Dependent.Map as DM
119import Data.Dependent.Map (DMap)
12−import qualified Type.Reflection as R
1310import 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)
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)
18+import qualified Type.Reflection as R
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 = StashValue { getStashValue :: NonEmpty ([Provenance], a)
27− , stashHash :: IntSet
28− }
26+data StashValue a
27+ = StashValue
28+ { getStashValue :: NonEmpty ([Provenance], a),
29+ stashHash :: IntSet
30+ }
2931 deriving (Functor, Show)
3032
3133-- wrap in newtype to give a custom Show instance, since the normal
3234-- instance for DMap is not happy since StashValue needs Show a to show
33−newtype Stash = Stash { getStash :: DMap R.TypeRep StashValue }
35+newtype Stash = Stash {getStash :: DMap R.TypeRep StashValue}
3436 deriving (Semigroup, Monoid)
3537
3638instance Show Stash where
37− showsPrec i (Stash x) = showsPrec i $
38− Map.fromList . map (\(tr :=> StashValue vs _) -> (R.SomeTypeRep tr, fmap fst vs)) $ DM.toList x
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
3943
4044-- | Can't be built up from parts, can't be broken down further.
41−newtype Atom x = Atom { unAtom :: x }
42− deriving newtype (Hashable,Typeable)
45+newtype Atom x = Atom {unAtom :: x}
46+ deriving newtype (Hashable, Typeable,Eq)
4347
4448-- | can be broken down and built up from generic pieces
45−newtype Compound x = Compound { unCompound :: x }
46− deriving newtype (Hashable,Typeable)
49+newtype Compound x = Compound {unCompound :: x}
50+ deriving newtype (Hashable, Typeable, Eq)
4751
4852hashedDyn :: (Hashable a, Typeable a) => a -> (Dynamic, Int)
4953hashedDyn 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+58−50
@@ -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,60 +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− toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
55+instance Show ReifiedEndpoint where
56+ show _ = "lol"
6057
61−class ( V.Curried (EndpointArgs endpoint) (Handler (EndpointRes endpoint)) ~ Server endpoint
62− , V.RecordToList (EndpointArgs endpoint)
58+class ( 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)
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
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+
10478
10579instance
10680 (Typeable responseType, Breakdown responseType) =>
@@ -110,6 +84,12 @@ instance
11084 type EndpointRes (Verb method statusCode contentTypes responseType) = responseType
11185 reifiedEndpointArguments = V.RNil
11286
87+instance ToReifiedEndpoint (NoContentVerb method)
88+ where
89+ type EndpointArgs (NoContentVerb method) = '[]
90+ type EndpointRes (NoContentVerb method) = NoContent
91+ reifiedEndpointArguments = V.RNil
92+
11393instance
11494 (ToReifiedEndpoint endpoint) =>
11595 ToReifiedEndpoint ((x :: Symbol) :> endpoint)
@@ -118,6 +98,16 @@ instance
11898 type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
11999 reifiedEndpointArguments = reifiedEndpointArguments @endpoint
120100
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+
121111instance
122112 (ToReifiedEndpoint endpoint) =>
123113 ToReifiedEndpoint (Description s :> endpoint)
@@ -160,6 +150,24 @@ instance
160150 tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
161151 V.:& reifiedEndpointArguments @endpoint
162152
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+
163171instance
164172 ( BuildFrom (IfRequiredLenient T.Text mods headerType)
165173 , 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−11
@@ -1,16 +1,10 @@
1−# nb: 16.8 doesn't work because the version of servant is too old to have FoldLenient', fixme.
2−resolver: lts-16.16
1+resolver: lts-20.4
32packages:
43- .
54
65extra-deps:
6+# - dependent-map-0.4.0.0@sha256:ca2b131046f4340a1c35d138c5a003fe4a5be96b14efc26291ed35fd08c62221,1657
7+# - dependent-sum-0.7.1.0@sha256:5599aa89637db434431b1dd3fa7c34bc3d565ee44f0519bfbc877be1927c2531,2068
78- servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
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
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: 532380
73− url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/16.yaml
74− sha256: d6b004b095fe2a0b8b14fbc30014ee97e58843b9c9362ddb9244273dda62649e
75− original: lts-16.16
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+11−11
@@ -1,36 +1,39 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
88module Breakdown where
99
1010import Data.Aeson
11+import Data.Hashable
1112import Data.Typeable (Typeable)
1213import GHC.Generics (Generic)
1314import Servant
14−import Data.Hashable
1515
1616data Foo = Foo Int String
1717 deriving (Generic, Eq, Show, Typeable)
1818
1919instance Hashable Foo
20+
2021instance ToJSON Foo
21−instance FromJSON Foo
2222
23+instance FromJSON Foo
2324
2425data SomeSum = A Int | B String
2526 deriving (Generic, Eq, Show, Typeable)
2627
2728instance Hashable SomeSum
29+
2830instance ToJSON SomeSum
29−instance FromJSON SomeSum
3031
32+instance FromJSON SomeSum
3133
32−type ProductApi = "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
33− :<|> "getFoo" :> Get '[JSON] Foo
34+type ProductApi =
35+ "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
36+ :<|> "getFoo" :> Get '[JSON] Foo
3437
3538eliminate :: Int -> Handler ()
3639eliminate _ = throwError $ err500 {errBody = "eliminate blew up, oh no!"}
@@ -38,13 +41,10 @@ eliminate _ = throwError $ err500 {errBody = "eliminate blew up, oh no!"}
3841productServer :: Server ProductApi
3942productServer = eliminate :<|> pure (Foo 12 "abc")
4043
41−
42−type SumApi
43− = "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
44+type SumApi =
45+ "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
4446 :<|> "getFoo1" :> Get '[JSON] SomeSum
4547 :<|> "getFoo2" :> Get '[JSON] SomeSum
4648
47−
48−
4949sumServer :: Server SumApi
5050sumServer = eliminate :<|> pure (B "hi") :<|> pure (A 3)
test/Foo.hs+3−1
@@ -8,17 +8,19 @@
88module Foo where
99
1010import Data.Aeson
11+import Data.Hashable
1112import Data.Typeable (Typeable)
1213import GHC.Generics (Generic)
1314import 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+
2122instance ToJSON Foo
23+
2224instance FromJSON Foo
2325
2426type Api =
test/Headers.hs+4−3
@@ -8,17 +8,19 @@
88module Headers where
99
1010import Data.Aeson
11+import Data.Hashable
1112import Data.Typeable (Typeable)
1213import GHC.Generics (Generic)
1314import 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+
2122instance ToJSON Foo
23+
2224instance FromJSON Foo
2325
2426type Api =
@@ -33,8 +35,7 @@ combine :: Maybe Foo -> Maybe Foo -> Handler Foo
3335combine (Just (Foo a)) (Just (Foo b)) = pure (Foo (a + b))
3436combine (Just a) Nothing = pure a
3537combine Nothing (Just a) = pure a
36−combine Nothing Nothing = pure (Foo 1)
37−
38+combine Nothing Nothing = pure (Foo 1)
3839
3940eliminate :: Foo -> Handler ()
4041eliminate (Foo a)
test/Nested.hsnew file+23
@@ -0,0 +1,23 @@
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+13−9
@@ -1,27 +1,31 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5−{-# LANGUAGE OverloadedStrings #-}
4+
5+
66{-# LANGUAGE TypeOperators #-}
77
88module Post where
99
10−import GHC.Generics (Generic)
11−import Servant
1210import Data.Aeson
1311import Data.Hashable
12+import GHC.Generics (Generic)
13+import Servant
1414
15−type Api = Get '[JSON] FooPost
16− :<|> ReqBody '[JSON] FooPost :> Post '[JSON] ()
15+type Api =
16+ Get '[JSON] FooPost
17+ :<|> ReqBody '[JSON] FooPost :> Post '[JSON] ()
1718
1819data FooPost = FooPost
19− deriving (Eq,Show,Generic)
20+ deriving (Eq, Show, Generic)
2021
2122instance Hashable FooPost
23+
2224instance ToJSON FooPost
25+
2326instance FromJSON FooPost
2427
2528server :: Server Api
26−server = pure FooPost
27− :<|> const (pure ())
29+server =
30+ pure FooPost
31+ :<|> const (pure ())
test/Product.hs+4−2
@@ -1,23 +1,25 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
88module Product where
99
1010import Data.Aeson
11+import Data.Hashable
1112import Data.Typeable (Typeable)
1213import GHC.Generics (Generic)
1314import Servant
14−import Data.Hashable
1515
1616data Foo = Foo Int String
1717 deriving (Generic, Eq, Show, Typeable)
1818
1919instance Hashable Foo
20+
2021instance ToJSON Foo
22+
2123instance FromJSON Foo
2224
2325type Api = "item" :> ReqBody '[JSON] Foo :> Post '[JSON] ()
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/Seeded.hs+9−5
@@ -8,22 +8,26 @@
88module Seeded where
99
1010import Data.Aeson
11+import Data.Hashable
1112import Data.Typeable (Typeable)
1213import GHC.Generics (Generic)
1314import 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+
2122instance FromJSON Seed
23+
2224instance Hashable Seed
2325
24−type Api = Capture "seed" Seed :> Get '[JSON] ()
25− :<|> Get '[JSON] ()
26+type Api =
27+ Capture "seed" Seed :> Get '[JSON] ()
28+ :<|> Get '[JSON] ()
2629
2730server :: Server Api
28−server = (\(Seed _) -> error "we blow up if we get here")
29− :<|> pure ()
31+server =
32+ (\(Seed _) -> error "we blow up if we get here")
33+ :<|> pure ()
test/Spec.hs+130−95
@@ -1,135 +1,170 @@
1+{-# LANGUAGE DerivingVia #-}
2+{-# LANGUAGE LambdaCase #-}
13{-# LANGUAGE OverloadedStrings #-}
2−{-# LANGUAGE StandaloneDeriving #-}
34{-# LANGUAGE RecordWildCards #-}
5+{-# LANGUAGE StandaloneDeriving #-}
46{-# LANGUAGE TypeApplications #-}
5−{-# LANGUAGE LambdaCase #-}
6−{-# LANGUAGE DerivingVia #-}
7−
7+{-# LANGUAGE RankNTypes #-}
8+{-# LANGUAGE FlexibleContexts #-}
9+{-# LANGUAGE AllowAmbiguousTypes #-}
10+{-# LANGUAGE ScopedTypeVariables #-}
11+{-# LANGUAGE DataKinds #-}
812{-# OPTIONS_GHC -fno-warn-orphans #-}
13+{-# LANGUAGE TypeFamilies #-}
14+
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)
920import qualified Foo
10−import qualified Seeded
11−import qualified Valid
1221import qualified Headers
22+import qualified Nested
1323import qualified Post
24+import qualified Put
1425import qualified Product
15−import qualified Breakdown
16−
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
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
2031import Test.Hspec
21−import Data.Void ( Void )
22−import Data.Maybe ( isNothing )
23−import Data.Hashable ( Hashable(hash) )
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 )
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
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− }
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 })
3964
4065spec :: Spec
4166spec = do
4267 describe "Basic usage" $ do
4368 describe "noError" $ do
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)
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)
6281
63− describe "seeded" $ do
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)
7382
74− describe "BuildFrom" $ do
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)
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)
7986
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)
8487
85− describe "Breakdown" $ do
86− it "handles products" $ do
87− RS.fuzz @Breakdown.ProductApi Breakdown.productServer defaultConfig noCheck
88− >>= (`shouldSatisfy` serverFailure)
88+ 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)
8993
90− it "handles sums" $ do
91− RS.fuzz @Breakdown.SumApi Breakdown.sumServer defaultConfig noCheck
92− >>= (`shouldSatisfy` serverFailure)
94+ describe "Foo" $
95+ fuzzBoth @Foo.Api "finds an error in a basic app" Foo.server R.defaultConfig (`shouldSatisfy` serverFailure)
9396
97+ describe "QueryParams" $
98+ fuzzBoth @QueryParams.Api "can handle query params" QueryParams.server R.defaultConfig { R.seed = [R.hashedDyn (12::Int)] }
99+ (`shouldSatisfy` isNothing)
94100
95−serverFailure :: Maybe RS.Report -> Bool
101+ 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
96119serverFailure = \case
97− Just RS.Report{..} ->
98− let RS.RoboservantException{..} = rsException
99− in failureReason /= RS.NoPossibleMoves
120+ Just R.Report {..} ->
121+ let R.RoboservantException {..} = rsException
122+ in failureReason /= R.NoPossibleMoves
100123 _ -> False
101124
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
130+
131+deriving via (R.Atom Headers.Foo) instance R.BuildFrom Headers.Foo
132+
133+deriving via (R.Atom Seeded.Seed) instance R.Breakdown Seeded.Seed
134+
135+deriving via (R.Atom Seeded.Seed) instance R.BuildFrom Seeded.Seed
102136
103−deriving via (RS.Atom Foo.Foo) instance RS.Breakdown Foo.Foo
104−deriving via (RS.Atom Foo.Foo) instance RS.BuildFrom Foo.Foo
137+deriving via (R.Atom Void) instance R.BuildFrom Void
105138
106−deriving via (RS.Atom Headers.Foo) instance RS.Breakdown Headers.Foo
107−deriving via (RS.Atom Headers.Foo) instance RS.BuildFrom Headers.Foo
139+deriving via (R.Atom Post.FooPost) instance R.Breakdown Post.FooPost
140+deriving via (R.Atom Post.FooPost) instance R.BuildFrom Post.FooPost
108141
109−deriving via (RS.Atom Seeded.Seed) instance RS.Breakdown Seeded.Seed
110−deriving via (RS.Atom Seeded.Seed) instance RS.BuildFrom Seeded.Seed
142+deriving via (R.Atom Put.Foo) instance R.Breakdown Put.Foo
143+deriving via (R.Atom Put.Foo) instance R.BuildFrom Put.Foo
111144
112145
113−-- instance RS.BuildFrom Seeded.Seed
114146
115−deriving via (RS.Atom Void) instance RS.BuildFrom Void
147+deriving via (R.Compound Breakdown.Foo) instance R.Breakdown Breakdown.Foo
116148
117−deriving via (RS.Atom Post.FooPost) instance RS.BuildFrom Post.FooPost
118−deriving via (RS.Atom Post.FooPost) instance RS.Breakdown Post.FooPost
149+deriving via (R.Compound Product.Foo) instance R.BuildFrom Product.Foo
119150
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
151+deriving via (R.Compound Breakdown.SomeSum) instance R.Breakdown Breakdown.SomeSum
123152
124153-- | `shouldFail` allows you to assert that a given `Spec` should contain at least one failing test.
125154-- this is often useful when testing tests.
126155shouldFail :: SpecWith a -> SpecWith a
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− })
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+ )
test/UnsafeIO.hs+13−15
@@ -1,41 +1,39 @@
11{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE DeriveGeneric #-}
2+
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
88module UnsafeIO where
99
10−import Data.Aeson()
11−import Servant
12−import Data.IORef (writeIORef, IORef, readIORef, newIORef)
13−import Control.Monad.Trans (MonadIO(liftIO))
10+import Control.Monad.Trans (MonadIO (liftIO))
11+import Data.Aeson ()
1412import qualified Data.ByteString.Lazy.Char8 as BL8
15−
13+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
14+import Servant
1615
1716type UnsafeApi =
1817 "add" :> Get '[JSON] ()
19− :<|> "healthcheck" :> Get '[JSON] ()
18+ :<|> "healthcheck" :> Get '[JSON] ()
2019
2120healthcheck :: IORef Int -> Handler ()
2221healthcheck ref = do
2322 t <- liftIO $ readIORef ref
2423 case t of
2524 0 -> pure ()
26− n -> throwError $ err500 {errBody = "observed inconsistency: " <> (BL8.pack $ show n)}
27−
28−
25+ n -> throwError $ err500 {errBody = "observed inconsistency: " <> BL8.pack (show n)}
2926
3027makeServer :: IO (Server UnsafeApi)
3128makeServer = do
3229 ref <- newIORef 0
33− pure $ unsafeMunge ref
34− :<|> healthcheck ref
30+ pure $
31+ unsafeMunge ref
32+ :<|> healthcheck ref
3533
3634unsafeMunge :: IORef Int -> Handler ()
3735unsafeMunge ref = liftIO $ do
3836 t <- readIORef ref
39− writeIORef ref (t+1)
37+ writeIORef ref (t + 1)
4038 t2 <- readIORef ref
41− writeIORef ref (t2-1)
39+ writeIORef ref (t2 -1)
test/Valid.hs+18−14
@@ -1,28 +1,31 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
88module Valid where
99
10−import Servant
1110import Data.Void
1211import GHC.Generics
13−import Servant.API.Generic
12+import Servant
1413import Servant.Server.Generic
1514
16−type Api = Get '[JSON] Int
17− :<|> Capture "void" Void :> Get '[JSON] ()
15+type Api =
16+ Get '[JSON] Int
17+ :<|> Capture "void" Void :> Get '[JSON] ()
1818
1919data Routes route
2020 = Routes
21− { getInt :: route :-
22− Get '[JSON] Int
23− , captureIt :: route :-
24− Capture "void" Void :> Get '[JSON] ()
25− } deriving Generic
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)
2629
2730type RoutedApi = ToServantApi Routes
2831
@@ -31,10 +34,11 @@ routedServer :: Server RoutedApi
3134routedServer = genericServer routes
3235
3336routes :: Routes AsServer
34−routes = Routes
35− { getInt = pure 7
36− , captureIt = const (pure ())
37− }
37+routes =
38+ Routes
39+ { getInt = pure 7,
40+ captureIt = const (pure ())
41+ }
3842
3943server :: Server Api
4044server = pure 7 :<|> const (pure ())