on master help sign in
polishing interface for 1.0 release
commit4d4858effe0baab008d805580be1d13c36c1f7f9 authorMark Wotton <mwotton@gmail.com> dateSun 3 Jan 2021, 11:38 (6 years ago) parenta96a0ff
21 files changed+300−283
EXAMPLE.md+3−23 roboservant.cabal+3−1 src/Roboservant.hs+5−2 src/Roboservant/Direct.hs+34−37 src/Roboservant/Types.hs+8−7 src/Roboservant/Types/Breakdown.hs+16−12 src/Roboservant/Types/BuildFrom.hs+40−41 src/Roboservant/Types/Config.hs+29 src/Roboservant/Types/FlattenServer.hs+5−7 src/Roboservant/Types/Internal.hs+24−20 stack.yaml+1−1 test/Breakdown.hs+10−10 test/Foo.hs+3−1 test/Headers.hs+4−3 test/Nested.hs+4−6 test/Post.hs+11−7 test/Product.hs+3−1 test/Seeded.hs+9−5 test/Spec.hs+61−75 test/UnsafeIO.hs+10−12 test/Valid.hs+17−12
EXAMPLE.md+3−23
@@ -45,35 +45,15 @@ server introduce = introduce :<|> combine :<|> eliminate
4545 | otherwise = pure ()
4646```
4747
48−In the test file, we first define the configuration:
49−
50−```haskell
51−defaultConfig :: Config
52−defaultConfig = Config {
53− -- you can pass extra values in using the seed argument. This can be useful
54− -- for things that might not be produceable within the api, like auth tokens.
55− seed = [hashedDyn "blah"]
56− , maxRuntime = 0.5
57− -- if we get to 1000 interactions with the api, call it quits.
58− , maxReps = 1000
59− -- if you're using this inside quickcheck or hedgehog, you might want to set this
60− -- from their seed to make sure it stays deterministic
61− , rngSeed = 0
62− -- 0 to 100: fail tests if we hit less than this percentage of endpoints.
63− , coverageThreshold = 0
64− }
65−
66−```
67−
68−and the tests: the faulty server should fail and the good server should pass.
48+In the test file, we first define the tests: the faulty server should fail and the good server should pass.
6949
7050```haskell
7151spec = describe "example" $ do
7252 it "good server should not fail" $ do
73− fuzz @Api goodServer defaultConfig { coverageThreshold = 0.99 } (pure ())
53+ fuzz @Api goodServer defaultConfig { coverageThreshold = 0.99 }
7454 >>= (`shouldSatisfy` isNothing)
7555 it "bad server should fail" $ do
76− fuzz @Api badServer defaultConfig { coverageThreshold = 0.99 } (pure ())
56+ fuzz @Api badServer defaultConfig { coverageThreshold = 0.99 }
7757 >>= (`shouldSatisfy` serverFailure)
7858```
7959
roboservant.cabal+3−1
@@ -4,7 +4,7 @@ cabal-version: 1.12
44--
55-- see: https://github.com/sol/hpack
66--
7−-- hash: 1a4c7ba230ea0cde1208ffe68cfb5433046265dea94f292c81d73644caa87cd6
7+-- hash: 3eb8db6b6899ba904be853bb6865aba4f208077587e89f5377df3169743034b0
88
99name: roboservant
1010version: 0.1.0.2
@@ -32,6 +32,8 @@ library
3232 Roboservant.Types
3333 Roboservant.Types.Breakdown
3434 Roboservant.Types.BuildFrom
35+ Roboservant.Types.Config
36+ Roboservant.Types.Config.Internal
3537 Roboservant.Types.FlattenServer
3638 Roboservant.Types.Internal
3739 Roboservant.Types.ReifiedApi
src/Roboservant.hs+5−2
@@ -1,5 +1,8 @@
1−module Roboservant (module Roboservant.Direct
2− ,module Roboservant.Types) where
1+module Roboservant
2+ ( module Roboservant.Direct,
3+ module Roboservant.Types,
4+ )
5+where
36
47import Roboservant.Direct
58import Roboservant.Types
src/Roboservant/Direct.hs+34−37
@@ -2,6 +2,7 @@
22{-# LANGUAGE FlexibleContexts #-}
33{-# LANGUAGE GADTs #-}
44{-# LANGUAGE LambdaCase #-}
5+{-# LANGUAGE PolyKinds #-}
56{-# LANGUAGE RankNTypes #-}
67{-# LANGUAGE RecordWildCards #-}
78{-# LANGUAGE ScopedTypeVariables #-}
@@ -9,7 +10,6 @@
910{-# LANGUAGE TypeApplications #-}
1011{-# LANGUAGE TypeFamilies #-}
1112{-# LANGUAGE UndecidableInstances #-}
12−{-# LANGUAGE PolyKinds #-}
1313
1414module Roboservant.Direct
1515 ( fuzz,
@@ -25,25 +25,30 @@ module Roboservant.Direct
2525where
2626
2727import Control.Exception.Lifted
28− ( SomeException,
29− throw,
30− Exception,
31− SomeAsyncException,
32− catch,
33− catches,
34− handle,
35− Handler(Handler) )
28+ ( Exception,
29+ Handler (Handler),
30+ SomeAsyncException,
31+ SomeException,
32+ catch,
33+ catches,
34+ handle,
35+ throw,
36+ )
3637import Control.Monad.State.Strict
37− ( MonadIO(..), StateT(runStateT), modify', MonadState(get) )
38+ ( MonadIO (..),
39+ MonadState (get),
40+ StateT (runStateT),
41+ modify',
42+ )
3843import Control.Monad.Trans.Control (MonadBaseControl)
3944import qualified Data.Dependent.Map as DM
40−import Data.Dynamic ( Dynamic(..) )
45+import Data.Dynamic (Dynamic (..))
4146import qualified Data.IntSet as IntSet
4247import Data.List.NonEmpty (NonEmpty (..))
4348import qualified Data.List.NonEmpty as NEL
4449import Data.Maybe (mapMaybe)
4550import qualified Data.Set as Set
46−import Data.Time.Clock ( addUTCTime, getCurrentTime, UTCTime )
51+import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
4752import qualified Data.Vinyl as V
4853import qualified Data.Vinyl.Curry as V
4954import qualified Data.Vinyl.Functor as V
@@ -59,8 +64,9 @@ import Roboservant.Types
5964 ToReifiedApi (..),
6065 TypedF,
6166 )
67+import Roboservant.Types.Config
6268import Servant (Endpoints, Proxy (Proxy), Server, ServerError (..))
63−import System.Random ( mkStdGen, Random(randomR), StdGen )
69+import System.Random (Random (randomR), StdGen, mkStdGen)
6470import qualified Type.Reflection as R
6571
6672data RoboservantException
@@ -87,15 +93,6 @@ data FuzzOp
8793 }
8894 deriving (Show, Eq)
8995
90−data Config
91− = Config
92− { seed :: [(Dynamic, Int)],
93− maxRuntime :: Double, -- seconds to test for
94− maxReps :: Integer,
95− rngSeed :: Int,
96− coverageThreshold :: Double
97− }
98−
9996data FuzzState
10097 = FuzzState
10198 { path :: [FuzzOp],
@@ -129,9 +126,8 @@ fuzz ::
129126 (FlattenServer api, ToReifiedApi (Endpoints api)) =>
130127 Server api ->
131128 Config ->
132− IO () ->
133129 IO (Maybe Report)
134−fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
130+fuzz server Config {..} = handle (pure . Just . formatException) $ do
135131 let path = []
136132 stash = addToStash seed mempty
137133 currentRng = mkStdGen rngSeed
@@ -140,7 +136,7 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
140136 runStateT
141137 (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get))
142138 FuzzState {..}
143− print stopreason
139+ logInfo $ show stopreason
144140 pure Nothing
145141 where
146142 -- something less terrible later
@@ -150,12 +146,14 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
150146 (unlines [show failureType, show exception])
151147 r
152148 displayDiagnostics FuzzState {..} = liftIO $ do
153− putStrLn "api endpoints covered"
154− mapM_ print (Set.toList $ Set.fromList $ map apiOffset path)
155− putStrLn ""
156− putStrLn "types in stash"
157− DM.forWithKey_ (getStash stash) $ \_k v ->
158− print (NEL.length $ getStashValue v)
149+ logInfo $ unlines $
150+ ["api endpoints covered"]
151+ <> (map show . Set.toList . Set.fromList $ map apiOffset path)
152+ <> ["", "types in stash"]
153+ <> DM.foldrWithKey (\_ v r -> (show . NEL.length . getStashValue $ v) : r) [] (getStash stash)
154+ -- <> (map (show . NEL.length . getStashValue ) $ DM.assocs (getStash stash))
155+ -- $ \_k v ->
156+ -- (show . NEL.length $ getStashValue v))
159157
160158 evaluateCoverage f@FuzzState {..}
161159 | coverage > coverageThreshold = pure ()
@@ -237,23 +235,22 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
237235 V.Rec (TypedF V.Identity) as ->
238236 m ()
239237 execute fuzzop func args = do
240− (liftIO . print . (fuzzop,) . stash) =<< get
238+ (liftIO . logInfo . show . (fuzzop,) . stash) =<< get
241239 liftIO (V.runcurry' func argVals) >>= \case
242240 -- parameterise this
243241 Left (serverError :: ServerError) ->
244242 case errHTTPCode serverError of
245243 500 -> throw serverError
246244 _ ->
247− liftIO $ print ("ignoring non-500 error", serverError)
245+ liftIO . logInfo . show $ ("ignoring non-500 error", serverError)
248246 Right (dyn :: NEL.NonEmpty (Dynamic, Int)) -> do
249− -- liftIO $ print ("storing", fmap dynTypeRep dyn)
250247 modify'
251248 ( \fs@FuzzState {..} ->
252249 fs {stash = addToStash (NEL.toList dyn) stash}
253250 )
254251 where
255252 argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
256− -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
253+ -- argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
257254 go ::
258255 (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m) =>
259256 m ()
@@ -263,12 +260,12 @@ fuzz server Config {..} checker = handle (pure . Just . formatException) $ do
263260 [ Handler (\(e :: SomeAsyncException) -> throw e),
264261 Handler
265262 ( \(e :: SomeException) -> do
266− displayDiagnostics =<< get
263+ -- displayDiagnostics =<< get
267264 throw . RoboservantException ServerCrashed (Just e) =<< get
268265 )
269266 ]
270267 catch
271− (liftIO checker)
268+ (liftIO healthCheck)
272269 (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) =<< get)
273270
274271addToStash ::
src/Roboservant/Types.hs+8−7
@@ -8,17 +8,18 @@
88{-# LANGUAGE UndecidableInstances #-}
99
1010module Roboservant.Types
11− ( module Roboservant.Types.Breakdown
12− , module Roboservant.Types.BuildFrom
13− , module Roboservant.Types.FlattenServer
14− , module Roboservant.Types.ReifiedApi
15− , module Roboservant.Types.Internal
16−
11+ ( module Roboservant.Types.Breakdown,
12+ module Roboservant.Types.BuildFrom,
13+ module Roboservant.Types.FlattenServer,
14+ module Roboservant.Types.ReifiedApi,
15+ module Roboservant.Types.Internal,
16+ module Roboservant.Types.Config,
1717 )
1818where
1919
2020import Roboservant.Types.Breakdown
2121import Roboservant.Types.BuildFrom
22+import Roboservant.Types.Config
2223import Roboservant.Types.FlattenServer
23−import Roboservant.Types.ReifiedApi
2424import Roboservant.Types.Internal
25+import Roboservant.Types.ReifiedApi
src/Roboservant/Types/Breakdown.hs+16−12
@@ -15,35 +15,39 @@
1515module Roboservant.Types.Breakdown where
1616
1717import Data.Dynamic (Dynamic, toDyn)
18−import Data.List.NonEmpty (NonEmpty (..))
19−import GHC.Generics
20−import Data.Typeable (Typeable)
21−import Roboservant.Types.Internal
2218import Data.Hashable
2319import Data.Kind
20+import Data.List.NonEmpty (NonEmpty (..))
2421import qualified Data.List.NonEmpty as NEL
22+import Data.Typeable (Typeable)
23+import GHC.Generics
24+import Roboservant.Types.Internal
2525
26−breakdown :: (Hashable x, Typeable x, Breakdown x)
27− => x -> NonEmpty (Dynamic, Int)
26+breakdown ::
27+ (Hashable x, Typeable x, Breakdown x) =>
28+ x ->
29+ NonEmpty (Dynamic, Int)
2830breakdown x = hashedDyn x :| breakdownExtras x
2931
3032class Breakdown x where
31− breakdownExtras :: x -> [(Dynamic,Int)]
33+ breakdownExtras :: x -> [(Dynamic, Int)]
3234
3335instance (Hashable x, Typeable x) => Breakdown (Atom x) where
3436 breakdownExtras _ = []
3537
3638deriving via (Atom ()) instance Breakdown ()
39+
3740deriving via (Atom Int) instance Breakdown Int
41+
3842deriving via (Atom [Char]) instance Breakdown [Char]
3943
40−class GBreakdown (f :: k -> *) where
41− gBreakdownExtras :: f a -> [(Dynamic,Int)]
44+class GBreakdown (f :: k -> *) where
45+ gBreakdownExtras :: f a -> [(Dynamic, Int)]
4246
43−instance (Hashable x, Typeable x, Generic x, GBreakdown (Rep x)) => Breakdown (Compound (x::Type)) where
47+instance (Hashable x, Typeable x, Generic x, GBreakdown (Rep x)) => Breakdown (Compound (x :: Type)) where
4448 breakdownExtras = gBreakdownExtras . from . unCompound
4549
46−instance GBreakdown f => GBreakdown (M1 S c f ) where
50+instance GBreakdown f => GBreakdown (M1 S c f) where
4751 gBreakdownExtras (M1 f) = gBreakdownExtras f
4852
4953instance GBreakdown b => GBreakdown (M1 D a b) where
@@ -60,5 +64,5 @@ instance (GBreakdown a, GBreakdown b) => GBreakdown (a :+: b) where
6064 L1 a -> gBreakdownExtras a
6165 R1 a -> gBreakdownExtras a
6266
63−instance (Hashable a, Typeable a, Breakdown a) => GBreakdown (K1 R a) where
67+instance (Hashable a, Typeable a, Breakdown a) => GBreakdown (K1 R a) where
6468 gBreakdownExtras (K1 c) = NEL.toList $ breakdown c
src/Roboservant/Types/BuildFrom.hs+40−41
@@ -1,55 +1,52 @@
1−{-# LANGUAGE ScopedTypeVariables #-}
2−{-# LANGUAGE TypeFamilies #-}
3−{-# LANGUAGE StandaloneDeriving #-}
1+{-# LANGUAGE AllowAmbiguousTypes #-}
42{-# LANGUAGE DerivingVia #-}
5−{-# LANGUAGE TypeApplications #-}
63{-# LANGUAGE FlexibleContexts #-}
7−{-# LANGUAGE UndecidableInstances #-}
4+{-# LANGUAGE FlexibleInstances #-}
85{-# LANGUAGE PolyKinds #-}
6+{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE StandaloneDeriving #-}
8+{-# LANGUAGE TypeApplications #-}
9+{-# LANGUAGE TypeFamilies #-}
910{-# LANGUAGE TypeOperators #-}
10−{-# LANGUAGE FlexibleInstances #-}
11−{-# LANGUAGE AllowAmbiguousTypes #-}
11+{-# LANGUAGE UndecidableInstances #-}
12+
1213module Roboservant.Types.BuildFrom where
1314
14−import Control.Monad(filterM)
15−import Data.List.NonEmpty (NonEmpty(..))
16−import qualified Data.List.NonEmpty as NEL
17−import Data.Typeable (Typeable)
15+import Control.Monad (filterM)
1816import qualified Data.Dependent.Map as DM
19−import qualified Type.Reflection as R
20−import Data.Kind
21−import Roboservant.Types.Internal
2217import Data.Hashable
2318import qualified Data.IntSet as IntSet
19+import Data.Kind
20+import Data.List.NonEmpty (NonEmpty (..))
21+import qualified Data.List.NonEmpty as NEL
22+import Data.Typeable (Typeable)
2423import GHC.Generics
24+import Roboservant.Types.Internal
25+import qualified Type.Reflection as R
2526
26−buildFrom :: forall x . (Hashable x, BuildFrom x, Typeable x) => Stash -> Maybe (StashValue x)
27+buildFrom :: forall x. (Hashable x, BuildFrom x, Typeable x) => Stash -> Maybe (StashValue x)
2728buildFrom = buildStash . buildFrom'
2829 where
2930 buildStash :: [([Provenance], x)] -> Maybe (StashValue x)
30− buildStash = fmap (foldr1 addStash . fmap promoteToStash) . NEL.nonEmpty
31−
31+ buildStash = fmap (foldr1 addStash . fmap promoteToStash) . NEL.nonEmpty
3232 promoteToStash :: ([Provenance], x) -> StashValue x
33− promoteToStash (p,x) = StashValue (pure (p,x))
34− (IntSet.singleton (hash x))
35−
36− -- | sorta fiddly. looks like (<>), but in general it's not safe to add arbitrary StashValues, because
37− -- there's no guarantee that they came from the same run, which would invalidate all the provenance stuff.
38− -- in this context it's safe because we have no access to anything else.
39− --
40− -- we _do_ have to check here that the new elements are not already contained, however.
33+ promoteToStash (p, x) =
34+ StashValue
35+ (pure (p, x))
36+ (IntSet.singleton (hash x))
4137 addStash :: StashValue x -> StashValue x -> StashValue x
42− addStash old (StashValue newVal _)
43− = let insertableVals = NEL.filter ((`IntSet.notMember` stashHash old) . hash) newVal
44− in StashValue ( addListToNE (getStashValue old) insertableVals )
45− (IntSet.union (IntSet.fromList . map hash . fmap snd . NEL.toList $ newVal) (stashHash old))
46−
38+ addStash old (StashValue newVal _) =
39+ let insertableVals = NEL.filter ((`IntSet.notMember` stashHash old) . hash) newVal
40+ in StashValue
41+ (addListToNE (getStashValue old) insertableVals)
42+ (IntSet.union (IntSet.fromList . map hash . fmap snd . NEL.toList $ newVal) (stashHash old))
4743 addListToNE :: NonEmpty a -> [a] -> NonEmpty a
4844 addListToNE ne l = NEL.fromList (NEL.toList ne <> l)
4945
50−buildFrom' :: forall x . (Hashable x, BuildFrom x, Typeable x) => Stash -> [([Provenance], x)]
51−buildFrom' stash = maybe [] (NEL.toList . getStashValue) (DM.lookup R.typeRep (getStash stash))
52− <> extras stash
46+buildFrom' :: forall x. (Hashable x, BuildFrom x, Typeable x) => Stash -> [([Provenance], x)]
47+buildFrom' stash =
48+ maybe [] (NEL.toList . getStashValue) (DM.lookup R.typeRep (getStash stash))
49+ <> extras stash
5350
5451class (Hashable x, Typeable x) => BuildFrom (x :: Type) where
5552 extras :: Stash -> [([Provenance], x)]
@@ -64,17 +61,18 @@ deriving via (Compound (Maybe x)) instance (Typeable x, Hashable x, BuildFrom x)
6461-- this isn't wonderful, but we need a hand-rolled instance for recursive datatypes right now.
6562-- with an arbitrary-ish interface, we could use a size parameter, rng access etc.
6663instance (BuildFrom x) => BuildFrom [x] where
67− extras stash = map (\xs -> (concatMap fst xs,map snd xs)) $ powerset $ extras @x stash
64+ extras stash = map (\xs -> (concatMap fst xs, map snd xs)) $ powerset $ extras @x stash
6865 where
6966 powerset xs = filterM (const [True, False]) xs
7067
71−instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x::Type)) where
68+instance (Hashable x, Typeable x, Generic x, GBuildFrom (Rep x)) => BuildFrom (Compound (x :: Type)) where
7269 extras stash = fmap (Compound . to) <$> gExtras stash
7370
7471deriving via (Atom Int) instance BuildFrom Int
72+
7573deriving via (Atom Char) instance BuildFrom Char
7674
77−class GBuildFrom (f :: k -> *) where
75+class GBuildFrom (f :: k -> *) where
7876 gExtras :: Stash -> [([Provenance], f a)]
7977
8078instance GBuildFrom b => GBuildFrom (M1 D a b) where
@@ -82,14 +80,15 @@ instance GBuildFrom b => GBuildFrom (M1 D a b) where
8280
8381-- not recursion safe!
8482instance (GBuildFrom a, GBuildFrom b) => GBuildFrom (a :+: b) where
85− gExtras stash = (fmap L1 <$> gExtras stash)
86− <> (fmap R1 <$> gExtras stash)
83+ gExtras stash =
84+ (fmap L1 <$> gExtras stash)
85+ <> (fmap R1 <$> gExtras stash)
8786
8887instance (GBuildFrom a, GBuildFrom b) => GBuildFrom (a :*: b) where
89− gExtras stash = [ (pa<>pb, a' :*: b') | (pa,a') <- gExtras stash , (pb,b') <- gExtras stash]
88+ gExtras stash = [(pa <> pb, a' :*: b') | (pa, a') <- gExtras stash, (pb, b') <- gExtras stash]
9089
9190instance GBuildFrom b => GBuildFrom (M1 C a b) where
92− gExtras =fmap (fmap M1) . gExtras
91+ gExtras = fmap (fmap M1) . gExtras
9392
9493instance GBuildFrom b => GBuildFrom (M1 S a b) where
9594 gExtras = fmap (fmap M1) . gExtras
@@ -98,4 +97,4 @@ instance BuildFrom a => GBuildFrom (K1 i a) where
9897 gExtras = fmap (fmap K1) . buildFrom'
9998
10099instance GBuildFrom U1 where
101− gExtras _ = [([],U1)]
100+ gExtras _ = [([], U1)]
src/Roboservant/Types/Config.hsnew file+29
@@ -0,0 +1,29 @@
1+module Roboservant.Types.Config where
2+
3+import Data.Dynamic
4+
5+data Config
6+ = Config
7+ { seed :: [(Dynamic, Int)],
8+ maxRuntime :: Double, -- seconds to test for
9+ maxReps :: Integer,
10+ rngSeed :: Int,
11+ coverageThreshold :: Double,
12+ logInfo :: String -> IO (),
13+ healthCheck :: IO ()
14+ }
15+
16+defaultConfig :: Config
17+defaultConfig =
18+ Config
19+ { seed = [],
20+ maxRuntime = 0.5,
21+ maxReps = 1000,
22+ rngSeed = 0,
23+ coverageThreshold = 0,
24+ logInfo = const (pure ()),
25+ healthCheck = pure ()
26+ }
27+
28+noisyConfig :: Config
29+noisyConfig = defaultConfig {logInfo = print}
src/Roboservant/Types/FlattenServer.hs+5−7
@@ -6,8 +6,8 @@
66{-# LANGUAGE TypeApplications #-}
77{-# LANGUAGE TypeOperators #-}
88{-# LANGUAGE UndecidableInstances #-}
9−-- | terrible name, this really just pulls stuff out where we can fiddle with it.
109
10+-- | terrible name, this really just pulls stuff out where we can fiddle with it.
1111module Roboservant.Types.FlattenServer where
1212
1313import Servant
@@ -19,10 +19,9 @@ data Bundled endpoints where
1919class FlattenServer api where
2020 flattenServer :: Server api -> Bundled (Endpoints api)
2121
22−
2322instance
24− ( FlattenServer api
25− , Endpoints endpoint ~ '[endpoint]
23+ ( FlattenServer api,
24+ Endpoints endpoint ~ '[endpoint]
2625 ) =>
2726 FlattenServer (endpoint :<|> api)
2827 where
@@ -37,9 +36,8 @@ instance
3736 flattenServer server = server `AnEndpoint` NoEndpoints
3837
3938instance
40− ( Endpoints (Verb method statusCode contentTypes responseType) ~ '[Verb method statusCode contentTypes responseType],
41− HasServer (Verb method statusCode contentTypes responseType) '[]
42−
39+ ( Endpoints (Verb method statusCode contentTypes responseType) ~ '[Verb method statusCode contentTypes responseType],
40+ HasServer (Verb method statusCode contentTypes responseType) '[]
4341 ) =>
4442 FlattenServer (Verb method statusCode contentTypes responseType)
4543 where
src/Roboservant/Types/Internal.hs+24−20
@@ -1,49 +1,53 @@
11{-# LANGUAGE DeriveFunctor #-}
2−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
32{-# LANGUAGE DeriveGeneric #-}
43{-# LANGUAGE DerivingStrategies #-}
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
55
66module Roboservant.Types.Internal where
77
8−import Data.List.NonEmpty (NonEmpty(..))
9−import qualified Data.Map.Strict as Map
108import qualified Data.Dependent.Map as DM
119import Data.Dependent.Map (DMap)
12−import qualified Type.Reflection as R
1310import Data.Dependent.Sum
14−import Data.IntSet(IntSet)
15−import Data.Hashable(Hashable,hash)
16−import GHC.Generics(Generic)
17−import Data.Typeable(Typeable)
18−import Data.Dynamic(Dynamic,toDyn)
11+import Data.Dynamic (Dynamic, toDyn)
12+import Data.Hashable (Hashable, hash)
13+import Data.IntSet (IntSet)
14+import Data.List.NonEmpty (NonEmpty (..))
15+import qualified Data.Map.Strict as Map
16+import Data.Typeable (Typeable)
17+import GHC.Generics (Generic)
18+import qualified Type.Reflection as R
1919
2020data Provenance
2121 = Provenance R.SomeTypeRep Int
22− deriving (Show,Eq,Generic)
22+ deriving (Show, Eq, Generic)
2323
2424instance Hashable Provenance
2525
26−data StashValue a = StashValue { getStashValue :: NonEmpty ([Provenance], a)
27− , stashHash :: IntSet
28− }
26+data StashValue a
27+ = StashValue
28+ { getStashValue :: NonEmpty ([Provenance], a),
29+ stashHash :: IntSet
30+ }
2931 deriving (Functor, Show)
3032
3133-- wrap in newtype to give a custom Show instance, since the normal
3234-- instance for DMap is not happy since StashValue needs Show a to show
33−newtype Stash = Stash { getStash :: DMap R.TypeRep StashValue }
35+newtype Stash = Stash {getStash :: DMap R.TypeRep StashValue}
3436 deriving (Semigroup, Monoid)
3537
3638instance Show Stash where
37− showsPrec i (Stash x) = showsPrec i $
38− Map.fromList . map (\(tr :=> StashValue vs _) -> (R.SomeTypeRep tr, fmap fst vs)) $ DM.toList x
39+ showsPrec i (Stash x) =
40+ showsPrec i
41+ $ Map.fromList . map (\(tr :=> StashValue vs _) -> (R.SomeTypeRep tr, fmap fst vs))
42+ $ DM.toList x
3943
4044-- | Can't be built up from parts, can't be broken down further.
41−newtype Atom x = Atom { unAtom :: x }
42− deriving newtype (Hashable,Typeable)
45+newtype Atom x = Atom {unAtom :: x}
46+ deriving newtype (Hashable, Typeable)
4347
4448-- | can be broken down and built up from generic pieces
45−newtype Compound x = Compound { unCompound :: x }
46− deriving newtype (Hashable,Typeable)
49+newtype Compound x = Compound {unCompound :: x}
50+ deriving newtype (Hashable, Typeable)
4751
4852hashedDyn :: (Hashable a, Typeable a) => a -> (Dynamic, Int)
4953hashedDyn a = (toDyn a, hash a)
stack.yaml+1−1
@@ -1,6 +1,6 @@
11resolver: lts-16.27
22# bit faster on ReifiedApi stuff which is unfortunately terribly slow.
3−compiler: ghc-8.10.3
3+# compiler: ghc-8.10.3
44packages:
55- .
66
test/Breakdown.hs+10−10
@@ -8,29 +8,32 @@
88module Breakdown where
99
1010import Data.Aeson
11+import Data.Hashable
1112import Data.Typeable (Typeable)
1213import GHC.Generics (Generic)
1314import Servant
14−import Data.Hashable
1515
1616data Foo = Foo Int String
1717 deriving (Generic, Eq, Show, Typeable)
1818
1919instance Hashable Foo
20+
2021instance ToJSON Foo
21−instance FromJSON Foo
2222
23+instance FromJSON Foo
2324
2425data SomeSum = A Int | B String
2526 deriving (Generic, Eq, Show, Typeable)
2627
2728instance Hashable SomeSum
29+
2830instance ToJSON SomeSum
29−instance FromJSON SomeSum
3031
32+instance FromJSON SomeSum
3133
32−type ProductApi = "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
33− :<|> "getFoo" :> Get '[JSON] Foo
34+type ProductApi =
35+ "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
36+ :<|> "getFoo" :> Get '[JSON] Foo
3437
3538eliminate :: Int -> Handler ()
3639eliminate _ = throwError $ err500 {errBody = "eliminate blew up, oh no!"}
@@ -38,13 +41,10 @@ eliminate _ = throwError $ err500 {errBody = "eliminate blew up, oh no!"}
3841productServer :: Server ProductApi
3942productServer = eliminate :<|> pure (Foo 12 "abc")
4043
41−
42−type SumApi
43− = "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
44+type SumApi =
45+ "item" :> ReqBody '[JSON] Int :> Post '[JSON] ()
4446 :<|> "getFoo1" :> Get '[JSON] SomeSum
4547 :<|> "getFoo2" :> Get '[JSON] SomeSum
4648
47−
48−
4949sumServer :: Server SumApi
5050sumServer = eliminate :<|> pure (B "hi") :<|> pure (A 3)
test/Foo.hs+3−1
@@ -8,17 +8,19 @@
88module Foo where
99
1010import Data.Aeson
11+import Data.Hashable
1112import Data.Typeable (Typeable)
1213import GHC.Generics (Generic)
1314import Servant
14−import Data.Hashable
1515
1616newtype Foo = Foo Int
1717 deriving (Generic, Eq, Show, Typeable)
1818 deriving newtype (FromHttpApiData, ToHttpApiData)
1919
2020instance Hashable Foo
21+
2122instance ToJSON Foo
23+
2224instance FromJSON Foo
2325
2426type Api =
test/Headers.hs+4−3
@@ -8,17 +8,19 @@
88module Headers where
99
1010import Data.Aeson
11+import Data.Hashable
1112import Data.Typeable (Typeable)
1213import GHC.Generics (Generic)
1314import Servant
14−import Data.Hashable
1515
1616newtype Foo = Foo Int
1717 deriving (Generic, Eq, Show, Typeable)
1818 deriving newtype (FromHttpApiData, ToHttpApiData)
1919
2020instance Hashable Foo
21+
2122instance ToJSON Foo
23+
2224instance FromJSON Foo
2325
2426type Api =
@@ -33,8 +35,7 @@ combine :: Maybe Foo -> Maybe Foo -> Handler Foo
3335combine (Just (Foo a)) (Just (Foo b)) = pure (Foo (a + b))
3436combine (Just a) Nothing = pure a
3537combine Nothing (Just a) = pure a
36−combine Nothing Nothing = pure (Foo 1)
37−
38+combine Nothing Nothing = pure (Foo 1)
3839
3940eliminate :: Foo -> Handler ()
4041eliminate (Foo a)
test/Nested.hs+4−6
@@ -10,14 +10,12 @@ module Nested where
1010import Servant
1111import Servant.API.Flatten
1212
13−
1413type Api =
15− ("one" :> Post '[JSON] Int
16− :<|> "two" :> Post '[JSON] Int
17− )
18− :<|> (
19− "three" :> Post '[JSON] Int
14+ ( "one" :> Post '[JSON] Int
15+ :<|> "two" :> Post '[JSON] Int
2016 )
17+ :<|> ( "three" :> Post '[JSON] Int
18+ )
2119
2220type FlatApi = Flat Api
2321
test/Post.hs+11−7
@@ -7,21 +7,25 @@
77
88module Post where
99
10−import GHC.Generics (Generic)
11−import Servant
1210import Data.Aeson
1311import Data.Hashable
12+import GHC.Generics (Generic)
13+import Servant
1414
15−type Api = Get '[JSON] FooPost
16− :<|> ReqBody '[JSON] FooPost :> Post '[JSON] ()
15+type Api =
16+ Get '[JSON] FooPost
17+ :<|> ReqBody '[JSON] FooPost :> Post '[JSON] ()
1718
1819data FooPost = FooPost
19− deriving (Eq,Show,Generic)
20+ deriving (Eq, Show, Generic)
2021
2122instance Hashable FooPost
23+
2224instance ToJSON FooPost
25+
2326instance FromJSON FooPost
2427
2528server :: Server Api
26−server = pure FooPost
27− :<|> const (pure ())
29+server =
30+ pure FooPost
31+ :<|> const (pure ())
test/Product.hs+3−1
@@ -8,16 +8,18 @@
88module Product where
99
1010import Data.Aeson
11+import Data.Hashable
1112import Data.Typeable (Typeable)
1213import GHC.Generics (Generic)
1314import Servant
14−import Data.Hashable
1515
1616data Foo = Foo Int String
1717 deriving (Generic, Eq, Show, Typeable)
1818
1919instance Hashable Foo
20+
2021instance ToJSON Foo
22+
2123instance FromJSON Foo
2224
2325type Api = "item" :> ReqBody '[JSON] Foo :> Post '[JSON] ()
test/Seeded.hs+9−5
@@ -8,22 +8,26 @@
88module Seeded where
99
1010import Data.Aeson
11+import Data.Hashable
1112import Data.Typeable (Typeable)
1213import GHC.Generics (Generic)
1314import Servant
14−import Data.Hashable
1515
1616newtype Seed = Seed Int
1717 deriving (Generic, Eq, Show, Typeable)
1818 deriving newtype (FromHttpApiData, ToHttpApiData)
1919
2020instance ToJSON Seed
21+
2122instance FromJSON Seed
23+
2224instance Hashable Seed
2325
24−type Api = Capture "seed" Seed :> Get '[JSON] ()
25− :<|> Get '[JSON] ()
26+type Api =
27+ Capture "seed" Seed :> Get '[JSON] ()
28+ :<|> Get '[JSON] ()
2629
2730server :: Server Api
28−server = (\(Seed _) -> error "we blow up if we get here")
29− :<|> pure ()
31+server =
32+ (\(Seed _) -> error "we blow up if we get here")
33+ :<|> pure ()
test/Spec.hs+61−75
@@ -1,144 +1,130 @@
1+{-# LANGUAGE DerivingVia #-}
2+{-# LANGUAGE LambdaCase #-}
13{-# LANGUAGE OverloadedStrings #-}
2−{-# LANGUAGE StandaloneDeriving #-}
34{-# LANGUAGE RecordWildCards #-}
5+{-# LANGUAGE StandaloneDeriving #-}
46{-# LANGUAGE TypeApplications #-}
5−{-# LANGUAGE LambdaCase #-}
6−{-# LANGUAGE DerivingVia #-}
7−
87{-# OPTIONS_GHC -fno-warn-orphans #-}
8+
9+import qualified Breakdown
10+import Data.Dynamic (toDyn)
11+import Data.Hashable (Hashable (hash))
12+import Data.Maybe (isNothing)
13+import Data.Void (Void)
914import qualified Foo
10−import qualified Seeded
11−import qualified Valid
1215import qualified Headers
16+import qualified Nested
1317import qualified Post
1418import qualified Product
15−import qualified Breakdown
16−import qualified Nested
17−
18−import Test.Hspec.Core.Spec(ResultStatus(Failure,Success),resultStatus,itemExample,FailureReason(Reason),mapSpecItem_)
19−import Data.Dynamic(toDyn)
2019import qualified Roboservant as RS
20+import qualified Seeded
2121import Test.Hspec
22−import Data.Void ( Void )
23−import Data.Maybe ( isNothing )
24−import Data.Hashable ( Hashable(hash) )
22+import Test.Hspec.Core.Spec (FailureReason (Reason), ResultStatus (Failure, Success), itemExample, mapSpecItem_, resultStatus)
23+import qualified Valid
2524
2625main :: IO ()
2726main = hspec spec
2827
29−noCheck :: IO ()
30−noCheck = pure ()
31−
32−defaultConfig :: RS.Config
33−defaultConfig = RS.Config
34− { RS.seed = []
35− , RS.maxRuntime = 0.5
36− , RS.maxReps = 1000
37− , RS.rngSeed = 0
38− , RS.coverageThreshold = 0
39− }
40−
4128spec :: Spec
4229spec = do
4330 describe "Basic usage" $ do
4431 describe "noError" $ do
4532 it "finds no error in a valid app" $ do
46− RS.fuzz @Valid.Api Valid.server defaultConfig noCheck
33+ RS.fuzz @Valid.Api Valid.server RS.defaultConfig
4734 >>= (`shouldSatisfy` isNothing)
4835 it "finds no error in a valid generic app" $ do
49− RS.fuzz @Valid.RoutedApi Valid.routedServer defaultConfig noCheck
36+ RS.fuzz @Valid.RoutedApi Valid.routedServer RS.defaultConfig
5037 >>= (`shouldSatisfy` isNothing)
51−
5238 it "does fail coverage check" $ do
53− r <- RS.fuzz @Valid.Api Valid.server defaultConfig { RS.coverageThreshold = 0.6 } noCheck
54− fmap (RS.failureReason . RS.rsException) r `shouldSatisfy`
55− (\case
56− Just (RS.InsufficientCoverage _) -> True
57− _ -> False)
58−
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+ )
5945 describe "posted body" $ do
6046 it "passes a coverage check using a posted body" $ do
61− RS.fuzz @Post.Api Post.server defaultConfig { RS.coverageThreshold = 0.99 } noCheck
47+ RS.fuzz @Post.Api Post.server RS.defaultConfig {RS.coverageThreshold = 0.99}
6248 >>= (`shouldSatisfy` isNothing)
63−
6449 describe "seeded" $ do
65− shouldFail $
66− it "finds an error using information passed in" $
67− let res = Seeded.Seed 1 in
68− RS.fuzz @Seeded.Api Seeded.server (defaultConfig{ RS.seed = [(toDyn res,hash res) ] }) noCheck
69− >>= (`shouldSatisfy` isNothing)
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)
7055 describe "Foo" $ do
7156 it "finds an error in a basic app" $
72− RS.fuzz @Foo.Api Foo.server defaultConfig noCheck
57+ RS.fuzz @Foo.Api Foo.server RS.defaultConfig
7358 >>= (`shouldSatisfy` serverFailure)
74−
7559 describe "BuildFrom" $ do
7660 describe "headers (and sum types)" $ do
7761 it "should find a failure that's dependent on using header info" $ do
78− RS.fuzz @Headers.Api Headers.server defaultConfig noCheck
62+ RS.fuzz @Headers.Api Headers.server RS.defaultConfig
7963 >>= (`shouldSatisfy` serverFailure)
80−
8164 describe "product types" $ do
8265 it "should find a failure that's dependent on creating a product" $ do
83− RS.fuzz @Product.Api Product.server defaultConfig { RS.seed = [RS.hashedDyn 'a', RS.hashedDyn (1::Int)]} noCheck
66+ RS.fuzz @Product.Api Product.server RS.defaultConfig {RS.seed = [RS.hashedDyn 'a', RS.hashedDyn (1 :: Int)]}
8467 >>= (`shouldSatisfy` serverFailure)
85−
8668 describe "Breakdown" $ do
8769 it "handles products" $ do
88− RS.fuzz @Breakdown.ProductApi Breakdown.productServer defaultConfig noCheck
70+ RS.fuzz @Breakdown.ProductApi Breakdown.productServer RS.defaultConfig
8971 >>= (`shouldSatisfy` serverFailure)
90−
9172 it "handles sums" $ do
92− RS.fuzz @Breakdown.SumApi Breakdown.sumServer defaultConfig noCheck
73+ RS.fuzz @Breakdown.SumApi Breakdown.sumServer RS.defaultConfig
9374 >>= (`shouldSatisfy` serverFailure)
94−
9575 describe "flattening" $ do
96− -- | we don't actually do much here, this is just here to document the appropriate response
97− -- if you get a type error with a nested api.
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.
9878 it "can handle nested apis" $ do
99− RS.fuzz @(Nested.FlatApi) Nested.server defaultConfig { RS.coverageThreshold = 0.99 } noCheck
79+ RS.fuzz @(Nested.FlatApi) Nested.server RS.defaultConfig {RS.coverageThreshold = 0.99}
10080 >>= (`shouldSatisfy` isNothing)
10181
102−
103−
10482serverFailure :: Maybe RS.Report -> Bool
10583serverFailure = \case
106− Just RS.Report{..} ->
107− let RS.RoboservantException{..} = rsException
108− in failureReason /= RS.NoPossibleMoves
84+ Just RS.Report {..} ->
85+ let RS.RoboservantException {..} = rsException
86+ in failureReason /= RS.NoPossibleMoves
10987 _ -> False
11088
111−
11289deriving via (RS.Atom Foo.Foo) instance RS.Breakdown Foo.Foo
90+
11391deriving via (RS.Atom Foo.Foo) instance RS.BuildFrom Foo.Foo
11492
11593deriving via (RS.Atom Headers.Foo) instance RS.Breakdown Headers.Foo
94+
11695deriving via (RS.Atom Headers.Foo) instance RS.BuildFrom Headers.Foo
11796
11897deriving via (RS.Atom Seeded.Seed) instance RS.Breakdown Seeded.Seed
119−deriving via (RS.Atom Seeded.Seed) instance RS.BuildFrom Seeded.Seed
120−
12198
122−-- instance RS.BuildFrom Seeded.Seed
99+deriving via (RS.Atom Seeded.Seed) instance RS.BuildFrom Seeded.Seed
123100
124101deriving via (RS.Atom Void) instance RS.BuildFrom Void
125102
126−deriving via (RS.Atom Post.FooPost) instance RS.BuildFrom Post.FooPost
127103deriving via (RS.Atom Post.FooPost) instance RS.Breakdown Post.FooPost
128104
129−deriving via (RS.Compound Product.Foo) instance RS.BuildFrom Product.Foo
105+deriving via (RS.Atom Post.FooPost) instance RS.BuildFrom Post.FooPost
106+
130107deriving via (RS.Compound Breakdown.Foo) instance RS.Breakdown Breakdown.Foo
108+
109+deriving via (RS.Compound Product.Foo) instance RS.BuildFrom Product.Foo
110+
131111deriving via (RS.Compound Breakdown.SomeSum) instance RS.Breakdown Breakdown.SomeSum
132112
133113-- | `shouldFail` allows you to assert that a given `Spec` should contain at least one failing test.
134114-- this is often useful when testing tests.
135115shouldFail :: SpecWith a -> SpecWith a
136−shouldFail = mapSpecItem_ (\i -> i {
137− itemExample = \p a cb -> do
138− r <- (itemExample i) p a cb
139− pure r {resultStatus = case resultStatus r of
140− Success -> Failure Nothing (Reason "Unexpected success")
141− Failure _ _ -> Success
142− x -> x
143− }
144− })
116+shouldFail =
117+ mapSpecItem_
118+ ( \i ->
119+ i
120+ { itemExample = \p a cb -> do
121+ r <- (itemExample i) p a cb
122+ pure
123+ r
124+ { resultStatus = case resultStatus r of
125+ Success -> Failure Nothing (Reason "Unexpected success")
126+ Failure _ _ -> Success
127+ x -> x
128+ }
129+ }
130+ )
test/UnsafeIO.hs+10−12
@@ -7,16 +7,15 @@
77
88module UnsafeIO where
99
10−import Data.Aeson()
11−import Servant
12−import Data.IORef (writeIORef, IORef, readIORef, newIORef)
13−import Control.Monad.Trans (MonadIO(liftIO))
10+import Control.Monad.Trans (MonadIO (liftIO))
11+import Data.Aeson ()
1412import qualified Data.ByteString.Lazy.Char8 as BL8
15−
13+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
14+import Servant
1615
1716type UnsafeApi =
1817 "add" :> Get '[JSON] ()
19− :<|> "healthcheck" :> Get '[JSON] ()
18+ :<|> "healthcheck" :> Get '[JSON] ()
2019
2120healthcheck :: IORef Int -> Handler ()
2221healthcheck ref = do
@@ -25,17 +24,16 @@ healthcheck ref = do
2524 0 -> pure ()
2625 n -> throwError $ err500 {errBody = "observed inconsistency: " <> (BL8.pack $ show n)}
2726
28−
29−
3027makeServer :: IO (Server UnsafeApi)
3128makeServer = do
3229 ref <- newIORef 0
33− pure $ unsafeMunge ref
34− :<|> healthcheck ref
30+ pure $
31+ unsafeMunge ref
32+ :<|> healthcheck ref
3533
3634unsafeMunge :: IORef Int -> Handler ()
3735unsafeMunge ref = liftIO $ do
3836 t <- readIORef ref
39− writeIORef ref (t+1)
37+ writeIORef ref (t + 1)
4038 t2 <- readIORef ref
41− writeIORef ref (t2-1)
39+ writeIORef ref (t2 -1)
test/Valid.hs+17−12
@@ -7,22 +7,26 @@
77
88module Valid where
99
10−import Servant
1110import Data.Void
1211import GHC.Generics
12+import Servant
1313import Servant.API.Generic
1414import Servant.Server.Generic
1515
16−type Api = Get '[JSON] Int
17− :<|> Capture "void" Void :> Get '[JSON] ()
16+type Api =
17+ Get '[JSON] Int
18+ :<|> Capture "void" Void :> Get '[JSON] ()
1819
1920data Routes route
2021 = Routes
21− { getInt :: route :-
22− Get '[JSON] Int
23− , captureIt :: route :-
24− Capture "void" Void :> Get '[JSON] ()
25− } deriving Generic
22+ { getInt ::
23+ route
24+ :- Get '[JSON] Int,
25+ captureIt ::
26+ route
27+ :- Capture "void" Void :> Get '[JSON] ()
28+ }
29+ deriving (Generic)
2630
2731type RoutedApi = ToServantApi Routes
2832
@@ -31,10 +35,11 @@ routedServer :: Server RoutedApi
3135routedServer = genericServer routes
3236
3337routes :: Routes AsServer
34−routes = Routes
35− { getInt = pure 7
36− , captureIt = const (pure ())
37− }
38+routes =
39+ Routes
40+ { getInt = pure 7,
41+ captureIt = const (pure ())
42+ }
3843
3944server :: Server Api
4045server = pure 7 :<|> const (pure ())