on master help sign in
32 files changed+875−342
since splitsnapshots
unifiedsplit
.github/workflows/cabal.yml+64 .github/workflows/ci.yml+2−2 EXAMPLE.md+73−24 Makefile+1−1 README.md+90−1 benchmarks/genFile.sh+15−2 benchmarks/prelude+7−16 benchmarks/runTiming.sh+2−2 package.yaml+12−3 roboservant.cabal+24−7 src/Roboservant/Client.hs+127 src/Roboservant/Direct.hs+33−31 src/Roboservant/Server.hs+36 src/Roboservant/Types.hs+3−2 src/Roboservant/Types/Breakdown.hs+16−3 src/Roboservant/Types/BuildFrom.hs+12−5 src/Roboservant/Types/FlattenServer.hs−44 src/Roboservant/Types/Internal.hs+2−2 src/Roboservant/Types/Orphans.hs+7 src/Roboservant/Types/ReifiedApi.hs+56−49 src/Roboservant/Types/ReifiedApi/Server.hs+114 stack.yaml+5−12 stack.yaml.lock+9−58 test/Breakdown.hs+1−1 test/Nested.hs+3−3 test/Post.hs+2−2 test/Product.hs+1−1 test/Put.hs+33 test/QueryParams.hs+15 test/Spec.hs+105−65 test/UnsafeIO.hs+3−3 test/Valid.hs+2−3
.github/workflows/cabal.ymlnew file+64
@@ -0,0 +1,64 @@
1+# modified from https://github.com/jgm/pandoc/blob/master/.github/workflows/ci.yml
2+name: Cabal CI
3+
4+on:
5+ push:
6+ branches:
7+ - '**'
8+ paths-ignore: []
9+ pull_request:
10+ paths-ignore: []
11+
12+jobs:
13+ linux:
14+
15+ runs-on: ubuntu-20.04
16+ strategy:
17+ fail-fast: false
18+ matrix:
19+ versions:
20+ - ghc: '8.8.4'
21+ cabal: '3.4'
22+ - ghc: '8.10.4'
23+ cabal: '3.4'
24+ steps:
25+ - uses: actions/checkout@v2
26+
27+ # need to install older cabal/ghc versions from ppa repository
28+
29+ - name: Install recent cabal/ghc
30+ uses: actions/setup-haskell@v1.1.3
31+ with:
32+ ghc-version: ${{ matrix.versions.ghc }}
33+ cabal-version: ${{ matrix.versions.cabal }}
34+
35+ # declare/restore cached things
36+ # caching doesn't work for scheduled runs yet
37+ # https://github.com/actions/cache/issues/63
38+
39+ - name: Cache cabal global package db
40+ id: cabal-global
41+ uses: actions/cache@v2
42+ with:
43+ path: |
44+ ~/.cabal
45+ key: ${{ runner.os }}-${{ matrix.versions.ghc }}-${{ matrix.versions.cabal }}-cabal-global-${{ hashFiles('cabal.project') }}
46+
47+ - name: Cache cabal work
48+ id: cabal-local
49+ uses: actions/cache@v2
50+ with:
51+ path: |
52+ dist-newstyle
53+ key: ${{ runner.os }}-${{ matrix.versions.ghc }}-${{ matrix.versions.cabal }}-cabal-local
54+
55+ - name: Install dependencies
56+ run: |
57+ cabal update
58+ cabal build all --dependencies-only --enable-tests --disable-optimization
59+ - name: Build
60+ run: |
61+ cabal build all --enable-tests --disable-optimization 2>&1 | tee build.log
62+ - name: Test
63+ run: |
64+ cabal test all --disable-optimization
.github/workflows/ci.yml+2−2
@@ -1,4 +1,4 @@
1−name: CI
1+name: Stack CI
22
33# Trigger the workflow on push or pull request, but only for the master branch
44on:
@@ -12,7 +12,7 @@ jobs:
1212 strategy:
1313 matrix:
1414 stack: ["2.3.3"]
15− ghc: ["8.8.3"]
15+ ghc: ["8.10.4"]
1616 os: [ubuntu-latest]
1717 steps:
1818 - uses: actions/checkout@v2
EXAMPLE.md+73−24
@@ -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,25 +58,66 @@ server introduce = introduce :<|> combine :<|> eliminate
4558 | otherwise = pure ()
4659```
4760
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.
63+
64+``` haskell
65+goodServer, badServer :: Server Api
66+goodServer = server (pure $ A 0)
67+badServer = server (pure $ A 1)
68+```
69+
4870In the test file, we first define the tests: the faulty server should fail and the good server should pass.
4971
5072```haskell
73+main :: IO ()
74+main = hspec spec
75+
76+spec :: Spec
5177spec = describe "example" $ do
5278 it "good server should not fail" $ do
53− fuzz @Api goodServer defaultConfig { coverageThreshold = 0.99 }
79+ RS.fuzz @Api goodServer config
5480 >>= (`shouldSatisfy` isNothing)
5581 it "bad server should fail" $ do
56− fuzz @Api badServer defaultConfig { coverageThreshold = 0.99 }
57− >>= (`shouldSatisfy` serverFailure)
82+ RS.fuzz @Api badServer config
83+ >>= (`shouldSatisfy` isJust)
5884```
5985
60−And unless we want to ship roboservant and all its dependencies to production, we also need
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+ }
108+```
109+
110+Unless we want to ship roboservant and all its dependencies to production, we also need
61111some orphan instances: because As are the only value we can get without
62112an input, we need to be able to break them down.
63113
64−```haskell
114+``` haskell
65115deriving via (Compound A) instance Breakdown A
66−-- 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
67121deriving via (Atom A) instance BuildFrom A
68122
69123```
@@ -76,19 +130,14 @@ deriving via (Compound B) instance BuildFrom B
76130deriving via (Atom B) instance Breakdown B
77131```
78132
79−finally some uninteresting utilities and the entrypoint
80−
81−```haskell
82−main = hspec spec
83133
84−serverFailure :: Maybe Report -> Bool
85−serverFailure c = case c of
86− Just Report{..} ->
87− let RoboservantException{..} = rsException
88− in failureReason /= NoPossibleMoves
89− _ -> False
134+test utilities:
90135
91−isNothing x = case x of
92− Nothing -> True
93− _ -> 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 })
94143```
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/genFile.sh+15−2
@@ -1,9 +1,11 @@
11n=$1
2+k=$2
23
34cat prelude
4−echo -n "type Api = \"route1\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
5+echo -n "type SubApi = \"route1\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
56for i in $(seq 2 $n); do
6− echo " :<|> \"route${i}\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
7+ echo -n " :<|> \"route${i}\" :> "
8+ echo "ReqBody '[JSON] Input :> Post '[JSON] Output"
79done
810
911echo -n "server = handler "
@@ -11,3 +13,14 @@ echo -n "server = handler "
1113for i in $(seq 2 $n); do
1214 echo -n ":<|> handler "
1315done
16+echo
17+echo
18+
19+echo -n "type Api = Flatten ("
20+for i in $(seq 1 $k); do
21+ echo -n '"foo" :> '
22+done;
23+
24+echo "SubApi )"
25+
26+
benchmarks/prelude+7−16
@@ -12,6 +12,7 @@ import Servant
1212import Data.Hashable
1313import GHC.Generics
1414import Data.Aeson
15+import Servant.API.Flatten
1516
1617data Input = Input1 { foo :: String }
1718 | Input2 { bar :: Int }
@@ -52,19 +53,9 @@ handler = pure . \case
5253 Input1 foo -> Output1 foo
5354 Input2 bar -> Output2 bar
5455
55−main = fuzz @Api server defaultConfig (pure ())
56−
57−defaultConfig :: Config
58−defaultConfig = Config {
59− -- you can pass extra values in using the seed argument. This can be useful
60− -- for things that might not be produceable within the api, like auth tokens.
61− seed = [hashedDyn "blah"]
62− , maxRuntime = 0.5
63− -- if we get to 1000 interactions with the api, call it quits.
64− , maxReps = 1000
65− -- if you're using this inside quickcheck or hedgehog, you might want to set this
66− -- from their seed to make sure it stays deterministic
67− , rngSeed = 0
68− -- 0 to 100: fail tests if we hit less than this percentage of endpoints.
69− , coverageThreshold = 0
70− }
56+main =
57+ -- fuzz @Api server defaultConfig
58+ pure ()
59+
60+f = flatten (Proxy @Api)
61+
benchmarks/runTiming.sh+2−2
@@ -1,8 +1,8 @@
11lo=$1
22hi=$2
3−
3+k=$3
44for i in $(seq $lo 5 $hi); do
5− ./genFile.sh $i > main.hs;
5+ ./genFile.sh $i $k > main.hs;
66 echo "sievedn: $i,";
77 /usr/bin/time -f "%e" stack ghc main.hs 2>&1;
88# /usr/bin/time -f "%e" stack ghc -- -fomit-interface-pragmas main.hs 2>&1;
package.yaml+12−3
@@ -1,6 +1,6 @@
11
22name: roboservant
3−version: 0.1.0.2
3+version: 0.1.0.3
44github: "mwotton/roboservant"
55license: BSD3
66copyright: "2020 Mark Wotton, Samuel Schlesinger"
@@ -19,6 +19,7 @@ dependencies:
1919- containers
2020- random
2121- hashable
22+- http-types
2223- lifted-base
2324- monad-control
2425- mtl
@@ -55,7 +56,10 @@ tests:
5556 - hspec
5657 - hspec-core
5758 - http-api-data
58−
59+ - http-client
60+ - hspec-wai
61+ - wai
62+ - warp
5963 example:
6064 main: Example.lhs
6165 source-dirs: .
@@ -63,5 +67,10 @@ tests:
6367 dependencies:
6468 - roboservant
6569 - hspec
66− # build-depends: base, markdown-unlit
70+ - warp
71+ - http-client
72+ - aeson
73+ - hspec-core
6774 ghc-options: -pgmL markdown-unlit
75+
76+ build-tools: markdown-unlit
roboservant.cabal+24−7
@@ -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: 3eb8db6b6899ba904be853bb6865aba4f208077587e89f5377df3169743034b0
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,15 +28,17 @@ source-repository head
2828library
2929 exposed-modules:
3030 Roboservant
31+ Roboservant.Client
3132 Roboservant.Direct
33+ Roboservant.Server
3234 Roboservant.Types
3335 Roboservant.Types.Breakdown
3436 Roboservant.Types.BuildFrom
3537 Roboservant.Types.Config
36− Roboservant.Types.Config.Internal
37− Roboservant.Types.FlattenServer
3838 Roboservant.Types.Internal
39+ Roboservant.Types.Orphans
3940 Roboservant.Types.ReifiedApi
41+ Roboservant.Types.ReifiedApi.Server
4042 other-modules:
4143 Paths_roboservant
4244 hs-source-dirs:
@@ -49,6 +51,7 @@ library
4951 , dependent-map
5052 , dependent-sum
5153 , hashable
54+ , http-types
5255 , lifted-base
5356 , monad-control
5457 , mtl
@@ -70,16 +73,22 @@ test-suite example
7073 other-modules:
7174 Paths_roboservant
7275 hs-source-dirs:
73− ./.
76+ ./
7477 ghc-options: -Wall -fwrite-ide-info -hiedir=.hie -pgmL markdown-unlit
78+ build-tool-depends:
79+ markdown-unlit:markdown-unlit
7580 build-depends:
76− base >=4.7 && <5
81+ aeson
82+ , base >=4.7 && <5
7783 , bytestring
7884 , containers
7985 , dependent-map
8086 , dependent-sum
8187 , hashable
8288 , hspec
89+ , hspec-core
90+ , http-client
91+ , http-types
8392 , lifted-base
8493 , monad-control
8594 , mtl
@@ -94,6 +103,7 @@ test-suite example
94103 , time
95104 , unordered-containers
96105 , vinyl
106+ , warp
97107 default-language: Haskell2010
98108
99109test-suite roboservant-test
@@ -106,6 +116,8 @@ test-suite roboservant-test
106116 Nested
107117 Post
108118 Product
119+ Put
120+ QueryParams
109121 Seeded
110122 UnsafeIO
111123 Valid
@@ -123,7 +135,10 @@ test-suite roboservant-test
123135 , hashable
124136 , hspec
125137 , hspec-core
138+ , hspec-wai
126139 , http-api-data
140+ , http-client
141+ , http-types
127142 , lifted-base
128143 , monad-control
129144 , mtl
@@ -138,4 +153,6 @@ test-suite roboservant-test
138153 , time
139154 , unordered-containers
140155 , vinyl
156+ , wai
157+ , warp
141158 default-language: Haskell2010
src/Roboservant/Client.hsnew file+127
@@ -0,0 +1,127 @@
1+{-# LANGUAGE RankNTypes #-}
2+{-# LANGUAGE TypeOperators #-}
3+{-# LANGUAGE NamedFieldPuns #-}
4+{-# LANGUAGE DataKinds #-}
5+{-# LANGUAGE AllowAmbiguousTypes #-}
6+{-# LANGUAGE DataKinds #-}
7+{-# LANGUAGE GADTs #-}
8+{-# LANGUAGE PolyKinds #-}
9+{-# LANGUAGE ScopedTypeVariables #-}
10+{-# LANGUAGE TypeApplications #-}
11+
12+{-# LANGUAGE UndecidableInstances #-}
13+{-# LANGUAGE TypeFamilies #-}
14+{-# LANGUAGE FlexibleContexts #-}
15+-- should all the NormalizeFunction instances be in one place?
16+{-# OPTIONS_GHC -fno-warn-orphans #-}
17+module Roboservant.Client where
18+
19+import Data.Proxy
20+import Servant.Client
21+import Roboservant.Types
22+import Roboservant(Report, fuzz')
23+import Servant
24+import Data.Bifunctor
25+import Data.List.NonEmpty (NonEmpty)
26+import Data.Dynamic (Dynamic,Typeable)
27+import qualified Data.Vinyl.Curry as V
28+import qualified Data.Text as T
29+import Control.Monad.Reader
30+import Data.Hashable
31+import Network.HTTP.Types.Status
32+
33+-- fuzz :: forall api.
34+-- (FlattenServer api, ToReifiedApi (Endpoints api)) =>
35+-- Server api ->
36+-- Config ->
37+-- IO (Maybe Report)
38+-- fuzz s = fuzz' (reifyServer s)
39+-- -- todo: how do we pull reifyServer out?
40+-- where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api))
41+-- => Server api -> ReifiedApi
42+-- reifyServer server = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
43+
44+fuzz :: forall api . (ToReifiedClientApi (Endpoints api), FlattenClient api, HasClient ClientM api)
45+ => ClientEnv -> Config -> IO (Maybe Report)
46+fuzz clientEnv
47+ = fuzz'
48+ (toReifiedClientApi
49+ (flattenClient @api apiClient) (Proxy @(Endpoints api)) clientEnv)
50+ where apiClient = client (Proxy @api)
51+
52+
53+
54+class ToReifiedClientApi api where
55+ toReifiedClientApi :: ClientBundled api -> Proxy api -> ClientEnv -> ReifiedApi
56+
57+data ClientBundled endpoints where
58+ AClientEndpoint :: Client ClientM endpoint -> ClientBundled endpoints -> ClientBundled (endpoint ': endpoints)
59+ NoClientEndpoints :: ClientBundled '[]
60+
61+
62+class FlattenClient api where
63+ flattenClient :: Client ClientM api -> ClientBundled (Endpoints api)
64+
65+instance
66+ ( NormalizeFunction (Client ClientM endpoint)
67+ , Normal (Client ClientM endpoint) ~ V.Curried (EndpointArgs endpoint) (ReaderT ClientEnv IO (Either InteractionError (NonEmpty (Dynamic,Int))))
68+ , ToReifiedClientApi endpoints
69+ , V.RecordCurry' (EndpointArgs endpoint)
70+ , ToReifiedEndpoint endpoint) =>
71+ ToReifiedClientApi (endpoint : endpoints) where
72+ toReifiedClientApi (endpoint `AClientEndpoint` endpoints) _ clientEnv =
73+ (0, ReifiedEndpoint
74+ { reArguments = reifiedEndpointArguments @endpoint
75+ , reEndpointFunc = foo (normalize endpoint)
76+ }
77+ )
78+ : (map . first) (+1)
79+ (toReifiedClientApi endpoints (Proxy @endpoints) clientEnv)
80+ where
81+
82+ foo :: V.Curried (EndpointArgs endpoint) (ReaderT ClientEnv IO ResultType)
83+ -> V.Curried (EndpointArgs endpoint) (IO ResultType)
84+ foo = mapCurried @(EndpointArgs endpoint) @(ReaderT ClientEnv IO ResultType) (`runReaderT` clientEnv)
85+
86+mapCurried :: forall ts a b. V.RecordCurry' ts => (a -> b) -> V.Curried ts a -> V.Curried ts b
87+mapCurried f g = V.rcurry' @ts $ f . V.runcurry' g
88+
89+type ResultType = Either InteractionError (NonEmpty (Dynamic,Int))
90+-- runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
91+
92+
93+instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (ClientM x) where
94+ type Normal (ClientM x) = ReaderT ClientEnv IO (Either InteractionError (NonEmpty (Dynamic,Int)))
95+ normalize c = ReaderT $
96+ fmap (bimap renderClientError breakdown) . runClientM c
97+ where
98+ renderClientError :: ClientError -> InteractionError
99+ renderClientError err = case err of
100+ FailureResponse _ Response{responseStatusCode} -> InteractionError textual (responseStatusCode == status500)
101+ _ -> InteractionError textual True
102+
103+ where textual = T.pack $ show err
104+instance ToReifiedClientApi '[] where
105+ toReifiedClientApi NoClientEndpoints _ _ = []
106+
107+
108+instance
109+ ( FlattenClient api,
110+ Endpoints endpoint ~ '[endpoint]
111+ ) =>
112+ FlattenClient (endpoint :<|> api)
113+ where
114+ flattenClient (endpoint :<|> c) = endpoint `AClientEndpoint` flattenClient @api c
115+
116+instance
117+ (
118+ Endpoints api ~ '[api]
119+ ) =>
120+ FlattenClient (x :> api)
121+ where
122+ flattenClient c = c `AClientEndpoint` NoClientEndpoints
123+
124+
125+instance FlattenClient (Verb method statusCode contentTypes responseType)
126+ where
127+ flattenClient c = c `AClientEndpoint` NoClientEndpoints
src/Roboservant/Direct.hs+33−31
@@ -7,12 +7,12 @@
77{-# LANGUAGE RecordWildCards #-}
88{-# LANGUAGE ScopedTypeVariables #-}
99{-# LANGUAGE TupleSections #-}
10−{-# LANGUAGE TypeApplications #-}
10+
1111{-# LANGUAGE TypeFamilies #-}
1212{-# LANGUAGE UndecidableInstances #-}
1313
1414module Roboservant.Direct
15− ( fuzz,
15+ ( fuzz',
1616 Config (..),
1717 -- TODO come up with something smarter than exporting all this, we should
1818 -- have some nice error-display functions
@@ -56,16 +56,16 @@ import GHC.Generics ((:*:) (..))
5656import Roboservant.Types
5757 ( ApiOffset (..),
5858 Argument (..),
59− FlattenServer (..),
59+ InteractionError(..),
6060 Provenance (..),
61+ ReifiedApi,
6162 ReifiedEndpoint (..),
6263 Stash (..),
6364 StashValue (..),
64− ToReifiedApi (..),
6565 TypedF,
6666 )
6767import Roboservant.Types.Config
68−import Servant (Endpoints, Proxy (Proxy), Server, ServerError (..))
68+
6969import System.Random (Random (randomR), StdGen, mkStdGen)
7070import qualified Type.Reflection as R
7171
@@ -105,7 +105,7 @@ data EndpointOption
105105 = forall as.
106106 (V.RecordToList as, V.RMap as) =>
107107 EndpointOption
108− { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))),
108+ { eoCall :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))),
109109 eoArgs :: V.Rec (TypedF StashValue) as
110110 }
111111
@@ -121,13 +121,18 @@ data Report
121121 }
122122 deriving (Show)
123123
124−fuzz ::
125− forall api.
126− (FlattenServer api, ToReifiedApi (Endpoints api)) =>
127− Server api ->
124+
125+
126+-- fuzzClient :: Client api -> Config -> IO (Maybe Report)
127+-- fuzzClient = undefined
128+
129+
130+
131+fuzz' ::
132+ ReifiedApi ->
128133 Config ->
129134 IO (Maybe Report)
130−fuzz server Config {..} = handle (pure . Just . formatException) $ do
135+fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
131136 let path = []
132137 stash = addToStash seed mempty
133138 currentRng = mkStdGen rngSeed
@@ -145,12 +150,12 @@ fuzz server Config {..} = handle (pure . Just . formatException) $ do
145150 Report
146151 (unlines [show failureType, show exception])
147152 r
148− displayDiagnostics FuzzState {..} = liftIO $ do
153+ displayDiagnostics FuzzState {..} = liftIO $
149154 logInfo $ unlines $
150− ["api endpoints covered"]
151− <> (map show . Set.toList . Set.fromList $ map apiOffset path)
152− <> ["", "types in stash"]
153− <> DM.foldrWithKey (\_ v r -> (show . NEL.length . getStashValue $ v) : r) [] (getStash stash)
155+ ["api endpoints covered"]
156+ <> (map show . Set.toList . Set.fromList $ map apiOffset path)
157+ <> ["", "types in stash"]
158+ <> DM.foldrWithKey (\_ v r -> (show . NEL.length . getStashValue $ v) : r) [] (getStash stash)
154159 -- <> (map (show . NEL.length . getStashValue ) $ DM.assocs (getStash stash))
155160 -- $ \_k v ->
156161 -- (show . NEL.length $ getStashValue v))
@@ -173,7 +178,7 @@ fuzz server Config {..} = handle (pure . Just . formatException) $ do
173178 else do
174179 _ <- action
175180 untilDone (n -1, deadline) action
176− reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
181+
177182 routeCount = length reifiedApi
178183 elementOrFail ::
179184 (MonadState FuzzState m, MonadIO m) =>
@@ -190,7 +195,7 @@ fuzz server Config {..} = handle (pure . Just . formatException) $ do
190195 ( forall as.
191196 (V.RecordToList as, V.RMap as) =>
192197 FuzzOp ->
193− V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))) ->
198+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
194199 V.Rec (TypedF V.Identity) as ->
195200 m r
196201 ) ->
@@ -231,23 +236,21 @@ fuzz server Config {..} = handle (pure . Just . formatException) $ do
231236 execute ::
232237 (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
233238 FuzzOp ->
234− V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))) ->
239+ V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
235240 V.Rec (TypedF V.Identity) as ->
236241 m ()
237242 execute fuzzop func args = do
238243 (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
239244 liftIO (V.runcurry' func argVals) >>= \case
240− -- parameterise this
241− Left (serverError :: ServerError) ->
242− case errHTTPCode serverError of
243− 500 -> throw serverError
244− _ ->
245− liftIO . logInfo . show $ ("ignoring non-500 error", serverError)
246− Right (dyn :: NEL.NonEmpty (Dynamic, Int)) -> do
245+ Left (e::InteractionError) ->
246+ if fatalError e
247+ then throw e
248+ else pure ()
249+ Right (dyn :: NEL.NonEmpty (Dynamic, Int)) ->
247250 modify'
248− ( \fs@FuzzState {..} ->
249− fs {stash = addToStash (NEL.toList dyn) stash}
250− )
251+ ( \fs@FuzzState {..} ->
252+ fs {stash = addToStash (NEL.toList dyn) stash}
253+ )
251254 where
252255 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
253256 -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
@@ -259,8 +262,7 @@ fuzz server Config {..} = handle (pure . Just . formatException) $ do
259262 (execute op func args)
260263 [ Handler (\(e :: SomeAsyncException) -> throw e),
261264 Handler
262− ( \(e :: SomeException) -> do
263− -- displayDiagnostics =<< get
265+ ( \(e :: SomeException) ->
264266 throw . RoboservantException ServerCrashed (Just e) =<< get
265267 )
266268 ]
src/Roboservant/Server.hsnew file+36
@@ -0,0 +1,36 @@
1+{-# LANGUAGE RankNTypes #-}
2+{-# LANGUAGE TypeApplications #-}
3+{-# LANGUAGE PolyKinds #-}
4+{-# LANGUAGE AllowAmbiguousTypes #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE GADTs #-}
7+{-# LANGUAGE PolyKinds #-}
8+
9+{-# LANGUAGE ScopedTypeVariables #-}
10+{-# LANGUAGE TypeApplications #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+
14+module Roboservant.Server (fuzz, module Roboservant.Types ) where
15+
16+import Roboservant.Direct(fuzz',Report)
17+import Roboservant.Types
18+ ( FlattenServer (..),
19+ ReifiedApi,
20+ )
21+import Roboservant.Types.ReifiedApi.Server(ToReifiedApi (..))
22+import Servant (Endpoints, Proxy (Proxy), Server)
23+import Roboservant.Types.Config
24+
25+fuzz :: forall api.
26+ (FlattenServer api, ToReifiedApi (Endpoints api)) =>
27+ Server api ->
28+ Config ->
29+ IO (Maybe Report)
30+fuzz s = fuzz' (reifyServer s)
31+ -- todo: how do we pull reifyServer out?
32+ where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api))
33+ => Server api -> ReifiedApi
34+ reifyServer server = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
35+-- reifyServer server = toReifiedApi server (Proxy @(Endpoints api))
36+
src/Roboservant/Types.hs+3−2
@@ -10,8 +10,8 @@
1010module Roboservant.Types
1111 ( module Roboservant.Types.Breakdown,
1212 module Roboservant.Types.BuildFrom,
13− module Roboservant.Types.FlattenServer,
1413 module Roboservant.Types.ReifiedApi,
14+ module Roboservant.Types.ReifiedApi.Server,
1515 module Roboservant.Types.Internal,
1616 module Roboservant.Types.Config,
1717 )
@@ -20,6 +20,7 @@ where
2020import Roboservant.Types.Breakdown
2121import Roboservant.Types.BuildFrom
2222import Roboservant.Types.Config
23−import Roboservant.Types.FlattenServer
2423import Roboservant.Types.Internal
2524import Roboservant.Types.ReifiedApi
25+import Roboservant.Types.ReifiedApi.Server
26+import Roboservant.Types.Orphans()
src/Roboservant/Types/Breakdown.hs+16−3
@@ -1,5 +1,6 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
22{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE FlexibleContexts #-}
34{-# LANGUAGE DerivingVia #-}
45{-# LANGUAGE FlexibleInstances #-}
56{-# LANGUAGE GADTs #-}
@@ -14,7 +15,7 @@
1415
1516module Roboservant.Types.Breakdown where
1617
17−import Data.Dynamic (Dynamic, toDyn)
18+import Data.Dynamic (Dynamic)
1819import Data.Hashable
1920import Data.Kind
2021import Data.List.NonEmpty (NonEmpty (..))
@@ -22,6 +23,8 @@ import qualified Data.List.NonEmpty as NEL
2223import Data.Typeable (Typeable)
2324import GHC.Generics
2425import Roboservant.Types.Internal
26+import Servant
27+import Roboservant.Types.Orphans()
2528
2629breakdown ::
2730 (Hashable x, Typeable x, Breakdown x) =>
@@ -38,10 +41,15 @@ instance (Hashable x, Typeable x) => Breakdown (Atom x) where
3841deriving via (Atom ()) instance Breakdown ()
3942
4043deriving via (Atom Int) instance Breakdown Int
44+deriving via (Atom Char) instance Breakdown Char
4145
42−deriving via (Atom [Char]) instance Breakdown [Char]
46+deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, Breakdown x) => Breakdown (Maybe x)
4347
44−class GBreakdown (f :: k -> *) where
48+instance (Hashable x, Typeable x, Breakdown x) => Breakdown [x] where
49+ breakdownExtras stash = concatMap (NEL.toList . breakdown) stash
50+
51+
52+class GBreakdown (f :: k -> Type) where
4553 gBreakdownExtras :: f a -> [(Dynamic, Int)]
4654
4755instance (Hashable x, Typeable x, Generic x, GBreakdown (Rep x)) => Breakdown (Compound (x :: Type)) where
@@ -66,3 +74,8 @@ instance (GBreakdown a, GBreakdown b) => GBreakdown (a :+: b) where
6674
6775instance (Hashable a, Typeable a, Breakdown a) => GBreakdown (K1 R a) where
6876 gBreakdownExtras (K1 c) = NEL.toList $ breakdown c
77+
78+instance GBreakdown U1 where
79+ gBreakdownExtras U1 = []
80+
81+deriving via (Atom NoContent) instance Breakdown NoContent
src/Roboservant/Types/BuildFrom.hs+12−5
@@ -12,7 +12,7 @@
1212
1313module Roboservant.Types.BuildFrom where
1414
15−import Control.Monad (filterM)
15+import Data.List(nub)
1616import qualified Data.Dependent.Map as DM
1717import Data.Hashable
1818import qualified Data.IntSet as IntSet
@@ -23,6 +23,8 @@ import Data.Typeable (Typeable)
2323import GHC.Generics
2424import Roboservant.Types.Internal
2525import qualified Type.Reflection as R
26+import Servant(NoContent)
27+import Roboservant.Types.Orphans()
2628
2729buildFrom :: forall x. (Hashable x, BuildFrom x, Typeable x) => Stash -> Maybe (StashValue x)
2830buildFrom = buildStash . buildFrom'
@@ -60,10 +62,13 @@ deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, BuildFrom x)
6062
6163-- this isn't wonderful, but we need a hand-rolled instance for recursive datatypes right now.
6264-- with an arbitrary-ish interface, we could use a size parameter, rng access etc.
63−instance (BuildFrom x) => BuildFrom [x] where
64− extras stash = map (\xs -> (concatMap fst xs, map snd xs)) $ powerset $ extras @x stash
65+instance (Eq x, BuildFrom x) => BuildFrom [x] where
66+ extras stash =
67+ nub $ map (\xs -> (concatMap fst xs, map snd xs)) $ notpowerset $ buildFrom' @x stash
6568 where
66− powerset xs = filterM (const [True, False]) xs
69+ -- powerset creates way too much stuff. something better here eventually.
70+ notpowerset xs = []:xs:map pure xs
71+
6772
6873instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x :: Type)) where
6974 extras stash = fmap (Compound . to) <$> gExtras stash
@@ -72,7 +77,7 @@ deriving via (Atom Int) instance BuildFrom Int
7277
7378deriving via (Atom Char) instance BuildFrom Char
7479
75−class GBuildFrom (f :: k -> *) where
80+class GBuildFrom (f :: k -> Type) where
7681 gExtras :: Stash -> [([Provenance], f a)]
7782
7883instance GBuildFrom b => GBuildFrom (M1 D a b) where
@@ -98,3 +103,5 @@ instance BuildFrom a => GBuildFrom (K1 i a) where
98103
99104instance GBuildFrom U1 where
100105 gExtras _ = [([], U1)]
106+
107+deriving via (Atom NoContent) instance BuildFrom NoContent
src/Roboservant/Types/FlattenServer.hsdeleted−44
@@ -1,44 +0,0 @@
1−{-# LANGUAGE AllowAmbiguousTypes #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE GADTs #-}
4−{-# LANGUAGE PolyKinds #-}
5−{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE TypeApplications #-}
7−{-# LANGUAGE TypeOperators #-}
8−{-# LANGUAGE UndecidableInstances #-}
9−
10−-- | terrible name, this really just pulls stuff out where we can fiddle with it.
11−module Roboservant.Types.FlattenServer where
12−
13−import Servant
14−
15−data Bundled endpoints where
16− AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
17− NoEndpoints :: Bundled '[]
18−
19−class FlattenServer api where
20− flattenServer :: Server api -> Bundled (Endpoints api)
21−
22−instance
23− ( FlattenServer api,
24− Endpoints endpoint ~ '[endpoint]
25− ) =>
26− FlattenServer (endpoint :<|> api)
27− where
28− flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
29−
30−instance
31− ( HasServer (x :> api) '[],
32− Endpoints (x :> api) ~ '[x :> api]
33− ) =>
34− FlattenServer (x :> api)
35− where
36− flattenServer server = server `AnEndpoint` NoEndpoints
37−
38−instance
39− ( Endpoints (Verb method statusCode contentTypes responseType) ~ '[Verb method statusCode contentTypes responseType],
40− HasServer (Verb method statusCode contentTypes responseType) '[]
41− ) =>
42− FlattenServer (Verb method statusCode contentTypes responseType)
43− where
44− flattenServer server = server `AnEndpoint` NoEndpoints
src/Roboservant/Types/Internal.hs+2−2
@@ -43,11 +43,11 @@ instance Show Stash where
4343
4444-- | Can't be built up from parts, can't be broken down further.
4545newtype Atom x = Atom {unAtom :: x}
46− deriving newtype (Hashable, Typeable)
46+ deriving newtype (Hashable, Typeable,Eq)
4747
4848-- | can be broken down and built up from generic pieces
4949newtype Compound x = Compound {unCompound :: x}
50− deriving newtype (Hashable, Typeable)
50+ deriving newtype (Hashable, Typeable, Eq)
5151
5252hashedDyn :: (Hashable a, Typeable a) => a -> (Dynamic, Int)
5353hashedDyn a = (toDyn a, hash a)
src/Roboservant/Types/Orphans.hsnew file+7
@@ -0,0 +1,7 @@
1+{-# OPTIONS_GHC -fno-warn-orphans #-}
2+module Roboservant.Types.Orphans where
3+
4+import Servant
5+import Data.Hashable
6+
7+instance Hashable NoContent
src/Roboservant/Types/ReifiedApi.hs+56−49
@@ -1,3 +1,8 @@
1+-- | Description: ways to build a reified api from a servant description.
2+--
3+-- arguably this could be more general and be abstracted away from even relying on servant
4+-- but that's future work.
5+
16{-# LANGUAGE AllowAmbiguousTypes #-}
27{-# LANGUAGE DataKinds #-}
38{-# LANGUAGE DerivingStrategies #-}
@@ -15,25 +20,22 @@
1520{-# LANGUAGE CPP #-}
1621module Roboservant.Types.ReifiedApi where
1722
18−import Control.Monad.Except (runExceptT)
19−import Data.Bifunctor
2023import Data.Dynamic (Dynamic)
21−import Data.Kind
24+import Control.Exception(Exception)
2225import Data.List.NonEmpty (NonEmpty)
2326import Data.Typeable (Typeable)
2427import GHC.Generics ((:*:)(..))
25−import GHC.TypeLits (Symbol)
2628import Roboservant.Types.Internal
2729import Roboservant.Types.Breakdown
2830import Roboservant.Types.BuildFrom
29−import Roboservant.Types.FlattenServer
31+import Data.Kind(Type)
3032import Servant
3133import Servant.API.Modifiers(FoldRequired,FoldLenient)
34+import GHC.TypeLits (Symbol)
3235import qualified Data.Text as T
3336import qualified Data.Vinyl as V
3437import qualified Data.Vinyl.Curry as V
3538import qualified Type.Reflection as R
36−import Data.Hashable(Hashable)
3739
3840newtype ApiOffset = ApiOffset Int
3941 deriving (Eq, Show, Ord)
@@ -47,61 +49,32 @@ newtype Argument a = Argument
4749
4850data ReifiedEndpoint = forall as. (V.RecordToList as, V.RMap as) => ReifiedEndpoint
4951 { reArguments :: V.Rec (TypedF Argument) as
50− , reEndpointFunc :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int))))
52+ , reEndpointFunc :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
5153 }
5254
53−type ReifiedApi = [(ApiOffset, ReifiedEndpoint)]
54−
55−tagType :: Typeable a => f a -> TypedF f a
56−tagType = (R.typeRep :*:)
57−
58−class ToReifiedApi (endpoints ) where
59−
60− toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
55+instance Show ReifiedEndpoint where
56+ show _ = "lol"
6157
6258class ( V.RecordToList (EndpointArgs endpoint)
6359 , V.RMap (EndpointArgs endpoint)
64− ) => ToReifiedEndpoint (endpoint :: *) where
60+ ) => ToReifiedEndpoint (endpoint :: Type) where
6561 type EndpointArgs endpoint :: [Type]
6662 type EndpointRes endpoint :: Type
6763
6864 reifiedEndpointArguments :: V.Rec (TypedF Argument) (EndpointArgs endpoint)
6965
70−instance ToReifiedApi '[] where
71− toReifiedApi NoEndpoints _ = []
7266
73−instance
74− ( Typeable (EndpointRes endpoint)
67+tagType :: Typeable a => f a -> TypedF f a
68+tagType = (R.typeRep :*:)
69+
70+data InteractionError = InteractionError
71+ { errorMessage :: T.Text
72+ , fatalError :: Bool
73+ }
74+ deriving Show
75+instance Exception InteractionError
76+
7577
76− , NormalizeFunction (ServerT endpoint Handler)
77− , Normal (ServerT endpoint Handler) ~ V.Curried (EndpointArgs endpoint) (IO (Either ServerError (NonEmpty (Dynamic,Int))))
78− , ToReifiedEndpoint endpoint
79− , ToReifiedApi endpoints
80− ) =>
81− ToReifiedApi (endpoint : endpoints)
82− where
83− toReifiedApi (endpoint `AnEndpoint` endpoints) _ =
84− (0, ReifiedEndpoint
85− { reArguments = reifiedEndpointArguments @endpoint
86− , reEndpointFunc = normalize endpoint
87− }
88− )
89− : (map . first) (+1)
90− (toReifiedApi endpoints (Proxy @endpoints))
91−
92−class NormalizeFunction m where
93− type Normal m
94− normalize :: m -> Normal m
95−
96−instance NormalizeFunction x => NormalizeFunction (r -> x) where
97− type Normal (r -> x) = r -> Normal x
98− normalize = fmap normalize
99−
100−instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x) where
101− type Normal (Handler x) = IO (Either ServerError (NonEmpty (Dynamic,Int)))
102− normalize handler = (runExceptT . runHandler') handler >>= \case
103− Left serverError -> pure (Left serverError)
104− Right x -> pure $ Right $ breakdown x
10578
10679instance
10780 (Typeable responseType, Breakdown responseType) =>
@@ -111,6 +84,12 @@ instance
11184 type EndpointRes (Verb method statusCode contentTypes responseType) = responseType
11285 reifiedEndpointArguments = V.RNil
11386
87+instance ToReifiedEndpoint (NoContentVerb method)
88+ where
89+ type EndpointArgs (NoContentVerb method) = '[]
90+ type EndpointRes (NoContentVerb method) = NoContent
91+ reifiedEndpointArguments = V.RNil
92+
11493instance
11594 (ToReifiedEndpoint endpoint) =>
11695 ToReifiedEndpoint ((x :: Symbol) :> endpoint)
@@ -119,6 +98,16 @@ instance
11998 type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
12099 reifiedEndpointArguments = reifiedEndpointArguments @endpoint
121100
101+instance
102+ (ToReifiedEndpoint endpoint) =>
103+ ToReifiedEndpoint (RemoteHost :> endpoint)
104+ where
105+ type EndpointArgs (RemoteHost :> endpoint) = EndpointArgs endpoint
106+ type EndpointRes (RemoteHost :> endpoint) = EndpointRes endpoint
107+ reifiedEndpointArguments = reifiedEndpointArguments @endpoint
108+
109+
110+
122111instance
123112 (ToReifiedEndpoint endpoint) =>
124113 ToReifiedEndpoint (Description s :> endpoint)
@@ -161,6 +150,24 @@ instance
161150 tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
162151 V.:& reifiedEndpointArguments @endpoint
163152
153+
154+instance
155+ ( BuildFrom paramType
156+ , ToReifiedEndpoint endpoint
157+ , Show paramType
158+ , Eq paramType
159+ ) =>
160+ ToReifiedEndpoint (QueryParams name paramType :> endpoint)
161+ where
162+ type EndpointArgs (QueryParams name paramType :> endpoint) = [paramType] ': EndpointArgs endpoint
163+ type EndpointRes (QueryParams name paramType :> endpoint) = EndpointRes endpoint
164+ reifiedEndpointArguments =
165+ tagType (Argument (buildFrom @[paramType]))
166+ V.:& reifiedEndpointArguments @endpoint
167+
168+
169+
170+
164171instance
165172 ( BuildFrom (IfRequiredLenient T.Text mods headerType)
166173 , ToReifiedEndpoint endpoint
src/Roboservant/Types/ReifiedApi/Server.hsnew file+114
@@ -0,0 +1,114 @@
1+{-# LANGUAGE RankNTypes #-}
2+{-# LANGUAGE TypeApplications #-}
3+{-# LANGUAGE PolyKinds #-}
4+{-# LANGUAGE AllowAmbiguousTypes #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE GADTs #-}
7+{-# LANGUAGE LambdaCase #-}
8+{-# LANGUAGE PolyKinds #-}
9+
10+{-# LANGUAGE ScopedTypeVariables #-}
11+{-# LANGUAGE TypeApplications #-}
12+{-# LANGUAGE TypeFamilies #-}
13+{-# LANGUAGE UndecidableInstances #-}
14+{-# LANGUAGE CPP #-}
15+{-# LANGUAGE DataKinds #-}
16+{-# LANGUAGE TypeOperators #-}
17+{-# LANGUAGE FlexibleInstances #-}
18+{-# LANGUAGE MultiParamTypeClasses #-}
19+module Roboservant.Types.ReifiedApi.Server(module Roboservant.Types.ReifiedApi.Server) where
20+
21+import Servant
22+
23+import Control.Monad.Except (runExceptT)
24+import Data.Bifunctor
25+import Data.Dynamic (Dynamic)
26+import Data.List.NonEmpty (NonEmpty)
27+import Data.Typeable (Typeable)
28+import Roboservant.Types.Breakdown
29+import Roboservant.Types.ReifiedApi
30+
31+import qualified Data.Text as T
32+import qualified Data.Vinyl.Curry as V
33+import Data.Hashable(Hashable)
34+
35+type ReifiedApi = [(ApiOffset, ReifiedEndpoint )]
36+
37+
38+class ToReifiedApi endpoints where
39+ toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
40+
41+instance ToReifiedApi '[] where
42+ toReifiedApi NoEndpoints _ = []
43+
44+instance
45+ ( NormalizeFunction (ServerT endpoint Handler)
46+ , Normal (ServerT endpoint Handler) ~ V.Curried (EndpointArgs endpoint) (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
47+ , ToReifiedEndpoint endpoint
48+ , ToReifiedApi endpoints
49+ ) =>
50+ ToReifiedApi (endpoint : endpoints)
51+ where
52+ toReifiedApi (endpoint `AnEndpoint` endpoints) _ =
53+ (0, ReifiedEndpoint
54+ { reArguments = reifiedEndpointArguments @endpoint
55+ , reEndpointFunc = normalize endpoint
56+ }
57+ )
58+ : (map . first) (+1)
59+ (toReifiedApi endpoints (Proxy @endpoints))
60+
61+
62+instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x) where
63+ type Normal (Handler x) = IO (Either InteractionError (NonEmpty (Dynamic,Int)))
64+ normalize handler = (runExceptT . runHandler') handler >>= \case
65+ Left serverError -> pure (Left (renderServerError serverError))
66+ where
67+ -- | TODO improve this
68+ renderServerError :: ServerError -> InteractionError
69+ renderServerError s = InteractionError (T.pack $ show s) (errHTTPCode serverError == 500)
70+
71+ Right x -> pure $ Right $ breakdown x
72+
73+
74+-- case errHTTPCode serverError of
75+-- 500 -> throw serverError
76+-- _ ->
77+-- liftIO . logInfo . show $ ("ignoring non-500 error", serverError)
78+
79+
80+data Bundled endpoints where
81+ -- AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
82+ AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
83+ NoEndpoints :: Bundled '[]
84+
85+class FlattenServer api where
86+ flattenServer :: Server api -> Bundled (Endpoints api)
87+
88+instance
89+ ( FlattenServer api,
90+ Endpoints endpoint ~ '[endpoint]
91+ ) =>
92+ FlattenServer (endpoint :<|> api)
93+ where
94+ flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
95+
96+instance
97+ (
98+ Endpoints api ~ '[api]
99+ ) =>
100+ FlattenServer (x :> api)
101+ where
102+ flattenServer server = server `AnEndpoint` NoEndpoints
103+
104+instance FlattenServer (Verb method statusCode contentTypes responseType)
105+ where
106+ flattenServer server = server `AnEndpoint` NoEndpoints
107+
108+class NormalizeFunction m where
109+ type Normal m
110+ normalize :: m -> Normal m
111+
112+instance NormalizeFunction x => NormalizeFunction (r -> x) where
113+ type Normal (r -> x) = r -> Normal x
114+ normalize = fmap normalize
stack.yaml+5−12
@@ -1,17 +1,10 @@
1−resolver: lts-16.27
2−# bit faster on ReifiedApi stuff which is unfortunately terribly slow.
3−# compiler: ghc-8.10.3
1+resolver: lts-20.4
42packages:
53- .
64
75extra-deps:
6+# - dependent-map-0.4.0.0@sha256:ca2b131046f4340a1c35d138c5a003fe4a5be96b14efc26291ed35fd08c62221,1657
7+# - dependent-sum-0.7.1.0@sha256:5599aa89637db434431b1dd3fa7c34bc3d565ee44f0519bfbc877be1927c2531,2068
88- servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
9−- vinyl-0.13.0
10−- dependent-sum-0.7.1.0
11−- constraints-extras-0.3.0.2
12−- dependent-map-0.4.0.0
13−
14−- servant-0.18.2
15−- servant-client-0.18.2
16−- servant-client-core-0.18.2
17−- servant-server-0.18.2
9+# - constraints-extras-0.3.0.2@sha256:013b8d0392582c6ca068e226718a4fe8be8e22321cc0634f6115505bf377ad26,1853
10+- some-1.0.3
stack.yaml.lock+9−58
@@ -7,69 +7,20 @@ packages:
77- completed:
88 hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
99 pantry-tree:
10− size: 325
1110 sha256: 04f12c7bef2c3f9a25d94eb9489752ed498db8e243069fe95838dbb51df1dcb3
11+ size: 325
1212 original:
1313 hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
1414- completed:
15− hackage: vinyl-0.13.0@sha256:0f247cd3f8682b30881a07de18e6fec52d540646fbcb328420049cc8d63cd407,3724
16− pantry-tree:
17− size: 1857
18− sha256: 860fb95820b595161cdbdec5f376100ebae2d14e5ef0dbe311546202f7525d01
19− original:
20− hackage: vinyl-0.13.0
21−- completed:
22− hackage: dependent-sum-0.7.1.0@sha256:5599aa89637db434431b1dd3fa7c34bc3d565ee44f0519bfbc877be1927c2531,2068
23− pantry-tree:
24− size: 290
25− sha256: 9cbfb32b5a8a782b7a1c941803fd517633cb699159b851c1d82267a9e9391b50
26− original:
27− hackage: dependent-sum-0.7.1.0
28−- completed:
29− hackage: constraints-extras-0.3.0.2@sha256:013b8d0392582c6ca068e226718a4fe8be8e22321cc0634f6115505bf377ad26,1853
30− pantry-tree:
31− size: 594
32− sha256: 3ce1012bfb02e4d7def9df19ce80b8cd2b472c691b25b181d9960638673fecd1
33− original:
34− hackage: constraints-extras-0.3.0.2
35−- completed:
36− hackage: dependent-map-0.4.0.0@sha256:ca2b131046f4340a1c35d138c5a003fe4a5be96b14efc26291ed35fd08c62221,1657
37− pantry-tree:
38− size: 551
39− sha256: 5defa30010904d2ad05a036f3eaf83793506717c93cbeb599f40db1a3632cfc5
40− original:
41− hackage: dependent-map-0.4.0.0
42−- completed:
43− hackage: servant-0.18.2@sha256:f8c9f0e9891a3ada1337a3c0b369333a3b5a2d0909dd3cd09d79bc26adeaca44,5298
44− pantry-tree:
45− size: 2662
46− sha256: e930e814de1aa4d24274bdf18341a50b7ed38604ae4734f730e09238ac5bf7e2
47− original:
48− hackage: servant-0.18.2
49−- completed:
50− hackage: servant-client-0.18.2@sha256:82578ade7468873259bb2fdc9d62290a0f998550900683e1410a237ed4b05410,4591
51− pantry-tree:
52− size: 1300
53− sha256: 6324892c77bedbce32f0d6f1612fc2cb0d82c163d3be39efb951d3cc3792ce4a
54− original:
55− hackage: servant-client-0.18.2
56−- completed:
57− hackage: servant-client-core-0.18.2@sha256:ad63ae0f227373fea7e547d4c2a7b0b69e112ff409a83cbadffc9f6ee049926f,3763
58− pantry-tree:
59− size: 1444
60− sha256: 9e37bc5f8cbb70cf1accb20bd6f83fca4c2ca42472cd0fc4b22183c2c57cbe3b
61− original:
62− hackage: servant-client-core-0.18.2
63−- completed:
64− hackage: servant-server-0.18.2@sha256:56679af62ab8820a2108da6153d9ae9dde37199e62172365bdaea1458c3f7c2d,5482
15+ hackage: some-1.0.3@sha256:938cec5fc0d3a08dde34cfc0acd5a0aa2e9fd489c9dbb64417f16588dfb47af0,2399
6516 pantry-tree:
66− size: 2614
67− sha256: 3ac7430134439e4b67f0f5333f63b89d0cb7de5e2e07f0af7801c8e223942b9c
17+ sha256: b88dddc6b991cd570c6750f8d309a9feb76938b29a690abdabc4ababb8c7b2f8
18+ size: 709
6819 original:
69− hackage: servant-server-0.18.2
20+ hackage: some-1.0.3
7021snapshots:
7122- completed:
72− size: 533252
73− url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/27.yaml
74− sha256: c2aaae52beeacf6a5727c1010f50e89d03869abfab6d2c2658ade9da8ed50c73
75− original: lts-16.27
23+ sha256: 3770dfd79f5aed67acdcc65c4e7730adddffe6dba79ea723cfb0918356fc0f94
24+ size: 648660
25+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/4.yaml
26+ original: lts-20.4
test/Breakdown.hs+1−1
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
test/Nested.hs+3−3
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE DeriveGeneric #-}
2+
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
@@ -11,7 +11,7 @@ import Servant
1111import Servant.API.Flatten
1212
1313type Api =
14− ( "one" :> Post '[JSON] Int
14+ ( "one" :> Summary "foo" :> Post '[JSON] Int
1515 :<|> "two" :> Post '[JSON] Int
1616 )
1717 :<|> ( "three" :> Post '[JSON] Int
test/Post.hs+2−2
@@ -1,8 +1,8 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5−{-# LANGUAGE OverloadedStrings #-}
4+
5+
66{-# LANGUAGE TypeOperators #-}
77
88module Post where
test/Product.hs+1−1
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
test/Put.hsnew file+33
@@ -0,0 +1,33 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE DerivingStrategies #-}
4+
5+
6+{-# LANGUAGE TypeOperators #-}
7+
8+module Put where
9+
10+import Data.Aeson
11+import Data.Hashable
12+import GHC.Generics (Generic)
13+import Servant
14+
15+type Api =
16+ Get '[JSON] Foo
17+ :<|> ReqBody '[JSON] Foo :> Put '[JSON] ()
18+ :<|> ReqBody '[JSON] Foo :> Put '[JSON] NoContent
19+
20+data Foo = Foo
21+ deriving (Eq, Show, Generic)
22+
23+instance Hashable Foo
24+
25+instance ToJSON Foo
26+
27+instance FromJSON Foo
28+
29+server :: Server Api
30+server =
31+ pure Foo
32+ :<|> const (pure ())
33+ :<|> const (pure NoContent)
test/QueryParams.hsnew file+15
@@ -0,0 +1,15 @@
1+{-# LANGUAGE DataKinds #-}
2+
3+{-# LANGUAGE DerivingStrategies #-}
4+
5+
6+{-# LANGUAGE TypeOperators #-}
7+
8+module QueryParams where
9+
10+import Servant
11+
12+type Api = QueryParams "ints" Int :> Get '[JSON] [Int]
13+
14+server :: Server Api
15+server = pure
test/Spec.hs+105−65
@@ -4,7 +4,13 @@
44{-# LANGUAGE RecordWildCards #-}
55{-# LANGUAGE StandaloneDeriving #-}
66{-# LANGUAGE TypeApplications #-}
7+{-# LANGUAGE RankNTypes #-}
8+{-# LANGUAGE FlexibleContexts #-}
9+{-# LANGUAGE AllowAmbiguousTypes #-}
10+{-# LANGUAGE ScopedTypeVariables #-}
11+{-# LANGUAGE DataKinds #-}
712{-# OPTIONS_GHC -fno-warn-orphans #-}
13+{-# LANGUAGE TypeFamilies #-}
814
915import qualified Breakdown
1016import Data.Dynamic (toDyn)
@@ -15,100 +21,134 @@ import qualified Foo
1521import qualified Headers
1622import qualified Nested
1723import qualified Post
24+import qualified Put
1825import qualified Product
19−import qualified Roboservant as RS
26+import qualified QueryParams
27+import qualified Roboservant as R
28+import qualified Roboservant.Server as RS
29+import qualified Roboservant.Client as RC
2030import qualified Seeded
2131import Test.Hspec
2232import Test.Hspec.Core.Spec (FailureReason (Reason), ResultStatus (Failure, Success), itemExample, mapSpecItem_, resultStatus)
2333import qualified Valid
34+import Servant ( Server, Proxy(..), serve, Endpoints, HasServer )
35+
36+import Servant.Client(ClientEnv, mkClientEnv, baseUrlPort, parseBaseUrl,HasClient,ClientM)
37+import Network.Wai(Application)
38+import qualified Network.Wai.Handler.Warp as Warp
39+import Network.HTTP.Client (newManager, defaultManagerSettings)
40+import Control.Monad((>=>))
2441
2542main :: IO ()
2643main = hspec spec
2744
45+fuzzBoth
46+ :: forall a .
47+ (R.ToReifiedApi (Endpoints a), HasServer a '[], RS.FlattenServer a, RC.ToReifiedClientApi (Endpoints a), RC.FlattenClient a,
48+ HasClient ClientM a)
49+ => String -> Server a -> R.Config -> (Maybe R.Report -> IO ()) -> Spec
50+fuzzBoth name server config condition = do
51+ it (name <> " via server") $
52+ RS.fuzz @a server config >>= condition
53+
54+ around (withServer (serve (Proxy :: Proxy a) server)) $
55+ it (name <> " via client") $ \(clientEnv::ClientEnv) -> do
56+ RC.fuzz @a clientEnv config >>= condition
57+
58+withServer :: Application -> ActionWith ClientEnv -> IO ()
59+withServer app action = Warp.testWithApplication (pure app) (genClientEnv >=> action)
60+ where genClientEnv port = do
61+ baseUrl <- parseBaseUrl "http://localhost"
62+ manager <- newManager defaultManagerSettings
63+ pure $ mkClientEnv manager (baseUrl { baseUrlPort = port })
64+
2865spec :: Spec
2966spec = do
3067 describe "Basic usage" $ do
3168 describe "noError" $ do
32− it "finds no error in a valid app" $ do
33− RS.fuzz @Valid.Api Valid.server RS.defaultConfig
34− >>= (`shouldSatisfy` isNothing)
35− it "finds no error in a valid generic app" $ do
36− RS.fuzz @Valid.RoutedApi Valid.routedServer RS.defaultConfig
37− >>= (`shouldSatisfy` isNothing)
38− it "does fail coverage check" $ do
39− r <- RS.fuzz @Valid.Api Valid.server RS.defaultConfig {RS.coverageThreshold = 0.6}
40− fmap (RS.failureReason . RS.rsException) r
41− `shouldSatisfy` ( \case
42− Just (RS.InsufficientCoverage _) -> True
43− _ -> False
44− )
45− describe "posted body" $ do
46− it "passes a coverage check using a posted body" $ do
47− RS.fuzz @Post.Api Post.server RS.defaultConfig {RS.coverageThreshold = 0.99}
48− >>= (`shouldSatisfy` isNothing)
69+ fuzzBoth @Valid.Api "find no error in a basic app" Valid.server R.defaultConfig (`shouldSatisfy` isNothing)
70+ fuzzBoth @Valid.RoutedApi "finds no error in a valid generic app" Valid.routedServer R.defaultConfig (`shouldSatisfy` isNothing)
71+ fuzzBoth @Valid.Api "fails coverage check" Valid.server R.defaultConfig {R.coverageThreshold = 0.6}
72+ (\r ->
73+ fmap (R.failureReason . R.rsException) r
74+ `shouldSatisfy` ( \case
75+ Just (R.InsufficientCoverage _) -> True
76+ _ -> False
77+ ))
78+ describe "posted body" $
79+ fuzzBoth @Post.Api "passes a coverage check using a posted body" Post.server R.defaultConfig {R.coverageThreshold = 0.99}
80+ (`shouldSatisfy` isNothing)
81+
82+
83+ describe "PUTted body" $
84+ fuzzBoth @Put.Api "passes a coverage check using a posted body" Put.server R.defaultConfig {R.coverageThreshold = 0.99}
85+ (`shouldSatisfy` isNothing)
86+
87+
4988 describe "seeded" $ do
50− shouldFail
51− $ it "finds an error using information passed in"
52− $ let res = Seeded.Seed 1
53− in RS.fuzz @Seeded.Api Seeded.server (RS.defaultConfig {RS.seed = [(toDyn res, hash res)]})
54− >>= (`shouldSatisfy` isNothing)
55− describe "Foo" $ do
56− it "finds an error in a basic app" $
57− RS.fuzz @Foo.Api Foo.server RS.defaultConfig
58− >>= (`shouldSatisfy` serverFailure)
89+ let res = Seeded.Seed 1
90+ shouldFail $ fuzzBoth @Seeded.Api "finds an error using information passed in" Seeded.server
91+ (R.defaultConfig {R.seed = [(toDyn res, hash res)]})
92+ (`shouldSatisfy` isNothing)
93+
94+ describe "Foo" $
95+ fuzzBoth @Foo.Api "finds an error in a basic app" Foo.server R.defaultConfig (`shouldSatisfy` serverFailure)
96+
97+ describe "QueryParams" $
98+ fuzzBoth @QueryParams.Api "can handle query params" QueryParams.server R.defaultConfig { R.seed = [R.hashedDyn (12::Int)] }
99+ (`shouldSatisfy` isNothing)
100+
59101 describe "BuildFrom" $ do
60− describe "headers (and sum types)" $ do
61− it "should find a failure that's dependent on using header info" $ do
62− RS.fuzz @Headers.Api Headers.server RS.defaultConfig
63− >>= (`shouldSatisfy` serverFailure)
64− describe "product types" $ do
65− it "should find a failure that's dependent on creating a product" $ do
66− RS.fuzz @Product.Api Product.server RS.defaultConfig {RS.seed = [RS.hashedDyn 'a', RS.hashedDyn (1 :: Int)]}
67− >>= (`shouldSatisfy` serverFailure)
102+ describe "headers (and sum types)" $
103+ fuzzBoth @Headers.Api "should find a failure that's dependent on using header info" Headers.server R.defaultConfig
104+ (`shouldSatisfy` serverFailure)
105+ describe "product types" $
106+ fuzzBoth @Product.Api "should find a failure that's dependent on creating a product" Product.server
107+ R.defaultConfig {R.seed = [R.hashedDyn 'a', R.hashedDyn (1 :: Int)]}
108+ (`shouldSatisfy` serverFailure)
68109 describe "Breakdown" $ do
69− it "handles products" $ do
70− RS.fuzz @Breakdown.ProductApi Breakdown.productServer RS.defaultConfig
71− >>= (`shouldSatisfy` serverFailure)
72− it "handles sums" $ do
73− RS.fuzz @Breakdown.SumApi Breakdown.sumServer RS.defaultConfig
74− >>= (`shouldSatisfy` serverFailure)
75− describe "flattening" $ do
76− -- we don't actually do much here, this is just here to document the appropriate response
77− -- if you get a type error with a nested api.
78− it "can handle nested apis" $ do
79− RS.fuzz @(Nested.FlatApi) Nested.server RS.defaultConfig {RS.coverageThreshold = 0.99}
80− >>= (`shouldSatisfy` isNothing)
81−
82−serverFailure :: Maybe RS.Report -> Bool
110+ fuzzBoth @Breakdown.ProductApi "handles products" Breakdown.productServer R.defaultConfig
111+ (`shouldSatisfy` serverFailure)
112+ fuzzBoth @Breakdown.SumApi "handles sums" Breakdown.sumServer R.defaultConfig
113+ (`shouldSatisfy` serverFailure)
114+ describe "flattening" $
115+ fuzzBoth @Nested.FlatApi "can handle nested apis" Nested.server R.defaultConfig {R.coverageThreshold = 0.99}
116+ (`shouldSatisfy` isNothing)
117+
118+serverFailure :: Maybe R.Report -> Bool
83119serverFailure = \case
84− Just RS.Report {..} ->
85− let RS.RoboservantException {..} = rsException
86− in failureReason /= RS.NoPossibleMoves
120+ Just R.Report {..} ->
121+ let R.RoboservantException {..} = rsException
122+ in failureReason /= R.NoPossibleMoves
87123 _ -> False
88124
89−deriving via (RS.Atom Foo.Foo) instance RS.Breakdown Foo.Foo
125+deriving via (R.Atom Foo.Foo) instance R.Breakdown Foo.Foo
126+
127+deriving via (R.Atom Foo.Foo) instance R.BuildFrom Foo.Foo
128+
129+deriving via (R.Atom Headers.Foo) instance R.Breakdown Headers.Foo
90130
91−deriving via (RS.Atom Foo.Foo) instance RS.BuildFrom Foo.Foo
131+deriving via (R.Atom Headers.Foo) instance R.BuildFrom Headers.Foo
92132
93−deriving via (RS.Atom Headers.Foo) instance RS.Breakdown Headers.Foo
133+deriving via (R.Atom Seeded.Seed) instance R.Breakdown Seeded.Seed
94134
95−deriving via (RS.Atom Headers.Foo) instance RS.BuildFrom Headers.Foo
135+deriving via (R.Atom Seeded.Seed) instance R.BuildFrom Seeded.Seed
96136
97−deriving via (RS.Atom Seeded.Seed) instance RS.Breakdown Seeded.Seed
137+deriving via (R.Atom Void) instance R.BuildFrom Void
98138
99−deriving via (RS.Atom Seeded.Seed) instance RS.BuildFrom Seeded.Seed
139+deriving via (R.Atom Post.FooPost) instance R.Breakdown Post.FooPost
140+deriving via (R.Atom Post.FooPost) instance R.BuildFrom Post.FooPost
100141
101−deriving via (RS.Atom Void) instance RS.BuildFrom Void
142+deriving via (R.Atom Put.Foo) instance R.Breakdown Put.Foo
143+deriving via (R.Atom Put.Foo) instance R.BuildFrom Put.Foo
102144
103−deriving via (RS.Atom Post.FooPost) instance RS.Breakdown Post.FooPost
104145
105−deriving via (RS.Atom Post.FooPost) instance RS.BuildFrom Post.FooPost
106146
107−deriving via (RS.Compound Breakdown.Foo) instance RS.Breakdown Breakdown.Foo
147+deriving via (R.Compound Breakdown.Foo) instance R.Breakdown Breakdown.Foo
108148
109−deriving via (RS.Compound Product.Foo) instance RS.BuildFrom Product.Foo
149+deriving via (R.Compound Product.Foo) instance R.BuildFrom Product.Foo
110150
111−deriving via (RS.Compound Breakdown.SomeSum) instance RS.Breakdown Breakdown.SomeSum
151+deriving via (R.Compound Breakdown.SomeSum) instance R.Breakdown Breakdown.SomeSum
112152
113153-- | `shouldFail` allows you to assert that a given `Spec` should contain at least one failing test.
114154-- this is often useful when testing tests.
@@ -118,7 +158,7 @@ shouldFail =
118158 ( \i ->
119159 i
120160 { itemExample = \p a cb -> do
121− r <- (itemExample i) p a cb
161+ r <- itemExample i p a cb
122162 pure
123163 r
124164 { resultStatus = case resultStatus r of
test/UnsafeIO.hs+3−3
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE DeriveGeneric #-}
2+
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
@@ -22,7 +22,7 @@ healthcheck ref = do
2222 t <- liftIO $ readIORef ref
2323 case t of
2424 0 -> pure ()
25− n -> throwError $ err500 {errBody = "observed inconsistency: " <> (BL8.pack $ show n)}
25+ n -> throwError $ err500 {errBody = "observed inconsistency: " <> BL8.pack (show n)}
2626
2727makeServer :: IO (Server UnsafeApi)
2828makeServer = do
test/Valid.hs+2−3
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
@@ -10,7 +10,6 @@ module Valid where
1010import Data.Void
1111import GHC.Generics
1212import Servant
13−import Servant.API.Generic
1413import Servant.Server.Generic
1514
1615type Api =
@@ -21,7 +20,7 @@ data Routes route
2120 = Routes
2221 { getInt ::
2322 route
24− :- Get '[JSON] Int,
23+ :- Summary "i'm a summary!" :> Get '[JSON] Int,
2524 captureIt ::
2625 route
2726 :- Capture "void" Void :> Get '[JSON] ()