on 8942fc5 help sign in
32 files changed+341−874
since splitsnapshots
unifiedsplit
.github/workflows/cabal.yml−64 .github/workflows/ci.yml+2−2 EXAMPLE.md+24−73 Makefile+1−1 README.md+1−90 benchmarks/genFile.sh+2−15 benchmarks/prelude+16−7 benchmarks/runTiming.sh+2−2 package.yaml+3−12 roboservant.cabal+7−24 src/Roboservant/Client.hs−127 src/Roboservant/Direct.hs+31−33 src/Roboservant/Server.hs−36 src/Roboservant/Types.hs+2−3 src/Roboservant/Types/Breakdown.hs+2−15 src/Roboservant/Types/BuildFrom.hs+5−12 src/Roboservant/Types/FlattenServer.hs+44 src/Roboservant/Types/Internal.hs+2−2 src/Roboservant/Types/Orphans.hs−7 src/Roboservant/Types/ReifiedApi.hs+49−56 src/Roboservant/Types/ReifiedApi/Server.hs−114 stack.yaml+12−5 stack.yaml.lock+58−9 test/Breakdown.hs+1−1 test/Nested.hs+3−3 test/Post.hs+2−2 test/Product.hs+1−1 test/Put.hs−33 test/QueryParams.hs−15 test/Spec.hs+65−105 test/UnsafeIO.hs+3−3 test/Valid.hs+3−2
.github/workflows/cabal.ymldeleted−64
@@ -1,64 +0,0 @@
1−# modified from https://github.com/jgm/pandoc/blob/master/.github/workflows/ci.yml
2−name: Cabal CI
3−
4−on:
5− push:
6− branches:
7− - '**'
8− paths-ignore: []
9− pull_request:
10− paths-ignore: []
11−
12−jobs:
13− linux:
14−
15− runs-on: ubuntu-20.04
16− strategy:
17− fail-fast: false
18− matrix:
19− versions:
20− - ghc: '8.8.4'
21− cabal: '3.4'
22− - ghc: '8.10.4'
23− cabal: '3.4'
24− steps:
25− - uses: actions/checkout@v2
26−
27− # need to install older cabal/ghc versions from ppa repository
28−
29− - name: Install recent cabal/ghc
30− uses: actions/setup-haskell@v1.1.3
31− with:
32− ghc-version: ${{ matrix.versions.ghc }}
33− cabal-version: ${{ matrix.versions.cabal }}
34−
35− # declare/restore cached things
36− # caching doesn't work for scheduled runs yet
37− # https://github.com/actions/cache/issues/63
38−
39− - name: Cache cabal global package db
40− id: cabal-global
41− uses: actions/cache@v2
42− with:
43− path: |
44− ~/.cabal
45− key: ${{ runner.os }}-${{ matrix.versions.ghc }}-${{ matrix.versions.cabal }}-cabal-global-${{ hashFiles('cabal.project') }}
46−
47− - name: Cache cabal work
48− id: cabal-local
49− uses: actions/cache@v2
50− with:
51− path: |
52− dist-newstyle
53− key: ${{ runner.os }}-${{ matrix.versions.ghc }}-${{ matrix.versions.cabal }}-cabal-local
54−
55− - name: Install dependencies
56− run: |
57− cabal update
58− cabal build all --dependencies-only --enable-tests --disable-optimization
59− - name: Build
60− run: |
61− cabal build all --enable-tests --disable-optimization 2>&1 | tee build.log
62− - name: Test
63− run: |
64− cabal test all --disable-optimization
.github/workflows/ci.yml+2−2
@@ -1,4 +1,4 @@
1−name: Stack CI
1+name: CI
22
33# Trigger the workflow on push or pull request, but only for the master branch
44on:
@@ -12,7 +12,7 @@ jobs:
1212 strategy:
1313 matrix:
1414 stack: ["2.3.3"]
15− ghc: ["8.10.4"]
15+ ghc: ["8.8.3"]
1616 os: [ubuntu-latest]
1717 steps:
1818 - uses: actions/checkout@v2
EXAMPLE.md+24−73
@@ -2,7 +2,7 @@
22
33Our api under test:
44
5−``` haskell
5+```haskell
66-- Obligatory fancy-types pragma tax
77{-# LANGUAGE DataKinds #-}
88{-# LANGUAGE TypeOperators #-}
@@ -13,43 +13,30 @@ Our api under test:
1313{-# LANGUAGE DeriveGeneric #-}
1414{-# LANGUAGE GeneralizedNewtypeDeriving #-}
1515{-# LANGUAGE RecordWildCards #-}
16−{-# LANGUAGE ScopedTypeVariables #-}
17−{-# LANGUAGE TypeApplications #-}
1816
19−import qualified Roboservant.Server as RS
20−import qualified Roboservant.Client as RC
21−import Servant.Client(ClientEnv, baseUrlPort, parseBaseUrl, mkClientEnv)
22−import Network.HTTP.Client (newManager, defaultManagerSettings)
23−import Roboservant.Types
17+import Roboservant
2418import Test.Hspec
2519import Servant
2620import GHC.Generics
2721import Data.Typeable
2822import Data.Hashable
29−import Data.Maybe(isNothing, isJust)
30−import qualified Network.Wai.Handler.Warp as Warp
31−import Data.Aeson(FromJSON,ToJSON)
3223
3324newtype A = A Int
3425 deriving (Generic, Eq, Show, Typeable)
3526 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
3627
37−instance FromJSON A
38−instance ToJSON A
39−
4028newtype B = B Int
4129 deriving (Generic, Eq, Show, Typeable)
4230 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
4331
44−instance FromJSON B
45−instance ToJSON B
46−
4732type Api =
4833 "item" :> Get '[JSON] A
4934 :<|> "itemAdd" :> Capture "one" B :> Capture "two" B :> Get '[JSON] B
5035 :<|> "item" :> Capture "itemId" B :> Get '[JSON] ()
5136
52−server :: Handler A -> Server Api
37+goodServer = server (pure $ A 0)
38+badServer = server (pure $ A 1)
39+
5340server introduce = introduce :<|> combine :<|> eliminate
5441 where
5542 combine (B i) (B j) = pure $ B (i + j)
@@ -58,66 +45,25 @@ server introduce = introduce :<|> combine :<|> eliminate
5845 | otherwise = pure ()
5946```
6047
61−We have a "good" server, that never generates anything other than a 0. This means repeated application of
62−the combination/addition rule can never bring us to the dangerous state of numbers larger than 10.
63−
64−``` haskell
65−goodServer, badServer :: Server Api
66−goodServer = server (pure $ A 0)
67−badServer = server (pure $ A 1)
68−```
69−
7048In the test file, we first define the tests: the faulty server should fail and the good server should pass.
7149
7250```haskell
73−main :: IO ()
74−main = hspec spec
75−
76−spec :: Spec
7751spec = describe "example" $ do
7852 it "good server should not fail" $ do
79− RS.fuzz @Api goodServer config
53+ fuzz @Api goodServer defaultConfig { coverageThreshold = 0.99 }
8054 >>= (`shouldSatisfy` isNothing)
8155 it "bad server should fail" $ do
82− RS.fuzz @Api badServer config
83− >>= (`shouldSatisfy` isJust)
56+ fuzz @Api badServer defaultConfig { coverageThreshold = 0.99 }
57+ >>= (`shouldSatisfy` serverFailure)
8458```
8559
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
60+And unless we want to ship roboservant and all its dependencies to production, we also need
11161some orphan instances: because As are the only value we can get without
11262an input, we need to be able to break them down.
11363
114−``` haskell
64+```haskell
11565deriving via (Compound A) instance Breakdown A
116−```
117−
118−if we wanted to assemble As from parts as well, we'd derive using Compound, but in this case we don't care.
119−
120−``` haskell
66+-- if we wanted to assemble As from parts as well, we'd derive using Compound
12167deriving via (Atom A) instance BuildFrom A
12268
12369```
@@ -130,14 +76,19 @@ deriving via (Compound B) instance BuildFrom B
13076deriving via (Atom B) instance Breakdown B
13177```
13278
79+finally some uninteresting utilities and the entrypoint
80+
81+```haskell
82+main = hspec spec
13383
134−test utilities:
84+serverFailure :: Maybe Report -> Bool
85+serverFailure c = case c of
86+ Just Report{..} ->
87+ let RoboservantException{..} = rsException
88+ in failureReason /= NoPossibleMoves
89+ _ -> False
13590
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 })
91+isNothing x = case x of
92+ Nothing -> True
93+ _ -> False
14394```
Makefile+1−1
@@ -1,4 +1,4 @@
11testwatch:
2− ghcid -T main -c 'stack repl roboservant:lib roboservant:test:roboservant-test --test --ghc-options=-fobject-code' --allow-eval --restart="stack.yaml" --restart="package.yaml" -W
2+ ghcid -T main -c 'stack repl --test --ghc-options=-fobject-code' --allow-eval --restart="stack.yaml" --restart="package.yaml" -W
33pedanticwatch:
44 ghcid -c 'stack repl --test --ghc-options=-fobject-code' --allow-eval --restart="stack.yaml" --restart="package.yaml" -W
README.md+1−90
@@ -2,8 +2,7 @@
22
33Automatically fuzz your servant apis in a contextually-aware way.
44
5−[![Stack CI](https://github.com/mwotton/roboservant/actions/workflows/ci.yml/badge.svg)](https://github.com/mwotton/roboservant/actions/workflows/ci.yml)
6−[![Cabal CI](https://github.com/mwotton/roboservant/actions/workflows/cabal.yml/badge.svg)](https://github.com/mwotton/roboservant/actions/workflows/cabal.yml)
5+![CI](https://github.com/mwotton/roboservant/workflows/CI/badge.svg)
76
87## example
98
@@ -15,71 +14,6 @@ Servant gives us a lot of information about what a server can do. We
1514use this information to generate arbitrarily long request/response
1615sessions and verify properties that should hold over them.
1716
18−## how?
19−
20−In essence, ```fuzz @Api yourServer config``` will make a bunch of
21−calls to your API, and record the results in a type-indexed
22−dictionary. This means that they are now available for the
23−prerequisites of other calls, so as you proceed, more and more api
24−calls become possible.
25−
26−We explicitly do not try to come up with plausible values that haven't
27−somehow come back from the API. That's straying into QC/Hedgehog
28−territory: if you want that, come up with the values on that side, and
29−set them as seeds in the configuration.
30−
31−### what does it mean to be "available"?
32−
33−In a simple API, you may make a call and get back a `Foo`, which will
34−allow you to make another call that requires a `Foo`. In a more
35−complicated app, it's likely that you'll send a request body that
36−includes many subcomponents, and it's likely you'll get a response
37−that needs to be broken down into pieces before it's useful.
38−
39−To cope with this, we have the typeclasses `BuildFrom` and
40−`Breakdown`. You can write instances for them if you feel like it, and
41−indeed it's currently required for recursive datatypes if you don't
42−want the fuzzer to hang, but for the majority of your types it should
43−be sufficient to derive them generically. (Sensible instances are
44−provided for lists.)
45−
46−There are two basic strategies here. In some cases, you want to regard
47−a type as indivisible: that's why we like newtypes, right? In this
48−case, we can derive using the `Atom` strategy.
49−
50−``` haskell
51−deriving via (Atom NewtypedKey) instance Breakdown NewtypedKey
52−deriving via (Atom NewtypedKey) instance BuildFrom NewtypedKey
53−```
54−
55−This is saying "A can neither be built from components or broken down
56−for spare parts. Hands off!". This is a good strategy for key types,
57−for instance.
58−
59−If instead it's a big complicated thing with lots of juicy
60−subcomponents, we want to rip it apart using Generics and feast on
61−its succulent headmeats:
62−
63−``` haskell
64−deriving via (Compound Payload) instance Breakdown Payload
65−deriving via (Compound Payload) instance BuildFrom Payload
66−```
67−
68−### priming the pump
69−
70−Sometimes there are values we'd like to smuggle into the API that are
71−not derivable from within the API itself: sometimes this is a warning
72−sign that your API is incomplete, but it can be quite reasonable to
73−require identifying credentials within an API and not provide a way to
74−get them. It might also be reasonable to have some sample values that
75−the user is expected to come up with.
76−
77−For those cases, override the `seed` in the `Config` with a
78−list of seed values, suitably hashed:
79−
80−``` haskell
81−defaultConfig { seed = [hashedDyn creds, hashedDyn userJwt]}
82−```
8317
8418## why not servant-quickcheck?
8519
@@ -89,26 +23,3 @@ there's a lot of the state space you just can't explore without context: modern
8923full of pointer-like structures, whether they're URLs or database
9024keys/uuids, and servant-quickcheck requires that you be able to generate
9125these without context via Arbitrary.
92−
93−## limitations and future work
94−
95−Currently, the display of failing traces is pretty tragic, both in the
96−formatting and in its non-minimality. This is pretty ticklish:
97−arguably the right way to do this is to return a trace that we can
98−also rerun, and let quickcheck or hedgehog a level up shrink it until
99−it's satisfactorily short. In the interest of being useful earlier
100−rather than later, I'm releasing v1.0 before I crack this particular
101−nut. We do know which calls we made that led to the failing case, so
102−we would want to show that distinction in a visible way: it's possible
103−that other calls that don't have direct data dependencies were
104−important, but we definitely know we need the direct data dependencies.
105−
106−The provenance stuff is a bit underbaked. It should at least pull a
107−representation of the route chosen rather than just an integer index.
108−
109−It would also be nice to have a robust strategy for deriving recursive
110−datatypes, or at least rejecting attempts to generate them that don't
111−end in an infinite loop.
112−
113−Currently the `FlattenServer` instance for `:>` is quadratic. It would
114−be nice to fix this but I lack the art.
benchmarks/genFile.sh+2−15
@@ -1,11 +1,9 @@
11n=$1
2−k=$2
32
43cat prelude
5−echo -n "type SubApi = \"route1\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
4+echo -n "type Api = \"route1\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
65for i in $(seq 2 $n); do
7− echo -n " :<|> \"route${i}\" :> "
8− echo "ReqBody '[JSON] Input :> Post '[JSON] Output"
6+ echo " :<|> \"route${i}\" :> ReqBody '[JSON] Input :> Post '[JSON] Output"
97done
108
119echo -n "server = handler "
@@ -13,14 +11,3 @@ echo -n "server = handler "
1311for i in $(seq 2 $n); do
1412 echo -n ":<|> handler "
1513done
16−echo
17−echo
18−
19−echo -n "type Api = Flatten ("
20−for i in $(seq 1 $k); do
21− echo -n '"foo" :> '
22−done;
23−
24−echo "SubApi )"
25−
26−
benchmarks/prelude+16−7
@@ -12,7 +12,6 @@ import Servant
1212import Data.Hashable
1313import GHC.Generics
1414import Data.Aeson
15−import Servant.API.Flatten
1615
1716data Input = Input1 { foo :: String }
1817 | Input2 { bar :: Int }
@@ -53,9 +52,19 @@ handler = pure . \case
5352 Input1 foo -> Output1 foo
5453 Input2 bar -> Output2 bar
5554
56−main =
57− -- fuzz @Api server defaultConfig
58− pure ()
59−
60−f = flatten (Proxy @Api)
61−
55+main = fuzz @Api server defaultConfig (pure ())
56+
57+defaultConfig :: Config
58+defaultConfig = Config {
59+ -- you can pass extra values in using the seed argument. This can be useful
60+ -- for things that might not be produceable within the api, like auth tokens.
61+ seed = [hashedDyn "blah"]
62+ , maxRuntime = 0.5
63+ -- if we get to 1000 interactions with the api, call it quits.
64+ , maxReps = 1000
65+ -- if you're using this inside quickcheck or hedgehog, you might want to set this
66+ -- from their seed to make sure it stays deterministic
67+ , rngSeed = 0
68+ -- 0 to 100: fail tests if we hit less than this percentage of endpoints.
69+ , coverageThreshold = 0
70+ }
benchmarks/runTiming.sh+2−2
@@ -1,8 +1,8 @@
11lo=$1
22hi=$2
3−k=$3
3+
44for i in $(seq $lo 5 $hi); do
5− ./genFile.sh $i $k > main.hs;
5+ ./genFile.sh $i > main.hs;
66 echo "sievedn: $i,";
77 /usr/bin/time -f "%e" stack ghc main.hs 2>&1;
88# /usr/bin/time -f "%e" stack ghc -- -fomit-interface-pragmas main.hs 2>&1;
package.yaml+3−12
@@ -1,6 +1,6 @@
11
22name: roboservant
3−version: 0.1.0.3
3+version: 0.1.0.2
44github: "mwotton/roboservant"
55license: BSD3
66copyright: "2020 Mark Wotton, Samuel Schlesinger"
@@ -19,7 +19,6 @@ dependencies:
1919- containers
2020- random
2121- hashable
22−- http-types
2322- lifted-base
2423- monad-control
2524- mtl
@@ -56,10 +55,7 @@ tests:
5655 - hspec
5756 - hspec-core
5857 - http-api-data
59− - http-client
60− - hspec-wai
61− - wai
62− - warp
58+
6359 example:
6460 main: Example.lhs
6561 source-dirs: .
@@ -67,10 +63,5 @@ tests:
6763 dependencies:
6864 - roboservant
6965 - hspec
70− - warp
71− - http-client
72− - aeson
73− - hspec-core
66+ # build-depends: base, markdown-unlit
7467 ghc-options: -pgmL markdown-unlit
75−
76− build-tools: markdown-unlit
roboservant.cabal+7−24
@@ -1,13 +1,13 @@
11cabal-version: 1.12
22
3−-- This file has been generated from package.yaml by hpack version 0.35.0.
3+-- This file has been generated from package.yaml by hpack version 0.33.0.
44--
55-- see: https://github.com/sol/hpack
66--
7−-- hash: 0c0c35e1dca1d6c52e5bc61591cbd8c0da9d811b83341bbc676c5f8b93b0b20d
7+-- hash: 3eb8db6b6899ba904be853bb6865aba4f208077587e89f5377df3169743034b0
88
99name: roboservant
10−version: 0.1.0.3
10+version: 0.1.0.2
1111synopsis: Automatic session-aware servant testing
1212description: Please see the README on GitHub at <https://github.com/mwotton/roboservant#readme>
1313category: Web
@@ -28,17 +28,15 @@ source-repository head
2828library
2929 exposed-modules:
3030 Roboservant
31− Roboservant.Client
3231 Roboservant.Direct
33− Roboservant.Server
3432 Roboservant.Types
3533 Roboservant.Types.Breakdown
3634 Roboservant.Types.BuildFrom
3735 Roboservant.Types.Config
36+ Roboservant.Types.Config.Internal
37+ Roboservant.Types.FlattenServer
3838 Roboservant.Types.Internal
39− Roboservant.Types.Orphans
4039 Roboservant.Types.ReifiedApi
41− Roboservant.Types.ReifiedApi.Server
4240 other-modules:
4341 Paths_roboservant
4442 hs-source-dirs:
@@ -51,7 +49,6 @@ library
5149 , dependent-map
5250 , dependent-sum
5351 , hashable
54− , http-types
5552 , lifted-base
5653 , monad-control
5754 , mtl
@@ -73,22 +70,16 @@ test-suite example
7370 other-modules:
7471 Paths_roboservant
7572 hs-source-dirs:
76− ./
73+ ./.
7774 ghc-options: -Wall -fwrite-ide-info -hiedir=.hie -pgmL markdown-unlit
78− build-tool-depends:
79− markdown-unlit:markdown-unlit
8075 build-depends:
81− aeson
82− , base >=4.7 && <5
76+ base >=4.7 && <5
8377 , bytestring
8478 , containers
8579 , dependent-map
8680 , dependent-sum
8781 , hashable
8882 , hspec
89− , hspec-core
90− , http-client
91− , http-types
9283 , lifted-base
9384 , monad-control
9485 , mtl
@@ -103,7 +94,6 @@ test-suite example
10394 , time
10495 , unordered-containers
10596 , vinyl
106− , warp
10797 default-language: Haskell2010
10898
10999test-suite roboservant-test
@@ -116,8 +106,6 @@ test-suite roboservant-test
116106 Nested
117107 Post
118108 Product
119− Put
120− QueryParams
121109 Seeded
122110 UnsafeIO
123111 Valid
@@ -135,10 +123,7 @@ test-suite roboservant-test
135123 , hashable
136124 , hspec
137125 , hspec-core
138− , hspec-wai
139126 , http-api-data
140− , http-client
141− , http-types
142127 , lifted-base
143128 , monad-control
144129 , mtl
@@ -153,6 +138,4 @@ test-suite roboservant-test
153138 , time
154139 , unordered-containers
155140 , vinyl
156− , wai
157− , warp
158141 default-language: Haskell2010
src/Roboservant/Client.hsdeleted−127
@@ -1,127 +0,0 @@
1−{-# LANGUAGE RankNTypes #-}
2−{-# LANGUAGE TypeOperators #-}
3−{-# LANGUAGE NamedFieldPuns #-}
4−{-# LANGUAGE DataKinds #-}
5−{-# LANGUAGE AllowAmbiguousTypes #-}
6−{-# LANGUAGE DataKinds #-}
7−{-# LANGUAGE GADTs #-}
8−{-# LANGUAGE PolyKinds #-}
9−{-# LANGUAGE ScopedTypeVariables #-}
10−{-# LANGUAGE TypeApplications #-}
11−
12−{-# LANGUAGE UndecidableInstances #-}
13−{-# LANGUAGE TypeFamilies #-}
14−{-# LANGUAGE FlexibleContexts #-}
15−-- should all the NormalizeFunction instances be in one place?
16−{-# OPTIONS_GHC -fno-warn-orphans #-}
17−module Roboservant.Client where
18−
19−import Data.Proxy
20−import Servant.Client
21−import Roboservant.Types
22−import Roboservant(Report, fuzz')
23−import Servant
24−import Data.Bifunctor
25−import Data.List.NonEmpty (NonEmpty)
26−import Data.Dynamic (Dynamic,Typeable)
27−import qualified Data.Vinyl.Curry as V
28−import qualified Data.Text as T
29−import Control.Monad.Reader
30−import Data.Hashable
31−import Network.HTTP.Types.Status
32−
33−-- fuzz :: forall api.
34−-- (FlattenServer api, ToReifiedApi (Endpoints api)) =>
35−-- Server api ->
36−-- Config ->
37−-- IO (Maybe Report)
38−-- fuzz s = fuzz' (reifyServer s)
39−-- -- todo: how do we pull reifyServer out?
40−-- where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api))
41−-- => Server api -> ReifiedApi
42−-- reifyServer server = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
43−
44−fuzz :: forall api . (ToReifiedClientApi (Endpoints api), FlattenClient api, HasClient ClientM api)
45− => ClientEnv -> Config -> IO (Maybe Report)
46−fuzz clientEnv
47− = fuzz'
48− (toReifiedClientApi
49− (flattenClient @api apiClient) (Proxy @(Endpoints api)) clientEnv)
50− where apiClient = client (Proxy @api)
51−
52−
53−
54−class ToReifiedClientApi api where
55− toReifiedClientApi :: ClientBundled api -> Proxy api -> ClientEnv -> ReifiedApi
56−
57−data ClientBundled endpoints where
58− AClientEndpoint :: Client ClientM endpoint -> ClientBundled endpoints -> ClientBundled (endpoint ': endpoints)
59− NoClientEndpoints :: ClientBundled '[]
60−
61−
62−class FlattenClient api where
63− flattenClient :: Client ClientM api -> ClientBundled (Endpoints api)
64−
65−instance
66− ( NormalizeFunction (Client ClientM endpoint)
67− , Normal (Client ClientM endpoint) ~ V.Curried (EndpointArgs endpoint) (ReaderT ClientEnv IO (Either InteractionError (NonEmpty (Dynamic,Int))))
68− , ToReifiedClientApi endpoints
69− , V.RecordCurry' (EndpointArgs endpoint)
70− , ToReifiedEndpoint endpoint) =>
71− ToReifiedClientApi (endpoint : endpoints) where
72− toReifiedClientApi (endpoint `AClientEndpoint` endpoints) _ clientEnv =
73− (0, ReifiedEndpoint
74− { reArguments = reifiedEndpointArguments @endpoint
75− , reEndpointFunc = foo (normalize endpoint)
76− }
77− )
78− : (map . first) (+1)
79− (toReifiedClientApi endpoints (Proxy @endpoints) clientEnv)
80− where
81−
82− foo :: V.Curried (EndpointArgs endpoint) (ReaderT ClientEnv IO ResultType)
83− -> V.Curried (EndpointArgs endpoint) (IO ResultType)
84− foo = mapCurried @(EndpointArgs endpoint) @(ReaderT ClientEnv IO ResultType) (`runReaderT` clientEnv)
85−
86−mapCurried :: forall ts a b. V.RecordCurry' ts => (a -> b) -> V.Curried ts a -> V.Curried ts b
87−mapCurried f g = V.rcurry' @ts $ f . V.runcurry' g
88−
89−type ResultType = Either InteractionError (NonEmpty (Dynamic,Int))
90−-- runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
91−
92−
93−instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (ClientM x) where
94− type Normal (ClientM x) = ReaderT ClientEnv IO (Either InteractionError (NonEmpty (Dynamic,Int)))
95− normalize c = ReaderT $
96− fmap (bimap renderClientError breakdown) . runClientM c
97− where
98− renderClientError :: ClientError -> InteractionError
99− renderClientError err = case err of
100− FailureResponse _ Response{responseStatusCode} -> InteractionError textual (responseStatusCode == status500)
101− _ -> InteractionError textual True
102−
103− where textual = T.pack $ show err
104−instance ToReifiedClientApi '[] where
105− toReifiedClientApi NoClientEndpoints _ _ = []
106−
107−
108−instance
109− ( FlattenClient api,
110− Endpoints endpoint ~ '[endpoint]
111− ) =>
112− FlattenClient (endpoint :<|> api)
113− where
114− flattenClient (endpoint :<|> c) = endpoint `AClientEndpoint` flattenClient @api c
115−
116−instance
117− (
118− Endpoints api ~ '[api]
119− ) =>
120− FlattenClient (x :> api)
121− where
122− flattenClient c = c `AClientEndpoint` NoClientEndpoints
123−
124−
125−instance FlattenClient (Verb method statusCode contentTypes responseType)
126− where
127− flattenClient c = c `AClientEndpoint` NoClientEndpoints
src/Roboservant/Direct.hs+31−33
@@ -7,12 +7,12 @@
77{-# LANGUAGE RecordWildCards #-}
88{-# LANGUAGE ScopedTypeVariables #-}
99{-# LANGUAGE TupleSections #-}
10−
10+{-# LANGUAGE TypeApplications #-}
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− InteractionError(..),
59+ FlattenServer (..),
6060 Provenance (..),
61− ReifiedApi,
6261 ReifiedEndpoint (..),
6362 Stash (..),
6463 StashValue (..),
64+ ToReifiedApi (..),
6565 TypedF,
6666 )
6767import Roboservant.Types.Config
68−
68+import Servant (Endpoints, Proxy (Proxy), Server, ServerError (..))
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 InteractionError (NonEmpty (Dynamic, Int)))),
108+ { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))),
109109 eoArgs :: V.Rec (TypedF StashValue) as
110110 }
111111
@@ -121,18 +121,13 @@ data Report
121121 }
122122 deriving (Show)
123123
124−
125−
126−-- fuzzClient :: Client api -> Config -> IO (Maybe Report)
127−-- fuzzClient = undefined
128−
129−
130−
131−fuzz' ::
132− ReifiedApi ->
124+fuzz ::
125+ forall api.
126+ (FlattenServer api, ToReifiedApi (Endpoints api)) =>
127+ Server api ->
133128 Config ->
134129 IO (Maybe Report)
135−fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
130+fuzz server Config {..} = handle (pure . Just . formatException) $ do
136131 let path = []
137132 stash = addToStash seed mempty
138133 currentRng = mkStdGen rngSeed
@@ -150,12 +145,12 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
150145 Report
151146 (unlines [show failureType, show exception])
152147 r
153− displayDiagnostics FuzzState {..} = liftIO $
148+ displayDiagnostics FuzzState {..} = liftIO $ do
154149 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)
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)
159154 -- <> (map (show . NEL.length . getStashValue ) $ DM.assocs (getStash stash))
160155 -- $ \_k v ->
161156 -- (show . NEL.length $ getStashValue v))
@@ -178,7 +173,7 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
178173 else do
179174 _ <- action
180175 untilDone (n -1, deadline) action
181−
176+ reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
182177 routeCount = length reifiedApi
183178 elementOrFail ::
184179 (MonadState FuzzState m, MonadIO m) =>
@@ -195,7 +190,7 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
195190 ( forall as.
196191 (V.RecordToList as, V.RMap as) =>
197192 FuzzOp ->
198− V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
193+ V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))) ->
199194 V.Rec (TypedF V.Identity) as ->
200195 m r
201196 ) ->
@@ -236,21 +231,23 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
236231 execute ::
237232 (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as) =>
238233 FuzzOp ->
239− V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic, Int)))) ->
234+ V.Curried as (IO (Either ServerError (NonEmpty (Dynamic, Int)))) ->
240235 V.Rec (TypedF V.Identity) as ->
241236 m ()
242237 execute fuzzop func args = do
243238 (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
244239 liftIO (V.runcurry' func argVals) >>= \case
245− Left (e::InteractionError) ->
246− if fatalError e
247− then throw e
248− else pure ()
249− Right (dyn :: NEL.NonEmpty (Dynamic, Int)) ->
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
250247 modify'
251− ( \fs@FuzzState {..} ->
252− fs {stash = addToStash (NEL.toList dyn) stash}
253− )
248+ ( \fs@FuzzState {..} ->
249+ fs {stash = addToStash (NEL.toList dyn) stash}
250+ )
254251 where
255252 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
256253 -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
@@ -262,7 +259,8 @@ fuzz' reifiedApi Config {..} = handle (pure . Just . formatException) $ do
262259 (execute op func args)
263260 [ Handler (\(e :: SomeAsyncException) -> throw e),
264261 Handler
265− ( \(e :: SomeException) ->
262+ ( \(e :: SomeException) -> do
263+ -- displayDiagnostics =<< get
266264 throw . RoboservantException ServerCrashed (Just e) =<< get
267265 )
268266 ]
src/Roboservant/Server.hsdeleted−36
@@ -1,36 +0,0 @@
1−{-# LANGUAGE RankNTypes #-}
2−{-# LANGUAGE TypeApplications #-}
3−{-# LANGUAGE PolyKinds #-}
4−{-# LANGUAGE AllowAmbiguousTypes #-}
5−{-# LANGUAGE FlexibleContexts #-}
6−{-# LANGUAGE GADTs #-}
7−{-# LANGUAGE PolyKinds #-}
8−
9−{-# LANGUAGE ScopedTypeVariables #-}
10−{-# LANGUAGE TypeApplications #-}
11−{-# LANGUAGE TypeFamilies #-}
12−{-# LANGUAGE UndecidableInstances #-}
13−
14−module Roboservant.Server (fuzz, module Roboservant.Types ) where
15−
16−import Roboservant.Direct(fuzz',Report)
17−import Roboservant.Types
18− ( FlattenServer (..),
19− ReifiedApi,
20− )
21−import Roboservant.Types.ReifiedApi.Server(ToReifiedApi (..))
22−import Servant (Endpoints, Proxy (Proxy), Server)
23−import Roboservant.Types.Config
24−
25−fuzz :: forall api.
26− (FlattenServer api, ToReifiedApi (Endpoints api)) =>
27− Server api ->
28− Config ->
29− IO (Maybe Report)
30−fuzz s = fuzz' (reifyServer s)
31− -- todo: how do we pull reifyServer out?
32− where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api))
33− => Server api -> ReifiedApi
34− reifyServer server = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
35−-- reifyServer server = toReifiedApi server (Proxy @(Endpoints api))
36−
src/Roboservant/Types.hs+2−3
@@ -10,8 +10,8 @@
1010module Roboservant.Types
1111 ( module Roboservant.Types.Breakdown,
1212 module Roboservant.Types.BuildFrom,
13+ module Roboservant.Types.FlattenServer,
1314 module Roboservant.Types.ReifiedApi,
14− module Roboservant.Types.ReifiedApi.Server,
1515 module Roboservant.Types.Internal,
1616 module Roboservant.Types.Config,
1717 )
@@ -20,7 +20,6 @@ where
2020import Roboservant.Types.Breakdown
2121import Roboservant.Types.BuildFrom
2222import Roboservant.Types.Config
23+import Roboservant.Types.FlattenServer
2324import Roboservant.Types.Internal
2425import Roboservant.Types.ReifiedApi
25−import Roboservant.Types.ReifiedApi.Server
26−import Roboservant.Types.Orphans()
src/Roboservant/Types/Breakdown.hs+2−15
@@ -1,6 +1,5 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
22{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE FlexibleContexts #-}
43{-# LANGUAGE DerivingVia #-}
54{-# LANGUAGE FlexibleInstances #-}
65{-# LANGUAGE GADTs #-}
@@ -23,8 +22,6 @@ import qualified Data.List.NonEmpty as NEL
2322import Data.Typeable (Typeable)
2423import GHC.Generics
2524import Roboservant.Types.Internal
26−import Servant
27−import Roboservant.Types.Orphans()
2825
2926breakdown ::
3027 (Hashable x, Typeable x, Breakdown x) =>
@@ -41,15 +38,10 @@ instance (Hashable x, Typeable x) => Breakdown (Atom x) where
4138deriving via (Atom ()) instance Breakdown ()
4239
4340deriving via (Atom Int) instance Breakdown Int
44−deriving via (Atom Char) instance Breakdown Char
4541
46−deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, Breakdown x) => Breakdown (Maybe x)
42+deriving via (Atom [Char]) instance Breakdown [Char]
4743
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
44+class GBreakdown (f :: k -> *) where
5345 gBreakdownExtras :: f a -> [(Dynamic, Int)]
5446
5547instance (Hashable x, Typeable x, Generic x, GBreakdown (Rep x)) => Breakdown (Compound (x :: Type)) where
@@ -74,8 +66,3 @@ instance (GBreakdown a, GBreakdown b) => GBreakdown (a :+: b) where
7466
7567instance (Hashable a, Typeable a, Breakdown a) => GBreakdown (K1 R a) where
7668 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+5−12
@@ -12,7 +12,7 @@
1212
1313module Roboservant.Types.BuildFrom where
1414
15−import Data.List(nub)
15+import Control.Monad (filterM)
1616import qualified Data.Dependent.Map as DM
1717import Data.Hashable
1818import qualified Data.IntSet as IntSet
@@ -23,8 +23,6 @@ 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()
2826
2927buildFrom :: forall x. (Hashable x, BuildFrom x, Typeable x) => Stash -> Maybe (StashValue x)
3028buildFrom = buildStash . buildFrom'
@@ -62,13 +60,10 @@ deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, BuildFrom x)
6260
6361-- this isn't wonderful, but we need a hand-rolled instance for recursive datatypes right now.
6462-- with an arbitrary-ish interface, we could use a size parameter, rng access etc.
65−instance (Eq x, BuildFrom x) => BuildFrom [x] where
66− extras stash =
67− nub $ map (\xs -> (concatMap fst xs, map snd xs)) $ notpowerset $ buildFrom' @x stash
63+instance (BuildFrom x) => BuildFrom [x] where
64+ extras stash = map (\xs -> (concatMap fst xs, map snd xs)) $ powerset $ extras @x stash
6865 where
69− -- powerset creates way too much stuff. something better here eventually.
70− notpowerset xs = []:xs:map pure xs
71−
66+ powerset xs = filterM (const [True, False]) xs
7267
7368instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x :: Type)) where
7469 extras stash = fmap (Compound . to) <$> gExtras stash
@@ -77,7 +72,7 @@ deriving via (Atom Int) instance BuildFrom Int
7772
7873deriving via (Atom Char) instance BuildFrom Char
7974
80−class GBuildFrom (f :: k -> Type) where
75+class GBuildFrom (f :: k -> *) where
8176 gExtras :: Stash -> [([Provenance], f a)]
8277
8378instance GBuildFrom b => GBuildFrom (M1 D a b) where
@@ -103,5 +98,3 @@ instance BuildFrom a => GBuildFrom (K1 i a) where
10398
10499instance GBuildFrom U1 where
105100 gExtras _ = [([], U1)]
106−
107−deriving via (Atom NoContent) instance BuildFrom NoContent
src/Roboservant/Types/FlattenServer.hsnew file+44
@@ -0,0 +1,44 @@
1+{-# LANGUAGE AllowAmbiguousTypes #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE PolyKinds #-}
5+{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TypeApplications #-}
7+{-# LANGUAGE TypeOperators #-}
8+{-# LANGUAGE UndecidableInstances #-}
9+
10+-- | 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,Eq)
46+ deriving newtype (Hashable, Typeable)
4747
4848-- | can be broken down and built up from generic pieces
4949newtype Compound x = Compound {unCompound :: x}
50− deriving newtype (Hashable, Typeable, Eq)
50+ deriving newtype (Hashable, Typeable)
5151
5252hashedDyn :: (Hashable a, Typeable a) => a -> (Dynamic, Int)
5353hashedDyn a = (toDyn a, hash a)
src/Roboservant/Types/Orphans.hsdeleted−7
@@ -1,7 +0,0 @@
1−{-# OPTIONS_GHC -fno-warn-orphans #-}
2−module Roboservant.Types.Orphans where
3−
4−import Servant
5−import Data.Hashable
6−
7−instance Hashable NoContent
src/Roboservant/Types/ReifiedApi.hs+49−56
@@ -1,8 +1,3 @@
1−-- | Description: ways to build a reified api from a servant description.
2−--
3−-- arguably this could be more general and be abstracted away from even relying on servant
4−-- but that's future work.
5−
61{-# LANGUAGE AllowAmbiguousTypes #-}
72{-# LANGUAGE DataKinds #-}
83{-# LANGUAGE DerivingStrategies #-}
@@ -20,22 +15,25 @@
2015{-# LANGUAGE CPP #-}
2116module Roboservant.Types.ReifiedApi where
2217
18+import Control.Monad.Except (runExceptT)
19+import Data.Bifunctor
2320import Data.Dynamic (Dynamic)
24−import Control.Exception(Exception)
21+import Data.Kind
2522import Data.List.NonEmpty (NonEmpty)
2623import Data.Typeable (Typeable)
2724import GHC.Generics ((:*:)(..))
25+import GHC.TypeLits (Symbol)
2826import Roboservant.Types.Internal
2927import Roboservant.Types.Breakdown
3028import Roboservant.Types.BuildFrom
31−import Data.Kind(Type)
29+import Roboservant.Types.FlattenServer
3230import Servant
3331import Servant.API.Modifiers(FoldRequired,FoldLenient)
34−import GHC.TypeLits (Symbol)
3532import qualified Data.Text as T
3633import qualified Data.Vinyl as V
3734import qualified Data.Vinyl.Curry as V
3835import qualified Type.Reflection as R
36+import Data.Hashable(Hashable)
3937
4038newtype ApiOffset = ApiOffset Int
4139 deriving (Eq, Show, Ord)
@@ -49,32 +47,61 @@ newtype Argument a = Argument
4947
5048data ReifiedEndpoint = forall as. (V.RecordToList as, V.RMap as) => ReifiedEndpoint
5149 { reArguments :: V.Rec (TypedF Argument) as
52− , reEndpointFunc :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
50+ , reEndpointFunc :: V.Curried as (IO (Either ServerError (NonEmpty (Dynamic,Int))))
5351 }
5452
55−instance Show ReifiedEndpoint where
56− show _ = "lol"
53+type ReifiedApi = [(ApiOffset, ReifiedEndpoint)]
54+
55+tagType :: Typeable a => f a -> TypedF f a
56+tagType = (R.typeRep :*:)
57+
58+class ToReifiedApi (endpoints ) where
59+
60+ toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
5761
5862class ( V.RecordToList (EndpointArgs endpoint)
5963 , V.RMap (EndpointArgs endpoint)
60− ) => ToReifiedEndpoint (endpoint :: Type) where
64+ ) => ToReifiedEndpoint (endpoint :: *) where
6165 type EndpointArgs endpoint :: [Type]
6266 type EndpointRes endpoint :: Type
6367
6468 reifiedEndpointArguments :: V.Rec (TypedF Argument) (EndpointArgs endpoint)
6569
70+instance ToReifiedApi '[] where
71+ toReifiedApi NoEndpoints _ = []
6672
67−tagType :: Typeable a => f a -> TypedF f a
68−tagType = (R.typeRep :*:)
69−
70−data InteractionError = InteractionError
71− { errorMessage :: T.Text
72− , fatalError :: Bool
73− }
74− deriving Show
75−instance Exception InteractionError
76−
73+instance
74+ ( Typeable (EndpointRes endpoint)
7775
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
78105
79106instance
80107 (Typeable responseType, Breakdown responseType) =>
@@ -84,12 +111,6 @@ instance
84111 type EndpointRes (Verb method statusCode contentTypes responseType) = responseType
85112 reifiedEndpointArguments = V.RNil
86113
87−instance ToReifiedEndpoint (NoContentVerb method)
88− where
89− type EndpointArgs (NoContentVerb method) = '[]
90− type EndpointRes (NoContentVerb method) = NoContent
91− reifiedEndpointArguments = V.RNil
92−
93114instance
94115 (ToReifiedEndpoint endpoint) =>
95116 ToReifiedEndpoint ((x :: Symbol) :> endpoint)
@@ -98,16 +119,6 @@ instance
98119 type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
99120 reifiedEndpointArguments = reifiedEndpointArguments @endpoint
100121
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−
111122instance
112123 (ToReifiedEndpoint endpoint) =>
113124 ToReifiedEndpoint (Description s :> endpoint)
@@ -150,24 +161,6 @@ instance
150161 tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
151162 V.:& reifiedEndpointArguments @endpoint
152163
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−
171164instance
172165 ( BuildFrom (IfRequiredLenient T.Text mods headerType)
173166 , ToReifiedEndpoint endpoint
src/Roboservant/Types/ReifiedApi/Server.hsdeleted−114
@@ -1,114 +0,0 @@
1−{-# LANGUAGE RankNTypes #-}
2−{-# LANGUAGE TypeApplications #-}
3−{-# LANGUAGE PolyKinds #-}
4−{-# LANGUAGE AllowAmbiguousTypes #-}
5−{-# LANGUAGE FlexibleContexts #-}
6−{-# LANGUAGE GADTs #-}
7−{-# LANGUAGE LambdaCase #-}
8−{-# LANGUAGE PolyKinds #-}
9−
10−{-# LANGUAGE ScopedTypeVariables #-}
11−{-# LANGUAGE TypeApplications #-}
12−{-# LANGUAGE TypeFamilies #-}
13−{-# LANGUAGE UndecidableInstances #-}
14−{-# LANGUAGE CPP #-}
15−{-# LANGUAGE DataKinds #-}
16−{-# LANGUAGE TypeOperators #-}
17−{-# LANGUAGE FlexibleInstances #-}
18−{-# LANGUAGE MultiParamTypeClasses #-}
19−module Roboservant.Types.ReifiedApi.Server(module Roboservant.Types.ReifiedApi.Server) where
20−
21−import Servant
22−
23−import Control.Monad.Except (runExceptT)
24−import Data.Bifunctor
25−import Data.Dynamic (Dynamic)
26−import Data.List.NonEmpty (NonEmpty)
27−import Data.Typeable (Typeable)
28−import Roboservant.Types.Breakdown
29−import Roboservant.Types.ReifiedApi
30−
31−import qualified Data.Text as T
32−import qualified Data.Vinyl.Curry as V
33−import Data.Hashable(Hashable)
34−
35−type ReifiedApi = [(ApiOffset, ReifiedEndpoint )]
36−
37−
38−class ToReifiedApi endpoints where
39− toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
40−
41−instance ToReifiedApi '[] where
42− toReifiedApi NoEndpoints _ = []
43−
44−instance
45− ( NormalizeFunction (ServerT endpoint Handler)
46− , Normal (ServerT endpoint Handler) ~ V.Curried (EndpointArgs endpoint) (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
47− , ToReifiedEndpoint endpoint
48− , ToReifiedApi endpoints
49− ) =>
50− ToReifiedApi (endpoint : endpoints)
51− where
52− toReifiedApi (endpoint `AnEndpoint` endpoints) _ =
53− (0, ReifiedEndpoint
54− { reArguments = reifiedEndpointArguments @endpoint
55− , reEndpointFunc = normalize endpoint
56− }
57− )
58− : (map . first) (+1)
59− (toReifiedApi endpoints (Proxy @endpoints))
60−
61−
62−instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x) where
63− type Normal (Handler x) = IO (Either InteractionError (NonEmpty (Dynamic,Int)))
64− normalize handler = (runExceptT . runHandler') handler >>= \case
65− Left serverError -> pure (Left (renderServerError serverError))
66− where
67− -- | TODO improve this
68− renderServerError :: ServerError -> InteractionError
69− renderServerError s = InteractionError (T.pack $ show s) (errHTTPCode serverError == 500)
70−
71− Right x -> pure $ Right $ breakdown x
72−
73−
74−-- case errHTTPCode serverError of
75−-- 500 -> throw serverError
76−-- _ ->
77−-- liftIO . logInfo . show $ ("ignoring non-500 error", serverError)
78−
79−
80−data Bundled endpoints where
81− -- AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
82− AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
83− NoEndpoints :: Bundled '[]
84−
85−class FlattenServer api where
86− flattenServer :: Server api -> Bundled (Endpoints api)
87−
88−instance
89− ( FlattenServer api,
90− Endpoints endpoint ~ '[endpoint]
91− ) =>
92− FlattenServer (endpoint :<|> api)
93− where
94− flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
95−
96−instance
97− (
98− Endpoints api ~ '[api]
99− ) =>
100− FlattenServer (x :> api)
101− where
102− flattenServer server = server `AnEndpoint` NoEndpoints
103−
104−instance FlattenServer (Verb method statusCode contentTypes responseType)
105− where
106− flattenServer server = server `AnEndpoint` NoEndpoints
107−
108−class NormalizeFunction m where
109− type Normal m
110− normalize :: m -> Normal m
111−
112−instance NormalizeFunction x => NormalizeFunction (r -> x) where
113− type Normal (r -> x) = r -> Normal x
114− normalize = fmap normalize
stack.yaml+12−5
@@ -1,10 +1,17 @@
1−resolver: lts-20.4
1+resolver: lts-16.27
2+# bit faster on ReifiedApi stuff which is unfortunately terribly slow.
3+# compiler: ghc-8.10.3
24packages:
35- .
46
57extra-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−# - constraints-extras-0.3.0.2@sha256:013b8d0392582c6ca068e226718a4fe8be8e22321cc0634f6115505bf377ad26,1853
10−- some-1.0.3
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
stack.yaml.lock+58−9
@@ -7,20 +7,69 @@ packages:
77- completed:
88 hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
99 pantry-tree:
10− sha256: 04f12c7bef2c3f9a25d94eb9489752ed498db8e243069fe95838dbb51df1dcb3
1110 size: 325
11+ sha256: 04f12c7bef2c3f9a25d94eb9489752ed498db8e243069fe95838dbb51df1dcb3
1212 original:
1313 hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
1414- completed:
15− hackage: some-1.0.3@sha256:938cec5fc0d3a08dde34cfc0acd5a0aa2e9fd489c9dbb64417f16588dfb47af0,2399
15+ hackage: vinyl-0.13.0@sha256:0f247cd3f8682b30881a07de18e6fec52d540646fbcb328420049cc8d63cd407,3724
16+ pantry-tree:
17+ size: 1857
18+ sha256: 860fb95820b595161cdbdec5f376100ebae2d14e5ef0dbe311546202f7525d01
19+ original:
20+ hackage: vinyl-0.13.0
21+- completed:
22+ hackage: dependent-sum-0.7.1.0@sha256:5599aa89637db434431b1dd3fa7c34bc3d565ee44f0519bfbc877be1927c2531,2068
23+ pantry-tree:
24+ size: 290
25+ sha256: 9cbfb32b5a8a782b7a1c941803fd517633cb699159b851c1d82267a9e9391b50
26+ original:
27+ hackage: dependent-sum-0.7.1.0
28+- completed:
29+ hackage: constraints-extras-0.3.0.2@sha256:013b8d0392582c6ca068e226718a4fe8be8e22321cc0634f6115505bf377ad26,1853
30+ pantry-tree:
31+ size: 594
32+ sha256: 3ce1012bfb02e4d7def9df19ce80b8cd2b472c691b25b181d9960638673fecd1
33+ original:
34+ hackage: constraints-extras-0.3.0.2
35+- completed:
36+ hackage: dependent-map-0.4.0.0@sha256:ca2b131046f4340a1c35d138c5a003fe4a5be96b14efc26291ed35fd08c62221,1657
37+ pantry-tree:
38+ size: 551
39+ sha256: 5defa30010904d2ad05a036f3eaf83793506717c93cbeb599f40db1a3632cfc5
40+ original:
41+ hackage: dependent-map-0.4.0.0
42+- completed:
43+ hackage: servant-0.18.2@sha256:f8c9f0e9891a3ada1337a3c0b369333a3b5a2d0909dd3cd09d79bc26adeaca44,5298
44+ pantry-tree:
45+ size: 2662
46+ sha256: e930e814de1aa4d24274bdf18341a50b7ed38604ae4734f730e09238ac5bf7e2
47+ original:
48+ hackage: servant-0.18.2
49+- completed:
50+ hackage: servant-client-0.18.2@sha256:82578ade7468873259bb2fdc9d62290a0f998550900683e1410a237ed4b05410,4591
51+ pantry-tree:
52+ size: 1300
53+ sha256: 6324892c77bedbce32f0d6f1612fc2cb0d82c163d3be39efb951d3cc3792ce4a
54+ original:
55+ hackage: servant-client-0.18.2
56+- completed:
57+ hackage: servant-client-core-0.18.2@sha256:ad63ae0f227373fea7e547d4c2a7b0b69e112ff409a83cbadffc9f6ee049926f,3763
58+ pantry-tree:
59+ size: 1444
60+ sha256: 9e37bc5f8cbb70cf1accb20bd6f83fca4c2ca42472cd0fc4b22183c2c57cbe3b
61+ original:
62+ hackage: servant-client-core-0.18.2
63+- completed:
64+ hackage: servant-server-0.18.2@sha256:56679af62ab8820a2108da6153d9ae9dde37199e62172365bdaea1458c3f7c2d,5482
1665 pantry-tree:
17− sha256: b88dddc6b991cd570c6750f8d309a9feb76938b29a690abdabc4ababb8c7b2f8
18− size: 709
66+ size: 2614
67+ sha256: 3ac7430134439e4b67f0f5333f63b89d0cb7de5e2e07f0af7801c8e223942b9c
1968 original:
20− hackage: some-1.0.3
69+ hackage: servant-server-0.18.2
2170snapshots:
2271- completed:
23− sha256: 3770dfd79f5aed67acdcc65c4e7730adddffe6dba79ea723cfb0918356fc0f94
24− size: 648660
25− url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/4.yaml
26− original: lts-20.4
72+ size: 533252
73+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/27.yaml
74+ sha256: c2aaae52beeacf6a5727c1010f50e89d03869abfab6d2c2658ade9da8ed50c73
75+ original: lts-16.27
test/Breakdown.hs+1−1
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
test/Nested.hs+3−3
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
2−
2+{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
@@ -11,7 +11,7 @@ import Servant
1111import Servant.API.Flatten
1212
1313type Api =
14− ( "one" :> Summary "foo" :> Post '[JSON] Int
14+ ( "one" :> Post '[JSON] Int
1515 :<|> "two" :> Post '[JSON] Int
1616 )
1717 :<|> ( "three" :> Post '[JSON] Int
test/Post.hs+2−2
@@ -1,8 +1,8 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
5−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
88module Post where
test/Product.hs+1−1
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
test/Put.hsdeleted−33
@@ -1,33 +0,0 @@
1−{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE DeriveGeneric #-}
3−{-# LANGUAGE DerivingStrategies #-}
4−
5−
6−{-# LANGUAGE TypeOperators #-}
7−
8−module Put where
9−
10−import Data.Aeson
11−import Data.Hashable
12−import GHC.Generics (Generic)
13−import Servant
14−
15−type Api =
16− Get '[JSON] Foo
17− :<|> ReqBody '[JSON] Foo :> Put '[JSON] ()
18− :<|> ReqBody '[JSON] Foo :> Put '[JSON] NoContent
19−
20−data Foo = Foo
21− deriving (Eq, Show, Generic)
22−
23−instance Hashable Foo
24−
25−instance ToJSON Foo
26−
27−instance FromJSON Foo
28−
29−server :: Server Api
30−server =
31− pure Foo
32− :<|> const (pure ())
33− :<|> const (pure NoContent)
test/QueryParams.hsdeleted−15
@@ -1,15 +0,0 @@
1−{-# LANGUAGE DataKinds #-}
2−
3−{-# LANGUAGE DerivingStrategies #-}
4−
5−
6−{-# LANGUAGE TypeOperators #-}
7−
8−module QueryParams where
9−
10−import Servant
11−
12−type Api = QueryParams "ints" Int :> Get '[JSON] [Int]
13−
14−server :: Server Api
15−server = pure
test/Spec.hs+65−105
@@ -4,13 +4,7 @@
44{-# LANGUAGE RecordWildCards #-}
55{-# LANGUAGE StandaloneDeriving #-}
66{-# LANGUAGE TypeApplications #-}
7−{-# LANGUAGE RankNTypes #-}
8−{-# LANGUAGE FlexibleContexts #-}
9−{-# LANGUAGE AllowAmbiguousTypes #-}
10−{-# LANGUAGE ScopedTypeVariables #-}
11−{-# LANGUAGE DataKinds #-}
127{-# OPTIONS_GHC -fno-warn-orphans #-}
13−{-# LANGUAGE TypeFamilies #-}
148
159import qualified Breakdown
1610import Data.Dynamic (toDyn)
@@ -21,134 +15,100 @@ import qualified Foo
2115import qualified Headers
2216import qualified Nested
2317import qualified Post
24−import qualified Put
2518import qualified Product
26−import qualified QueryParams
27−import qualified Roboservant as R
28−import qualified Roboservant.Server as RS
29−import qualified Roboservant.Client as RC
19+import qualified Roboservant as RS
3020import qualified Seeded
3121import Test.Hspec
3222import Test.Hspec.Core.Spec (FailureReason (Reason), ResultStatus (Failure, Success), itemExample, mapSpecItem_, resultStatus)
3323import 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((>=>))
4124
4225main :: IO ()
4326main = hspec spec
4427
45−fuzzBoth
46− :: forall a .
47− (R.ToReifiedApi (Endpoints a), HasServer a '[], RS.FlattenServer a, RC.ToReifiedClientApi (Endpoints a), RC.FlattenClient a,
48− HasClient ClientM a)
49− => String -> Server a -> R.Config -> (Maybe R.Report -> IO ()) -> Spec
50−fuzzBoth name server config condition = do
51− it (name <> " via server") $
52− RS.fuzz @a server config >>= condition
53−
54− around (withServer (serve (Proxy :: Proxy a) server)) $
55− it (name <> " via client") $ \(clientEnv::ClientEnv) -> do
56− RC.fuzz @a clientEnv config >>= condition
57−
58−withServer :: Application -> ActionWith ClientEnv -> IO ()
59−withServer app action = Warp.testWithApplication (pure app) (genClientEnv >=> action)
60− where genClientEnv port = do
61− baseUrl <- parseBaseUrl "http://localhost"
62− manager <- newManager defaultManagerSettings
63− pure $ mkClientEnv manager (baseUrl { baseUrlPort = port })
64−
6528spec :: Spec
6629spec = do
6730 describe "Basic usage" $ do
6831 describe "noError" $ do
69− fuzzBoth @Valid.Api "find no error in a basic app" Valid.server R.defaultConfig (`shouldSatisfy` isNothing)
70− fuzzBoth @Valid.RoutedApi "finds no error in a valid generic app" Valid.routedServer R.defaultConfig (`shouldSatisfy` isNothing)
71− fuzzBoth @Valid.Api "fails coverage check" Valid.server R.defaultConfig {R.coverageThreshold = 0.6}
72− (\r ->
73− fmap (R.failureReason . R.rsException) r
74− `shouldSatisfy` ( \case
75− Just (R.InsufficientCoverage _) -> True
76− _ -> False
77− ))
78− describe "posted body" $
79− fuzzBoth @Post.Api "passes a coverage check using a posted body" Post.server R.defaultConfig {R.coverageThreshold = 0.99}
80− (`shouldSatisfy` isNothing)
81−
82−
83− describe "PUTted body" $
84− fuzzBoth @Put.Api "passes a coverage check using a posted body" Put.server R.defaultConfig {R.coverageThreshold = 0.99}
85− (`shouldSatisfy` isNothing)
86−
87−
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)
8849 describe "seeded" $ do
89− let res = Seeded.Seed 1
90− shouldFail $ fuzzBoth @Seeded.Api "finds an error using information passed in" Seeded.server
91− (R.defaultConfig {R.seed = [(toDyn res, hash res)]})
92− (`shouldSatisfy` isNothing)
93−
94− describe "Foo" $
95− fuzzBoth @Foo.Api "finds an error in a basic app" Foo.server R.defaultConfig (`shouldSatisfy` serverFailure)
96−
97− describe "QueryParams" $
98− fuzzBoth @QueryParams.Api "can handle query params" QueryParams.server R.defaultConfig { R.seed = [R.hashedDyn (12::Int)] }
99− (`shouldSatisfy` isNothing)
100−
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)
10159 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)
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)
10968 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
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
11983serverFailure = \case
120− Just R.Report {..} ->
121− let R.RoboservantException {..} = rsException
122− in failureReason /= R.NoPossibleMoves
84+ Just RS.Report {..} ->
85+ let RS.RoboservantException {..} = rsException
86+ in failureReason /= RS.NoPossibleMoves
12387 _ -> False
12488
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
89+deriving via (RS.Atom Foo.Foo) instance RS.Breakdown Foo.Foo
13090
131−deriving via (R.Atom Headers.Foo) instance R.BuildFrom Headers.Foo
91+deriving via (RS.Atom Foo.Foo) instance RS.BuildFrom Foo.Foo
13292
133−deriving via (R.Atom Seeded.Seed) instance R.Breakdown Seeded.Seed
93+deriving via (RS.Atom Headers.Foo) instance RS.Breakdown Headers.Foo
13494
135−deriving via (R.Atom Seeded.Seed) instance R.BuildFrom Seeded.Seed
95+deriving via (RS.Atom Headers.Foo) instance RS.BuildFrom Headers.Foo
13696
137−deriving via (R.Atom Void) instance R.BuildFrom Void
97+deriving via (RS.Atom Seeded.Seed) instance RS.Breakdown Seeded.Seed
13898
139−deriving via (R.Atom Post.FooPost) instance R.Breakdown Post.FooPost
140−deriving via (R.Atom Post.FooPost) instance R.BuildFrom Post.FooPost
99+deriving via (RS.Atom Seeded.Seed) instance RS.BuildFrom Seeded.Seed
141100
142−deriving via (R.Atom Put.Foo) instance R.Breakdown Put.Foo
143−deriving via (R.Atom Put.Foo) instance R.BuildFrom Put.Foo
101+deriving via (RS.Atom Void) instance RS.BuildFrom Void
144102
103+deriving via (RS.Atom Post.FooPost) instance RS.Breakdown Post.FooPost
145104
105+deriving via (RS.Atom Post.FooPost) instance RS.BuildFrom Post.FooPost
146106
147−deriving via (R.Compound Breakdown.Foo) instance R.Breakdown Breakdown.Foo
107+deriving via (RS.Compound Breakdown.Foo) instance RS.Breakdown Breakdown.Foo
148108
149−deriving via (R.Compound Product.Foo) instance R.BuildFrom Product.Foo
109+deriving via (RS.Compound Product.Foo) instance RS.BuildFrom Product.Foo
150110
151−deriving via (R.Compound Breakdown.SomeSum) instance R.Breakdown Breakdown.SomeSum
111+deriving via (RS.Compound Breakdown.SomeSum) instance RS.Breakdown Breakdown.SomeSum
152112
153113-- | `shouldFail` allows you to assert that a given `Spec` should contain at least one failing test.
154114-- this is often useful when testing tests.
@@ -158,7 +118,7 @@ shouldFail =
158118 ( \i ->
159119 i
160120 { itemExample = \p a cb -> do
161− r <- itemExample i p a cb
121+ r <- (itemExample i) p a cb
162122 pure
163123 r
164124 { resultStatus = case resultStatus r of
test/UnsafeIO.hs+3−3
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
2−
2+{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
@@ -22,7 +22,7 @@ healthcheck ref = do
2222 t <- liftIO $ readIORef ref
2323 case t of
2424 0 -> pure ()
25− n -> throwError $ err500 {errBody = "observed inconsistency: " <> BL8.pack (show n)}
25+ n -> throwError $ err500 {errBody = "observed inconsistency: " <> (BL8.pack $ show n)}
2626
2727makeServer :: IO (Server UnsafeApi)
2828makeServer = do
test/Valid.hs+3−2
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
4−
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE TypeOperators #-}
77
@@ -10,6 +10,7 @@ module Valid where
1010import Data.Void
1111import GHC.Generics
1212import Servant
13+import Servant.API.Generic
1314import Servant.Server.Generic
1415
1516type Api =
@@ -20,7 +21,7 @@ data Routes route
2021 = Routes
2122 { getInt ::
2223 route
23− :- Summary "i'm a summary!" :> Get '[JSON] Int,
24+ :- Get '[JSON] Int,
2425 captureIt ::
2526 route
2627 :- Capture "void" Void :> Get '[JSON] ()