on master help sign in
make servant-client work
commitc41cee329630929acd3a20304c22dd1c736a5a40 authorMark Wotton <mwotton@gmail.com> dateTue 16 Mar 2021, 15:20 (6 years ago) parent53ab47e
14 files changed+485−252
EXAMPLE.md+40−3 benchmarks/genFile.sh+15−2 benchmarks/prelude+7−16 benchmarks/runTiming.sh+2−2 package.yaml+8−1 roboservant.cabal+11−3 src/Roboservant/Client.hs+117 src/Roboservant/Direct.hs+1 src/Roboservant/Server.hs+40 src/Roboservant/Types.hs+2−4 src/Roboservant/Types/FlattenServer.hs−40 src/Roboservant/Types/ReifiedApi.hs+128−2 src/Roboservant/Types/ReifiedApi/Server.hs+34−123 test/Spec.hs+80−56
EXAMPLE.md+40−3
@@ -13,23 +13,37 @@ Our api under test:
1313{-# LANGUAGE DeriveGeneric #-}
1414{-# LANGUAGE GeneralizedNewtypeDeriving #-}
1515{-# LANGUAGE RecordWildCards #-}
16+{-# LANGUAGE ScopedTypeVariables #-}
17+{-# LANGUAGE TypeApplications #-}
1618
17−import Roboservant
19+import qualified Roboservant.Server as RS
20+import qualified Roboservant.Client as RC
21+import Servant.Client(ClientEnv, baseUrlPort, parseBaseUrl, mkClientEnv)
22+import Network.HTTP.Client (newManager, defaultManagerSettings)
23+import Roboservant.Types
1824import Test.Hspec
1925import Servant
2026import GHC.Generics
2127import Data.Typeable
2228import Data.Hashable
2329import Data.Maybe(isNothing, isJust)
30+import qualified Network.Wai.Handler.Warp as Warp
31+import Data.Aeson(FromJSON,ToJSON)
2432
2533newtype A = A Int
2634 deriving (Generic, Eq, Show, Typeable)
2735 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
2836
37+instance FromJSON A
38+instance ToJSON A
39+
2940newtype B = B Int
3041 deriving (Generic, Eq, Show, Typeable)
3142 deriving newtype (Hashable, FromHttpApiData, ToHttpApiData)
3243
44+instance FromJSON B
45+instance ToJSON B
46+
3347type Api =
3448 "item" :> Get '[JSON] A
3549 :<|> "itemAdd" :> Capture "one" B :> Capture "two" B :> Get '[JSON] B
@@ -62,13 +76,24 @@ main = hspec spec
6276spec :: Spec
6377spec = describe "example" $ do
6478 it "good server should not fail" $ do
65− fuzz @Api goodServer config
79+ RS.fuzz @Api goodServer config
6680 >>= (`shouldSatisfy` isNothing)
6781 it "bad server should fail" $ do
68− fuzz @Api badServer config
82+ RS.fuzz @Api badServer config
6983 >>= (`shouldSatisfy` isJust)
7084```
7185
86+The previous test just picked apart the server and ran functions manually: sometimes, we want to test via
87+an honest-to-goodness network port, like so:
88+
89+```haskell
90+ around (withServer (serve (Proxy :: Proxy Api) badServer)) $ do
91+ it "we should also be able to run the _client_ to an independent server (ignore server error messages)" $ \(clientEnv::ClientEnv) -> do
92+ RC.fuzz @Api clientEnv config >>= (`shouldSatisfy` isJust)
93+```
94+
95+(we use withApplication rather than testWithApplication because we don't primarily care what the server does here:
96+we want to check what a client does when presented with a faulty server.)
7297
7398We expect to be able to cover the whole api from our starting point, so let's set the coverage to 0.99.
7499There are other tweakable things in the config, like maximum runtime, reps,
@@ -104,3 +129,15 @@ build it up from components.
104129deriving via (Compound B) instance BuildFrom B
105130deriving via (Atom B) instance Breakdown B
106131```
132+
133+
134+test utilities:
135+
136+``` haskell
137+withServer :: Application -> ActionWith ClientEnv -> IO ()
138+withServer app action = Warp.withApplication (pure app) (\p -> genClientEnv p >>= action)
139+ where genClientEnv port = do
140+ baseUrl <- parseBaseUrl "http://localhost"
141+ manager <- newManager defaultManagerSettings
142+ pure $ mkClientEnv manager (baseUrl { baseUrlPort = port })
143+```
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+8−1
@@ -55,7 +55,10 @@ tests:
5555 - hspec
5656 - hspec-core
5757 - http-api-data
58−
58+ - http-client
59+ - hspec-wai
60+ - wai
61+ - warp
5962 example:
6063 main: Example.lhs
6164 source-dirs: .
@@ -63,6 +66,10 @@ tests:
6366 dependencies:
6467 - roboservant
6568 - hspec
69+ - warp
70+ - http-client
71+ - aeson
72+ - hspec-core
6673 ghc-options: -pgmL markdown-unlit
6774
6875 build-tools: markdown-unlit
roboservant.cabal+11−3
@@ -4,7 +4,7 @@ cabal-version: 1.12
44--
55-- see: https://github.com/sol/hpack
66--
7−-- hash: fa8c07f0c2fc10f18d3e925b8f702612c0c9fd81faa7942f901af388d509c231
7+-- hash: 4c71b6b9460f4719791bb8e67f4a5398b807cd8d17c43dc69da806bb5ba9fb4b
88
99name: roboservant
1010version: 0.1.0.2
@@ -28,13 +28,13 @@ source-repository head
2828library
2929 exposed-modules:
3030 Roboservant
31+ Roboservant.Client
3132 Roboservant.Direct
3233 Roboservant.Server
3334 Roboservant.Types
3435 Roboservant.Types.Breakdown
3536 Roboservant.Types.BuildFrom
3637 Roboservant.Types.Config
37− Roboservant.Types.FlattenServer
3838 Roboservant.Types.Internal
3939 Roboservant.Types.ReifiedApi
4040 Roboservant.Types.ReifiedApi.Server
@@ -76,13 +76,16 @@ test-suite example
7676 build-tool-depends:
7777 markdown-unlit:markdown-unlit
7878 build-depends:
79− base >=4.7 && <5
79+ aeson
80+ , base >=4.7 && <5
8081 , bytestring
8182 , containers
8283 , dependent-map
8384 , dependent-sum
8485 , hashable
8586 , hspec
87+ , hspec-core
88+ , http-client
8689 , lifted-base
8790 , monad-control
8891 , mtl
@@ -97,6 +100,7 @@ test-suite example
97100 , time
98101 , unordered-containers
99102 , vinyl
103+ , warp
100104 default-language: Haskell2010
101105
102106test-suite roboservant-test
@@ -126,7 +130,9 @@ test-suite roboservant-test
126130 , hashable
127131 , hspec
128132 , hspec-core
133+ , hspec-wai
129134 , http-api-data
135+ , http-client
130136 , lifted-base
131137 , monad-control
132138 , mtl
@@ -141,4 +147,6 @@ test-suite roboservant-test
141147 , time
142148 , unordered-containers
143149 , vinyl
150+ , wai
151+ , warp
144152 default-language: Haskell2010
src/Roboservant/Client.hsnew file+117
@@ -0,0 +1,117 @@
1+{-# LANGUAGE RankNTypes #-}
2+{-# LANGUAGE TypeOperators #-}
3+{-# LANGUAGE DataKinds #-}
4+{-# LANGUAGE AllowAmbiguousTypes #-}
5+{-# LANGUAGE DataKinds #-}
6+{-# LANGUAGE NamedFieldPuns#-}
7+{-# LANGUAGE GADTs #-}
8+{-# LANGUAGE PolyKinds #-}
9+{-# LANGUAGE ScopedTypeVariables #-}
10+{-# LANGUAGE TypeApplications #-}
11+{-# LANGUAGE TypeOperators #-}
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+
32+-- fuzz :: forall api.
33+-- (FlattenServer api, ToReifiedApi (Endpoints api)) =>
34+-- Server api ->
35+-- Config ->
36+-- IO (Maybe Report)
37+-- fuzz s = fuzz' (reifyServer s)
38+-- -- todo: how do we pull reifyServer out?
39+-- where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api))
40+-- => Server api -> ReifiedApi
41+-- reifyServer server = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
42+
43+fuzz :: forall api . (ToReifiedClientApi (Endpoints api), FlattenClient api, HasClient ClientM api)
44+ => ClientEnv -> Config -> IO (Maybe Report)
45+fuzz clientEnv config = fuzz' (toReifiedClientApi (flattenClient @api apiClient) (Proxy @(Endpoints api)) clientEnv ) config
46+ where apiClient = client (Proxy @api)
47+
48+
49+
50+class ToReifiedClientApi api where
51+ toReifiedClientApi :: ClientBundled api -> Proxy api -> ClientEnv -> ReifiedApi
52+
53+data ClientBundled endpoints where
54+ AClientEndpoint :: Client ClientM endpoint -> ClientBundled endpoints -> ClientBundled (endpoint ': endpoints)
55+ NoClientEndpoints :: ClientBundled '[]
56+
57+
58+class FlattenClient api where
59+ flattenClient :: Client ClientM api -> ClientBundled (Endpoints api)
60+
61+instance
62+ ( NormalizeFunction (Client ClientM endpoint)
63+ , Normal (Client ClientM endpoint) ~ V.Curried (EndpointArgs endpoint) (ReaderT ClientEnv IO (Either InteractionError (NonEmpty (Dynamic,Int))))
64+ , ToReifiedClientApi endpoints
65+ , V.RecordCurry' (EndpointArgs endpoint)
66+ , ToReifiedEndpoint endpoint) =>
67+ ToReifiedClientApi (endpoint : endpoints) where
68+ toReifiedClientApi (endpoint `AClientEndpoint` endpoints) _ clientEnv =
69+ (0, ReifiedEndpoint
70+ { reArguments = reifiedEndpointArguments @endpoint
71+ , reEndpointFunc = foo (normalize endpoint)
72+ }
73+ )
74+ : (map . first) (+1)
75+ (toReifiedClientApi endpoints (Proxy @endpoints) clientEnv)
76+ where
77+
78+ foo :: V.Curried (EndpointArgs endpoint) (ReaderT ClientEnv IO ResultType)
79+ -> V.Curried (EndpointArgs endpoint) (IO ResultType)
80+ foo = mapCurried @(EndpointArgs endpoint) @(ReaderT ClientEnv IO ResultType) (`runReaderT` clientEnv)
81+
82+mapCurried :: forall ts a b. V.RecordCurry' ts => (a -> b) -> V.Curried ts a -> V.Curried ts b
83+mapCurried f g = V.rcurry' @ts $ f . V.runcurry' g
84+
85+type ResultType = Either InteractionError (NonEmpty (Dynamic,Int))
86+-- runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
87+
88+
89+instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (ClientM x) where
90+ type Normal (ClientM x) = ReaderT ClientEnv IO (Either InteractionError (NonEmpty (Dynamic,Int)))
91+ normalize c = ReaderT $
92+ fmap (bimap (InteractionError . T.pack . show) breakdown) . runClientM c
93+--
94+
95+instance ToReifiedClientApi '[] where
96+ toReifiedClientApi NoClientEndpoints _ _ = []
97+
98+
99+instance
100+ ( FlattenClient api,
101+ Endpoints endpoint ~ '[endpoint]
102+ ) =>
103+ FlattenClient (endpoint :<|> api)
104+ where
105+ flattenClient (endpoint :<|> c) = endpoint `AClientEndpoint` flattenClient @api c
106+
107+instance
108+ (
109+ Endpoints api ~ '[api]
110+ ) =>
111+ FlattenClient (x :> api)
112+ where
113+ flattenClient c = c `AClientEndpoint` NoClientEndpoints
114+
115+instance FlattenClient (Verb method statusCode contentTypes responseType)
116+ where
117+ flattenClient c = c `AClientEndpoint` NoClientEndpoints
src/Roboservant/Direct.hs+1
@@ -65,6 +65,7 @@ import Roboservant.Types
6565 TypedF,
6666 )
6767import Roboservant.Types.Config
68+
6869import System.Random (Random (randomR), StdGen, mkStdGen)
6970import qualified Type.Reflection as R
7071
src/Roboservant/Server.hsnew file+40
@@ -0,0 +1,40 @@
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+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE RecordWildCards #-}
11+{-# LANGUAGE ScopedTypeVariables #-}
12+{-# LANGUAGE TupleSections #-}
13+{-# LANGUAGE TypeApplications #-}
14+{-# LANGUAGE TypeFamilies #-}
15+{-# LANGUAGE UndecidableInstances #-}
16+
17+module Roboservant.Server (fuzz, module Roboservant.Types ) where
18+
19+import Roboservant.Direct(fuzz',Report)
20+import Roboservant.Types
21+ ( FlattenServer (..),
22+ ReifiedApi,
23+ )
24+import Roboservant.Types.ReifiedApi.Server(ToReifiedApi (..))
25+import Servant (Endpoints, Proxy (Proxy), Server)
26+import Roboservant.Types.Config
27+
28+fuzz :: forall api.
29+ (FlattenServer api, ToReifiedApi (Endpoints api)) =>
30+ Server api ->
31+ Config ->
32+ IO (Maybe Report)
33+fuzz s = fuzz' (reifyServer s)
34+ -- todo: how do we pull reifyServer out?
35+ where reifyServer :: (FlattenServer api, ToReifiedApi (Endpoints api))
36+ => Server api -> ReifiedApi
37+ reifyServer server = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
38+-- reifyServer server = toReifiedApi server (Proxy @(Endpoints api))
39+
40+
src/Roboservant/Types.hs+2−4
@@ -10,18 +10,16 @@
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,
17− Atom, Compound
18−
1917 )
2018where
2119
2220import Roboservant.Types.Breakdown
2321import Roboservant.Types.BuildFrom
2422import Roboservant.Types.Config
25−import Roboservant.Types.FlattenServer
2623import Roboservant.Types.Internal
2724import Roboservant.Types.ReifiedApi
25+import Roboservant.Types.ReifiedApi.Server
src/Roboservant/Types/FlattenServer.hsdeleted−40
@@ -1,40 +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− (
32− Endpoints api ~ '[api]
33− ) =>
34− FlattenServer (x :> api)
35− where
36− flattenServer server = server `AnEndpoint` NoEndpoints
37−
38−instance FlattenServer (Verb method statusCode contentTypes responseType)
39− where
40− flattenServer server = server `AnEndpoint` NoEndpoints
src/Roboservant/Types/ReifiedApi.hs+128−2
@@ -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 #-}
@@ -21,7 +26,12 @@ import Data.List.NonEmpty (NonEmpty)
2126import Data.Typeable (Typeable)
2227import GHC.Generics ((:*:)(..))
2328import Roboservant.Types.Internal
24−
29+import Roboservant.Types.Breakdown
30+import Roboservant.Types.BuildFrom
31+import Data.Kind
32+import Servant
33+import Servant.API.Modifiers(FoldRequired,FoldLenient)
34+import GHC.TypeLits (Symbol)
2535import qualified Data.Text as T
2636import qualified Data.Vinyl as V
2737import qualified Data.Vinyl.Curry as V
@@ -42,7 +52,14 @@ data ReifiedEndpoint = forall as. (V.RecordToList as, V.RMap as) => ReifiedEndpo
4252 , reEndpointFunc :: V.Curried as (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
4353 }
4454
45−type ReifiedApi = [(ApiOffset, ReifiedEndpoint )]
55+class ( V.RecordToList (EndpointArgs endpoint)
56+ , V.RMap (EndpointArgs endpoint)
57+ ) => ToReifiedEndpoint (endpoint :: *) where
58+ type EndpointArgs endpoint :: [Type]
59+ type EndpointRes endpoint :: Type
60+
61+ reifiedEndpointArguments :: V.Rec (TypedF Argument) (EndpointArgs endpoint)
62+
4663
4764tagType :: Typeable a => f a -> TypedF f a
4865tagType = (R.typeRep :*:)
@@ -50,3 +67,112 @@ tagType = (R.typeRep :*:)
5067newtype InteractionError = InteractionError T.Text
5168 deriving Show
5269instance Exception InteractionError
70+
71+
72+
73+
74+instance
75+ (Typeable responseType, Breakdown responseType) =>
76+ ToReifiedEndpoint (Verb method statusCode contentTypes responseType)
77+ where
78+ type EndpointArgs (Verb method statusCode contentTypes responseType) = '[]
79+ type EndpointRes (Verb method statusCode contentTypes responseType) = responseType
80+ reifiedEndpointArguments = V.RNil
81+
82+instance
83+ (ToReifiedEndpoint endpoint) =>
84+ ToReifiedEndpoint ((x :: Symbol) :> endpoint)
85+ where
86+ type EndpointArgs ((x :: Symbol) :> endpoint) = EndpointArgs endpoint
87+ type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
88+ reifiedEndpointArguments = reifiedEndpointArguments @endpoint
89+
90+instance
91+ (ToReifiedEndpoint endpoint) =>
92+ ToReifiedEndpoint (Description s :> endpoint)
93+ where
94+ type EndpointArgs (Description s :> endpoint) = EndpointArgs endpoint
95+ type EndpointRes (Description s :> endpoint) = EndpointRes endpoint
96+ reifiedEndpointArguments = reifiedEndpointArguments @endpoint
97+
98+instance
99+ (ToReifiedEndpoint endpoint) =>
100+ ToReifiedEndpoint (Summary s :> endpoint)
101+ where
102+ type EndpointArgs (Summary s :> endpoint) = EndpointArgs endpoint
103+ type EndpointRes (Summary s :> endpoint) = EndpointRes endpoint
104+ reifiedEndpointArguments = reifiedEndpointArguments @endpoint
105+
106+instance
107+ (Typeable requestType
108+ ,BuildFrom requestType
109+ ,ToReifiedEndpoint endpoint) =>
110+ ToReifiedEndpoint (QueryFlag name :> endpoint)
111+ where
112+ type EndpointArgs (QueryFlag name :> endpoint) = Bool ': EndpointArgs endpoint
113+ type EndpointRes (QueryFlag name :> endpoint) = EndpointRes endpoint
114+ reifiedEndpointArguments = tagType (Argument (buildFrom @Bool)) V.:& reifiedEndpointArguments @endpoint
115+
116+type IfLenient s mods t = If (FoldLenient mods) (Either s t) t
117+type IfRequired mods t = If (FoldRequired mods) t (Maybe t)
118+type IfRequiredLenient s mods t = IfRequired mods (IfLenient s mods t)
119+
120+instance
121+ ( BuildFrom (IfRequiredLenient T.Text mods paramType)
122+ , ToReifiedEndpoint endpoint
123+ ) =>
124+ ToReifiedEndpoint (QueryParam' mods name paramType :> endpoint)
125+ where
126+ type EndpointArgs (QueryParam' mods name paramType :> endpoint) = IfRequiredLenient T.Text mods paramType ': EndpointArgs endpoint
127+ type EndpointRes (QueryParam' mods name paramType :> endpoint) = EndpointRes endpoint
128+ reifiedEndpointArguments =
129+ tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
130+ V.:& reifiedEndpointArguments @endpoint
131+
132+instance
133+ ( BuildFrom (IfRequiredLenient T.Text mods headerType)
134+ , ToReifiedEndpoint endpoint
135+ ) =>
136+ ToReifiedEndpoint (Header' mods headerName headerType :> endpoint)
137+ where
138+ type EndpointArgs (Header' mods headerName headerType :> endpoint) = IfRequiredLenient T.Text mods headerType ': EndpointArgs endpoint
139+ type EndpointRes (Header' mods headerName headerType :> endpoint) = EndpointRes endpoint
140+ reifiedEndpointArguments =
141+ tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods headerType)))
142+ V.:& reifiedEndpointArguments @endpoint
143+
144+#if MIN_VERSION_servant(0,17,0)
145+instance
146+ ( BuildFrom (IfLenient String mods captureType)
147+ , ToReifiedEndpoint endpoint) =>
148+ ToReifiedEndpoint (Capture' mods name captureType :> endpoint)
149+ where
150+ type EndpointArgs (Capture' mods name captureType :> endpoint) = IfLenient String mods captureType ': EndpointArgs endpoint
151+ type EndpointRes (Capture' mods name captureType :> endpoint) = EndpointRes endpoint
152+ reifiedEndpointArguments =
153+ tagType (Argument (buildFrom @(IfLenient String mods captureType)))
154+ V.:& reifiedEndpointArguments @endpoint
155+#else
156+instance
157+ ( BuildFrom captureType
158+ , ToReifiedEndpoint endpoint) =>
159+ ToReifiedEndpoint (Capture' mods name captureType :> endpoint)
160+ where
161+ type EndpointArgs (Capture' mods name captureType :> endpoint) = captureType ': EndpointArgs endpoint
162+ type EndpointRes (Capture' mods name captureType :> endpoint) = EndpointRes endpoint
163+ reifiedEndpointArguments =
164+ tagType (Argument (buildFrom @(captureType)))
165+ V.:& reifiedEndpointArguments @endpoint
166+
167+#endif
168+
169+instance
170+ ( BuildFrom (IfLenient String mods requestType)
171+ , ToReifiedEndpoint endpoint) =>
172+ ToReifiedEndpoint (ReqBody' mods contentTypes requestType :> endpoint)
173+ where
174+ type EndpointArgs (ReqBody' mods contentTypes requestType :> endpoint) = IfLenient String mods requestType ': EndpointArgs endpoint
175+ type EndpointRes (ReqBody' mods contentTypes requestType :> endpoint) = EndpointRes endpoint
176+ reifiedEndpointArguments =
177+ tagType (Argument (buildFrom @(IfLenient String mods requestType)))
178+ V.:& reifiedEndpointArguments @endpoint
src/Roboservant/Types/ReifiedApi/Server.hs+34−123
@@ -17,47 +17,34 @@
1717{-# LANGUAGE DataKinds #-}
1818{-# LANGUAGE TypeOperators #-}
1919{-# LANGUAGE FlexibleInstances #-}
20−module Roboservant.Types.ReifiedApi.Server where
20+{-# LANGUAGE MultiParamTypeClasses #-}
21+module Roboservant.Types.ReifiedApi.Server(module Roboservant.Types.ReifiedApi.Server) where
2122
2223import Servant
23−import Servant.API.Modifiers(FoldRequired,FoldLenient)
2424
2525import Control.Monad.Except (runExceptT)
2626import Data.Bifunctor
2727import Data.Dynamic (Dynamic)
28−import Data.Kind
2928import Data.List.NonEmpty (NonEmpty)
3029import Data.Typeable (Typeable)
31−import GHC.TypeLits (Symbol)
3230import Roboservant.Types.Breakdown
33−import Roboservant.Types.BuildFrom
34−import Roboservant.Types.FlattenServer
3531import Roboservant.Types.ReifiedApi
3632
3733import qualified Data.Text as T
38−import qualified Data.Vinyl as V
3934import qualified Data.Vinyl.Curry as V
4035import Data.Hashable(Hashable)
4136
37+type ReifiedApi = [(ApiOffset, ReifiedEndpoint )]
4238
43−class ToReifiedApi (endpoints ) where
44− toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
45−
46−class ( V.RecordToList (EndpointArgs endpoint)
47− , V.RMap (EndpointArgs endpoint)
48− ) => ToReifiedEndpoint (endpoint :: *) where
49− type EndpointArgs endpoint :: [Type]
50− type EndpointRes endpoint :: Type
5139
52− reifiedEndpointArguments :: V.Rec (TypedF Argument) (EndpointArgs endpoint)
40+class ToReifiedApi endpoints where
41+ toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
5342
5443instance ToReifiedApi '[] where
5544 toReifiedApi NoEndpoints _ = []
5645
5746instance
58− ( Typeable (EndpointRes endpoint)
59−
60− , NormalizeFunction (ServerT endpoint Handler)
47+ ( NormalizeFunction (ServerT endpoint Handler)
6148 , Normal (ServerT endpoint Handler) ~ V.Curried (EndpointArgs endpoint) (IO (Either InteractionError (NonEmpty (Dynamic,Int))))
6249 , ToReifiedEndpoint endpoint
6350 , ToReifiedApi endpoints
@@ -73,13 +60,6 @@ instance
7360 : (map . first) (+1)
7461 (toReifiedApi endpoints (Proxy @endpoints))
7562
76−class NormalizeFunction m where
77− type Normal m
78− normalize :: m -> Normal m
79−
80−instance NormalizeFunction x => NormalizeFunction (r -> x) where
81− type Normal (r -> x) = r -> Normal x
82− normalize = fmap normalize
8363
8464instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x) where
8565 type Normal (Handler x) = IO (Either InteractionError (NonEmpty (Dynamic,Int)))
@@ -91,114 +71,45 @@ instance (Typeable x, Hashable x, Breakdown x) => NormalizeFunction (Handler x)
9171 renderServerError = InteractionError . T.pack . show
9272 Right x -> pure $ Right $ breakdown x
9373
94−instance
95− (Typeable responseType, Breakdown responseType) =>
96− ToReifiedEndpoint (Verb method statusCode contentTypes responseType)
97− where
98− type EndpointArgs (Verb method statusCode contentTypes responseType) = '[]
99− type EndpointRes (Verb method statusCode contentTypes responseType) = responseType
100− reifiedEndpointArguments = V.RNil
101−
102−instance
103− (ToReifiedEndpoint endpoint) =>
104− ToReifiedEndpoint ((x :: Symbol) :> endpoint)
105− where
106− type EndpointArgs ((x :: Symbol) :> endpoint) = EndpointArgs endpoint
107− type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
108− reifiedEndpointArguments = reifiedEndpointArguments @endpoint
109−
110−instance
111− (ToReifiedEndpoint endpoint) =>
112− ToReifiedEndpoint (Description s :> endpoint)
113− where
114− type EndpointArgs (Description s :> endpoint) = EndpointArgs endpoint
115− type EndpointRes (Description s :> endpoint) = EndpointRes endpoint
116− reifiedEndpointArguments = reifiedEndpointArguments @endpoint
11774
118−instance
119− (ToReifiedEndpoint endpoint) =>
120− ToReifiedEndpoint (Summary s :> endpoint)
121− where
122− type EndpointArgs (Summary s :> endpoint) = EndpointArgs endpoint
123− type EndpointRes (Summary s :> endpoint) = EndpointRes endpoint
124− reifiedEndpointArguments = reifiedEndpointArguments @endpoint
75+-- case errHTTPCode serverError of
76+-- 500 -> throw serverError
77+-- _ ->
78+-- liftIO . logInfo . show $ ("ignoring non-500 error", serverError)
12579
126−instance
127− (Typeable requestType
128− ,BuildFrom requestType
129− ,ToReifiedEndpoint endpoint) =>
130− ToReifiedEndpoint (QueryFlag name :> endpoint)
131− where
132− type EndpointArgs (QueryFlag name :> endpoint) = Bool ': EndpointArgs endpoint
133− type EndpointRes (QueryFlag name :> endpoint) = EndpointRes endpoint
134− reifiedEndpointArguments = tagType (Argument (buildFrom @Bool)) V.:& reifiedEndpointArguments @endpoint
13580
136−type IfLenient s mods t = If (FoldLenient mods) (Either s t) t
137−type IfRequired mods t = If (FoldRequired mods) t (Maybe t)
138−type IfRequiredLenient s mods t = IfRequired mods (IfLenient s mods t)
81+data Bundled endpoints where
82+ -- AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
83+ AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
84+ NoEndpoints :: Bundled '[]
13985
140−instance
141− ( BuildFrom (IfRequiredLenient T.Text mods paramType)
142− , ToReifiedEndpoint endpoint
143− ) =>
144− ToReifiedEndpoint (QueryParam' mods name paramType :> endpoint)
145− where
146− type EndpointArgs (QueryParam' mods name paramType :> endpoint) = IfRequiredLenient T.Text mods paramType ': EndpointArgs endpoint
147− type EndpointRes (QueryParam' mods name paramType :> endpoint) = EndpointRes endpoint
148− reifiedEndpointArguments =
149− tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
150− V.:& reifiedEndpointArguments @endpoint
86+class FlattenServer api where
87+ flattenServer :: Server api -> Bundled (Endpoints api)
15188
15289instance
153− ( BuildFrom (IfRequiredLenient T.Text mods headerType)
154− , ToReifiedEndpoint endpoint
90+ ( FlattenServer api,
91+ Endpoints endpoint ~ '[endpoint]
15592 ) =>
156− ToReifiedEndpoint (Header' mods headerName headerType :> endpoint)
157− where
158− type EndpointArgs (Header' mods headerName headerType :> endpoint) = IfRequiredLenient T.Text mods headerType ': EndpointArgs endpoint
159− type EndpointRes (Header' mods headerName headerType :> endpoint) = EndpointRes endpoint
160− reifiedEndpointArguments =
161− tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods headerType)))
162− V.:& reifiedEndpointArguments @endpoint
163−
164−#if MIN_VERSION_servant(0,17,0)
165−instance
166− ( BuildFrom (IfLenient String mods captureType)
167− , ToReifiedEndpoint endpoint) =>
168− ToReifiedEndpoint (Capture' mods name captureType :> endpoint)
93+ FlattenServer (endpoint :<|> api)
16994 where
170− type EndpointArgs (Capture' mods name captureType :> endpoint) = IfLenient String mods captureType ': EndpointArgs endpoint
171− type EndpointRes (Capture' mods name captureType :> endpoint) = EndpointRes endpoint
172− reifiedEndpointArguments =
173− tagType (Argument (buildFrom @(IfLenient String mods captureType)))
174− V.:& reifiedEndpointArguments @endpoint
175−#else
95+ flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
96+
17697instance
177− ( BuildFrom captureType
178− , ToReifiedEndpoint endpoint) =>
179− ToReifiedEndpoint (Capture' mods name captureType :> endpoint)
98+ (
99+ Endpoints api ~ '[api]
100+ ) =>
101+ FlattenServer (x :> api)
180102 where
181− type EndpointArgs (Capture' mods name captureType :> endpoint) = captureType ': EndpointArgs endpoint
182− type EndpointRes (Capture' mods name captureType :> endpoint) = EndpointRes endpoint
183− reifiedEndpointArguments =
184− tagType (Argument (buildFrom @(captureType)))
185− V.:& reifiedEndpointArguments @endpoint
186−
187−#endif
103+ flattenServer server = server `AnEndpoint` NoEndpoints
188104
189−instance
190− ( BuildFrom (IfLenient String mods requestType)
191− , ToReifiedEndpoint endpoint) =>
192− ToReifiedEndpoint (ReqBody' mods contentTypes requestType :> endpoint)
105+instance FlattenServer (Verb method statusCode contentTypes responseType)
193106 where
194− type EndpointArgs (ReqBody' mods contentTypes requestType :> endpoint) = IfLenient String mods requestType ': EndpointArgs endpoint
195− type EndpointRes (ReqBody' mods contentTypes requestType :> endpoint) = EndpointRes endpoint
196− reifiedEndpointArguments =
197− tagType (Argument (buildFrom @(IfLenient String mods requestType)))
198− V.:& reifiedEndpointArguments @endpoint
107+ flattenServer server = server `AnEndpoint` NoEndpoints
199108
109+class NormalizeFunction m where
110+ type Normal m
111+ normalize :: m -> Normal m
200112
201−-- case errHTTPCode serverError of
202−-- 500 -> throw serverError
203−-- _ ->
204−-- liftIO . logInfo . show $ ("ignoring non-500 error", serverError)
113+instance NormalizeFunction x => NormalizeFunction (r -> x) where
114+ type Normal (r -> x) = r -> Normal x
115+ normalize = fmap normalize
test/Spec.hs+80−56
@@ -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)
@@ -16,100 +22,118 @@ import qualified Headers
1622import qualified Nested
1723import qualified Post
1824import qualified Product
19−import qualified Roboservant as RS
25+import qualified Roboservant as R
2026import qualified Roboservant.Server as RS
27+import qualified Roboservant.Client as RC
2128import qualified Seeded
2229import Test.Hspec
2330import Test.Hspec.Core.Spec (FailureReason (Reason), ResultStatus (Failure, Success), itemExample, mapSpecItem_, resultStatus)
2431import qualified Valid
32+import Servant(Server,Proxy(..), serve)
33+import Servant.Client(ClientEnv, mkClientEnv, baseUrlPort, parseBaseUrl,HasClient,ClientM)
34+import Network.Wai(Application)
35+import qualified Network.Wai.Handler.Warp as Warp
36+import Network.HTTP.Client (newManager, defaultManagerSettings)
37+import Servant(Endpoints,HasServer)
2538
2639main :: IO ()
2740main = hspec spec
2841
42+fuzzBoth
43+ :: forall a .
44+ (R.ToReifiedApi (Endpoints a), HasServer a '[], RS.FlattenServer a, RC.ToReifiedClientApi (Endpoints a), RC.FlattenClient a,
45+ HasClient ClientM a)
46+ => String -> Server a -> R.Config -> (Maybe R.Report -> IO ()) -> Spec
47+fuzzBoth name server config condition = do
48+ it (name <> " via server") $ do
49+ RS.fuzz @a server config >>= condition
50+
51+ around (withServer (serve (Proxy :: Proxy a) server)) $ do
52+ it (name <> " via client") $ \(clientEnv::ClientEnv) -> do
53+ RC.fuzz @a clientEnv config >>= condition
54+
55+withServer :: Application -> ActionWith ClientEnv -> IO ()
56+withServer app action = Warp.testWithApplication (pure app) (\p -> genClientEnv p >>= action)
57+ where genClientEnv port = do
58+ baseUrl <- parseBaseUrl "http://localhost"
59+ manager <- newManager defaultManagerSettings
60+ pure $ mkClientEnv manager (baseUrl { baseUrlPort = port })
61+
2962spec :: Spec
3063spec = do
3164 describe "Basic usage" $ do
3265 describe "noError" $ do
33− it "finds no error in a valid app" $ do
34− RS.fuzz @Valid.Api Valid.server RS.defaultConfig
35− >>= (`shouldSatisfy` isNothing)
36− it "finds no error in a valid generic app" $ do
37− RS.fuzz @Valid.RoutedApi Valid.routedServer RS.defaultConfig
38− >>= (`shouldSatisfy` isNothing)
39− it "does fail coverage check" $ do
40− r <- RS.fuzz @Valid.Api Valid.server RS.defaultConfig {RS.coverageThreshold = 0.6}
41− fmap (RS.failureReason . RS.rsException) r
42− `shouldSatisfy` ( \case
43− Just (RS.InsufficientCoverage _) -> True
44− _ -> False
45− )
66+ fuzzBoth @Valid.Api "find no error in a basic app" Valid.server R.defaultConfig (`shouldSatisfy` isNothing)
67+ fuzzBoth @Valid.RoutedApi "finds no error in a valid generic app" Valid.routedServer R.defaultConfig (`shouldSatisfy` isNothing)
68+ fuzzBoth @Valid.Api "fails coverage check" Valid.server R.defaultConfig {R.coverageThreshold = 0.6}
69+ (\r ->
70+ fmap (R.failureReason . R.rsException) r
71+ `shouldSatisfy` ( \case
72+ Just (R.InsufficientCoverage _) -> True
73+ _ -> False
74+ ))
4675 describe "posted body" $ do
47− it "passes a coverage check using a posted body" $ do
48− RS.fuzz @Post.Api Post.server RS.defaultConfig {RS.coverageThreshold = 0.99}
49− >>= (`shouldSatisfy` isNothing)
76+ fuzzBoth @Post.Api "passes a coverage check using a posted body" Post.server R.defaultConfig {R.coverageThreshold = 0.99}
77+ (`shouldSatisfy` isNothing)
78+
5079 describe "seeded" $ do
51− shouldFail
52− $ it "finds an error using information passed in"
53− $ let res = Seeded.Seed 1
54− in RS.fuzz @Seeded.Api Seeded.server (RS.defaultConfig {RS.seed = [(toDyn res, hash res)]})
55− >>= (`shouldSatisfy` isNothing)
80+ let res = Seeded.Seed 1
81+ shouldFail $ fuzzBoth @Seeded.Api "finds an error using information passed in" Seeded.server
82+ (R.defaultConfig {R.seed = [(toDyn res, hash res)]})
83+ (`shouldSatisfy` isNothing)
84+
5685 describe "Foo" $ do
57− it "finds an error in a basic app" $
58− RS.fuzz @Foo.Api Foo.server RS.defaultConfig
59− >>= (`shouldSatisfy` serverFailure)
86+ fuzzBoth @Foo.Api "finds an error in a basic app" Foo.server R.defaultConfig (`shouldSatisfy` serverFailure)
87+
6088 describe "BuildFrom" $ do
6189 describe "headers (and sum types)" $ do
62− it "should find a failure that's dependent on using header info" $ do
63− RS.fuzz @Headers.Api Headers.server RS.defaultConfig
64− >>= (`shouldSatisfy` serverFailure)
90+ fuzzBoth @Headers.Api "should find a failure that's dependent on using header info" Headers.server R.defaultConfig
91+ (`shouldSatisfy` serverFailure)
6592 describe "product types" $ do
66− it "should find a failure that's dependent on creating a product" $ do
67− RS.fuzz @Product.Api Product.server RS.defaultConfig {RS.seed = [RS.hashedDyn 'a', RS.hashedDyn (1 :: Int)]}
68− >>= (`shouldSatisfy` serverFailure)
93+ fuzzBoth @Product.Api "should find a failure that's dependent on creating a product" Product.server
94+ R.defaultConfig {R.seed = [R.hashedDyn 'a', R.hashedDyn (1 :: Int)]}
95+ (`shouldSatisfy` serverFailure)
6996 describe "Breakdown" $ do
70− it "handles products" $ do
71− RS.fuzz @Breakdown.ProductApi Breakdown.productServer RS.defaultConfig
72− >>= (`shouldSatisfy` serverFailure)
73− it "handles sums" $ do
74− RS.fuzz @Breakdown.SumApi Breakdown.sumServer RS.defaultConfig
75− >>= (`shouldSatisfy` serverFailure)
97+ fuzzBoth @Breakdown.ProductApi "handles products" Breakdown.productServer R.defaultConfig
98+ (`shouldSatisfy` serverFailure)
99+ fuzzBoth @Breakdown.SumApi "handles sums" Breakdown.sumServer R.defaultConfig
100+ (`shouldSatisfy` serverFailure)
76101 describe "flattening" $ do
77102 -- we don't actually do much here, this is just here to document the appropriate response
78103 -- if you get a type error with a nested api.
79− it "can handle nested apis" $ do
80− RS.fuzz @(Nested.FlatApi) Nested.server RS.defaultConfig {RS.coverageThreshold = 0.99}
81− >>= (`shouldSatisfy` isNothing)
104+ fuzzBoth @Nested.FlatApi "can handle nested apis" Nested.server R.defaultConfig {R.coverageThreshold = 0.99}
105+ (`shouldSatisfy` isNothing)
82106
83−serverFailure :: Maybe RS.Report -> Bool
107+serverFailure :: Maybe R.Report -> Bool
84108serverFailure = \case
85− Just RS.Report {..} ->
86− let RS.RoboservantException {..} = rsException
87− in failureReason /= RS.NoPossibleMoves
109+ Just R.Report {..} ->
110+ let R.RoboservantException {..} = rsException
111+ in failureReason /= R.NoPossibleMoves
88112 _ -> False
89113
90−deriving via (RS.Atom Foo.Foo) instance RS.Breakdown Foo.Foo
114+deriving via (R.Atom Foo.Foo) instance R.Breakdown Foo.Foo
91115
92−deriving via (RS.Atom Foo.Foo) instance RS.BuildFrom Foo.Foo
116+deriving via (R.Atom Foo.Foo) instance R.BuildFrom Foo.Foo
93117
94−deriving via (RS.Atom Headers.Foo) instance RS.Breakdown Headers.Foo
118+deriving via (R.Atom Headers.Foo) instance R.Breakdown Headers.Foo
95119
96−deriving via (RS.Atom Headers.Foo) instance RS.BuildFrom Headers.Foo
120+deriving via (R.Atom Headers.Foo) instance R.BuildFrom Headers.Foo
97121
98−deriving via (RS.Atom Seeded.Seed) instance RS.Breakdown Seeded.Seed
122+deriving via (R.Atom Seeded.Seed) instance R.Breakdown Seeded.Seed
99123
100−deriving via (RS.Atom Seeded.Seed) instance RS.BuildFrom Seeded.Seed
124+deriving via (R.Atom Seeded.Seed) instance R.BuildFrom Seeded.Seed
101125
102−deriving via (RS.Atom Void) instance RS.BuildFrom Void
126+deriving via (R.Atom Void) instance R.BuildFrom Void
103127
104−deriving via (RS.Atom Post.FooPost) instance RS.Breakdown Post.FooPost
128+deriving via (R.Atom Post.FooPost) instance R.Breakdown Post.FooPost
105129
106−deriving via (RS.Atom Post.FooPost) instance RS.BuildFrom Post.FooPost
130+deriving via (R.Atom Post.FooPost) instance R.BuildFrom Post.FooPost
107131
108−deriving via (RS.Compound Breakdown.Foo) instance RS.Breakdown Breakdown.Foo
132+deriving via (R.Compound Breakdown.Foo) instance R.Breakdown Breakdown.Foo
109133
110−deriving via (RS.Compound Product.Foo) instance RS.BuildFrom Product.Foo
134+deriving via (R.Compound Product.Foo) instance R.BuildFrom Product.Foo
111135
112−deriving via (RS.Compound Breakdown.SomeSum) instance RS.Breakdown Breakdown.SomeSum
136+deriving via (R.Compound Breakdown.SomeSum) instance R.Breakdown Breakdown.SomeSum
113137
114138-- | `shouldFail` allows you to assert that a given `Spec` should contain at least one failing test.
115139-- this is often useful when testing tests.