on master help sign in
Merge branch 'typesafe-servant' of https://github.com/mstksg/roboservant
commit1e54b220e8f52397888a11707adc6a815637f6d7 authorMark Wotton <mwotton@gmail.com> dateThu 26 Nov 2020, 14:17 (6 years ago) parentf72ca22d54960d
7 files changed+284−146
package.yaml+4−3 roboservant.cabal+10−3 src/Roboservant/Direct.hs+115−71 src/Roboservant/Types/Breakdown.hs+30−11 src/Roboservant/Types/ReifiedApi.hs+93−58 stack.yaml+4 stack.yaml.lock+28
package.yaml+4−3
@@ -2,8 +2,6 @@ name: roboservant
22version: 0.1.0.2
33github: "mwotton/roboservant"
44license: BSD3
5−author: "Mark Wotton, Samuel Schlesinger"
6−maintainer: "mwotton@gmail.com"
75copyright: "2020 Mark Wotton, Samuel Schlesinger"
86synopsis: Automatic session-aware servant testing
97category: Web
@@ -27,6 +25,10 @@ dependencies:
2725- servant-flatten
2826- servant-server # >= 0.17
2927- string-conversions
28+- vinyl
29+- dependent-sum
30+- dependent-map
31+- text
3032- time
3133
3234ghc-options:
@@ -49,4 +51,3 @@ tests:
4951 - aeson
5052 - hspec
5153 - hspec-core
52−
roboservant.cabal+10−3
@@ -4,7 +4,7 @@ cabal-version: 1.12
44--
55-- see: https://github.com/sol/hpack
66--
7−-- hash: 838fa22fc3ea6673432f485653caaca6c43bccce724777fafbb903dad8ce30cd
7+-- hash: 49f8c03988a25b7e35dddaab65f250955a078392aaa293298885aea164315479
88
99name: roboservant
1010version: 0.1.0.2
@@ -13,8 +13,6 @@ description: Please see the README on GitHub at <https://github.com/mwotton/r
1313category: Web
1414homepage: https://github.com/mwotton/roboservant#readme
1515bug-reports: https://github.com/mwotton/roboservant/issues
16−author: Mark Wotton, Samuel Schlesinger
17−maintainer: mwotton@gmail.com
1816copyright: 2020 Mark Wotton, Samuel Schlesinger
1917license: BSD3
2018license-file: LICENSE
@@ -44,6 +42,8 @@ library
4442 base >=4.7 && <5
4543 , bytestring
4644 , containers
45+ , dependent-map
46+ , dependent-sum
4747 , lifted-base
4848 , monad-control
4949 , mtl
@@ -53,13 +53,16 @@ library
5353 , servant-flatten
5454 , servant-server
5555 , string-conversions
56+ , text
5657 , time
58+ , vinyl
5759 default-language: Haskell2010
5860
5961test-suite roboservant-test
6062 type: exitcode-stdio-1.0
6163 main-is: Spec.hs
6264 other-modules:
65+ BuildFrom
6366 Foo
6467 Headers
6568 Seeded
@@ -74,6 +77,8 @@ test-suite roboservant-test
7477 , base >=4.7 && <5
7578 , bytestring
7679 , containers
80+ , dependent-map
81+ , dependent-sum
7782 , hspec
7883 , hspec-core
7984 , lifted-base
@@ -86,5 +91,7 @@ test-suite roboservant-test
8691 , servant-flatten
8792 , servant-server
8893 , string-conversions
94+ , text
8995 , time
96+ , vinyl
9097 default-language: Haskell2010
src/Roboservant/Direct.hs+115−71
@@ -7,6 +7,7 @@
77{-# LANGUAGE GeneralizedNewtypeDeriving #-}
88{-# LANGUAGE LambdaCase #-}
99{-# LANGUAGE PolyKinds #-}
10+{-# LANGUAGE RankNTypes #-}
1011{-# LANGUAGE RecordWildCards #-}
1112{-# LANGUAGE ScopedTypeVariables #-}
1213{-# LANGUAGE StandaloneDeriving #-}
@@ -20,7 +21,7 @@ module Roboservant.Direct
2021 ( fuzz, Config(..)
2122
2223 -- TODO come up with something smarter than exporting all this, we should
23− -- have some nice error-display functions
24+ -- have some nice error-display functions
2425 , RoboservantException(..), FuzzState(..), FuzzOp(..)
2526 )
2627where
@@ -28,8 +29,13 @@ where
2829import Control.Exception.Lifted(throw,handle,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
2930import Control.Monad(void,replicateM)
3031import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,runStateT)
32+import Control.Exception.Lifted(throw,Handler(..), Exception,SomeException,SomeAsyncException, catch, catches)
33+import Control.Monad(void,replicateM,forM_)
34+import Control.Monad.State.Strict(MonadState,MonadIO,get,modify',liftIO,execStateT)
3135import Control.Monad.Trans.Control(MonadBaseControl)
32−import Data.Dynamic (Dynamic, dynApply, dynTypeRep, fromDynamic)
36+import Data.Dependent.Map (DMap)
37+import Data.Dynamic (Dynamic(..), dynApply, dynTypeRep, fromDynamic)
38+import Data.List.NonEmpty (NonEmpty(..))
3339import Data.Map.Strict(Map)
3440import qualified Data.Set as Set
3541import Data.Maybe (mapMaybe)
@@ -37,9 +43,15 @@ import Data.Typeable (TypeRep)
3743import Servant (Endpoints, Proxy (Proxy), Server, ServerError(..))
3844import System.Random(StdGen,randomR,mkStdGen)
3945import System.Timeout.Lifted(timeout)
46+import GHC.Generics ((:*:)(..))
47+import qualified Data.Vinyl.Functor as V
48+import qualified Data.Vinyl.Curry as V
49+import qualified Data.Dependent.Map as DM
50+import qualified Data.Vinyl as V
4051import qualified Data.List.NonEmpty as NEL
4152import qualified Data.Map.Strict as Map
4253import Data.Time.Clock
54+import qualified Type.Reflection as R
4355
4456import Roboservant.Types.Breakdown
4557import Roboservant.Types
@@ -47,10 +59,14 @@ import Roboservant.Types
4759 FlattenServer (..),
4860-- ReifiedApi,
4961 ToReifiedApi (..),
62+ ReifiedEndpoint(..),
63+ Argument(..),
64+ TypedF
65+
5066 )
5167
5268data RoboservantException
53− = RoboservantException FailureType (Maybe SomeException) Int FuzzState
69+ = RoboservantException FailureType (Maybe SomeException) Int FuzzState
5470 deriving (Show)
5571-- we believe in nussink, lebowski
5672instance Exception RoboservantException
@@ -63,7 +79,7 @@ data FailureType
6379 deriving (Show,Eq)
6480
6581
66−data FuzzOp = FuzzOp
82+data FuzzOp = FuzzOp
6783 { apiOffset :: ApiOffset
6884 , provenance :: [Provenance]
6985 } deriving (Show,Eq)
@@ -82,7 +98,12 @@ data FuzzState = FuzzState
8298 , stash :: Stash
8399 , currentRng :: StdGen
84100 }
85− deriving (Show)
101+ deriving Show
102+
103+data EndpointOption = forall as. (V.RecordToList as, V.RMap as) => EndpointOption
104+ { eoCall :: V.Curried as (IO (Either ServerError (NonEmpty Dynamic)))
105+ , eoArgs :: V.Rec (TypedF StashValue) as
106+ }
86107
87108data StopReason
88109 = TimedOut
@@ -91,7 +112,7 @@ data StopReason
91112
92113data Report = Report
93114 { textual :: String }
94− deriving (Show,Eq)
115+ deriving (Show,Eq)
95116
96117fuzz :: forall api. (FlattenServer api, ToReifiedApi (Endpoints api))
97118 => Server api
@@ -103,16 +124,23 @@ fuzz server Config{..} checker = handle (pure . Just . formatException) $ do
103124 stash = addToStash seed mempty
104125 currentRng = mkStdGen rngSeed
105126
106−
127+
107128 deadline :: UTCTime <- addUTCTime (fromInteger $ maxRuntime * 1000000) <$> getCurrentTime
108129 -- either we time out without finding an error, which is fine, or we find an error
109130 -- and throw an exception that propagates through this.
110−
131+
111132 (stopreason, fs ) <- runStateT
112133 (untilDone (maxReps, deadline) go <* (evaluateCoverage =<< get)) FuzzState{..}
113134 pure Nothing
114135 -- mapM_ (print . (\(offset, (args, dyn) ) -> (offset, map fst args, dyn))) reifiedApi
115136
137+
138+ -- void $ timeout (maxRuntime * 1000000) ( execStateT (replicateM maxReps go) FuzzState{..})
139+ -- forM_ reifiedApi $ \(offset, ReifiedEndpoint{..}) ->
140+ -- print (
141+ -- offset
142+ -- , recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) reArguments
143+ -- )
116144 where
117145 -- something less terrible later
118146 formatException :: RoboservantException -> Report
@@ -126,7 +154,7 @@ fuzz server Config{..} checker = handle (pure . Just . formatException) $ do
126154 | otherwise = throw $ RoboservantException InsufficientCoverage Nothing routeCount f
127155 where hitRoutes = (fromIntegral . Set.size . Set.fromList $ map apiOffset path)
128156 totalRoutes = (fromIntegral routeCount)
129−
157+
130158
131159 untilDone :: MonadIO m => (Integer,UTCTime) -> m a -> m StopReason
132160 untilDone (0,_) _ = pure HitMaxIterations
@@ -137,10 +165,10 @@ fuzz server Config{..} checker = handle (pure . Just . formatException) $ do
137165 else do
138166 action
139167 untilDone (n-1, deadline) action
140−
168+
141169 reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
142170 routeCount = length reifiedApi
143−
171+
144172 elementOrFail :: (MonadState FuzzState m, MonadIO m)
145173 => [a] -> m a
146174 elementOrFail [] = liftIO . throw . RoboservantException NoPossibleMoves Nothing routeCount =<< get
@@ -150,92 +178,108 @@ fuzz server Config{..} checker = handle (pure . Just . formatException) $ do
150178 modify' $ \st' -> st' { currentRng = newGen }
151179 pure (l !! index)
152180
153− genOp :: (MonadState FuzzState m, MonadIO m)
154− => m (FuzzOp, Dynamic, [Dynamic])
155− genOp = do -- fs@FuzzState{..} = do
181+ withOp :: (MonadState FuzzState m, MonadIO m)
182+ => (forall as. (V.RecordToList as, V.RMap as)
183+ => FuzzOp
184+ -> V.Curried as (IO (Either ServerError (NonEmpty Dynamic)))
185+ -> V.Rec (TypedF V.Identity) as -> m r
186+ )
187+ -> m r
188+ withOp callback = do
156189 -- choose a call to make, from the endpoints with fillable arguments.
157− (offset, dynCall, args) <- elementOrFail . options =<< get
158− r <- mapM (elementOrFail . zip [0..] . NEL.toList) args
159− let pathSegment = FuzzOp offset (map (\(index,(_,dyn) ) -> Provenance (dynTypeRep dyn) index) r)
190+ (offset, EndpointOption{..}) <- elementOrFail . options =<< get
191+ r <- V.rtraverse
192+ (\(tr :*: StashValue svs) -> elementOrFail $
193+ zipWith (\i xy -> V.Const i :*: tr :*: xy)
194+ [0..]
195+ (NEL.toList svs)
196+ )
197+ eoArgs
198+ let pathSegment = FuzzOp offset $
199+ recordToList'
200+ (\(V.Const index :*: tr :*: _) -> Provenance (R.SomeTypeRep tr) index)
201+ r
202+ argValues = V.rmap
203+ (\(_ :*: tr :*: (_, x)) -> tr :*: V.Identity x)
204+ r
160205 modify' (\f -> f { path = path f <> [pathSegment] })
161− pure (pathSegment, dynCall, fmap (snd . snd) r)
162−
206+ callback pathSegment eoCall argValues
163207 where
164− options :: FuzzState -> [(ApiOffset, Dynamic, [NEL.NonEmpty ([Provenance], Dynamic)])]
208+ options :: FuzzState -> [(ApiOffset, EndpointOption)]
165209 options FuzzState{..} =
166210 mapMaybe
167− ( \(offset, (argreps, dynCall)) -> (offset,dynCall,) <$> do
168− mapM (\(_tr,bf) -> bf stash ) argreps
211+ ( \(offset, ReifiedEndpoint{..}) -> do
212+ args <- V.rtraverse (\(tr :*: Argument bf) -> (tr :*:) <$> bf stash) reArguments
213+ pure (offset, EndpointOption reEndpointFunc args)
169214 )
170215 reifiedApi
171216
172− execute :: (MonadState FuzzState m, MonadIO m)
173− => (FuzzOp,Dynamic,[Dynamic]) -> m ()
174− execute (fuzzop, dyncall, args) = do
175− -- liftIO $ print fuzzop
176− -- now, magic happens: we apply some dynamic arguments to a dynamic
177− -- function and hopefully something useful pops out the end.
178− let func = foldr (\arg curr -> flip dynApply arg =<< curr) (Just dyncall) (reverse args)
217+ execute :: (MonadState FuzzState m, MonadIO m, V.RecordToList as, V.RMap as)
218+ => FuzzOp
219+ -> V.Curried as (IO (Either ServerError (NonEmpty Dynamic)))
220+ -> V.Rec (TypedF V.Identity) as
221+ -> m ()
222+ execute fuzzop func args = do
223+ liftIO $ print fuzzop
179224 st <- get
180− let showable = unlines $ ("args":map (show . dynTypeRep) args)
225+ let showable = unlines $ ("args":map show argTypes)
181226 <> ["fuzzop"
182227 , show fuzzop
183− ,"dyncall"
184− ,show (dynTypeRep dyncall)
228+ -- ,"dyncall"
229+ -- ,show (dynTypeRep dyncall)
185230 ,"state"
186231 ,show st]
187− -- liftIO $ putStrLn showable
188−
189− case func of
190− Nothing -> error ("all screwed up 1: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
191− Just (f') -> do
192− -- liftIO $ print
193− case fromDynamic f' of
194− Nothing -> error ("all screwed up 2: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
195− Just (f) -> liftIO f >>= \case
196− -- parameterise this
197− Left (serverError :: ServerError) ->
198− case errHTTPCode serverError of
199− 500 -> throw serverError
200− _ -> do
201− liftIO $ print ("ignoring non-500 error" , serverError)
202−
203− Right (dyn :: NEL.NonEmpty Dynamic) -> do
204− -- liftIO $ print ("storing", fmap dynTypeRep dyn)
205− modify' (\fs@FuzzState{..} ->
206− fs { stash = addToStash (NEL.toList dyn) stash } )
207− pure ()
232+ liftIO $ putStrLn showable
233+ liftIO (V.runcurry' func argVals) >>= \case
234+ -- parameterise this
235+ Left (serverError :: ServerError) ->
236+ case errHTTPCode serverError of
237+ 500 -> throw serverError
238+ _ -> do
239+ liftIO $ print ("ignoring non-500 error" , serverError)
240+ Right (dyn :: NEL.NonEmpty Dynamic) -> do
241+ liftIO $ print ("storing", fmap dynTypeRep dyn)
242+ modify' (\fs@FuzzState{..} ->
243+ fs { stash = addToStash (NEL.toList dyn) stash } )
244+ where
245+ argVals = V.rmap (\(_ :*: V.Identity x) -> V.Identity x) args
246+ argTypes = recordToList' (\(tr :*: _) -> R.SomeTypeRep tr) args
208247
209248 go :: (MonadState FuzzState m, MonadIO m, MonadBaseControl IO m)
210249 => m ()
211− go = do
212− op <- genOp
213− catches (execute op)
250+ go = withOp $ \op func args -> do
251+ catches (execute op func args)
214252 [ Handler (\(e :: SomeAsyncException) -> throw e)
215253 , Handler (\(e :: SomeException) -> throw . RoboservantException ServerCrashed (Just e) routeCount =<< get)
216254 ]
217255 catch (liftIO checker)
218256 (\(e :: SomeException) -> throw . RoboservantException CheckerFailed (Just e) routeCount =<< get)
219257
220− -- actions <-
221− -- forAll $ do
222− -- Gen.sequential
223− -- (Range.linear 1 100)
224− -- emptyState
225− -- (fmap preload seed <> [callEndpoint reifiedApi])
226− -- executeSequential emptyState actions
227258addToStash :: [Dynamic]
228− -> Map TypeRep (NEL.NonEmpty ([Provenance], Dynamic))
229− -> Map TypeRep (NEL.NonEmpty ([Provenance], Dynamic))
259+ -> Stash
260+ -> Stash
230261addToStash result stash =
231− foldr (\dyn dict -> let tr = dynTypeRep dyn in
232− Map.insertWith renumber tr (pure ([Provenance tr 0],dyn)) dict) stash result
262+ foldr (\(Dynamic tr x) (Stash dict) -> Stash $
263+ DM.insertWith renumber tr (StashValue (([Provenance (R.SomeTypeRep tr) 0],x):|[])) dict
264+ -- in DM.insertWith renumber _ (pure ([Provenance tr 0],x)) dict
265+ )
266+ stash
267+ result
233268-- Map.insertWith (flip (<>)) (dynTypeRep result) (_pure result) stash })
234269 where
235− renumber :: NEL.NonEmpty ([Provenance],Dynamic)
236− -> NEL.NonEmpty ([Provenance],Dynamic)
237− -> NEL.NonEmpty ([Provenance],Dynamic)
238− renumber singleDyn l = case NEL.toList singleDyn of
270+ renumber :: StashValue a
271+ -> StashValue a
272+ -> StashValue a
273+ renumber (StashValue singleDyn) (StashValue l) = StashValue $ case NEL.toList singleDyn of
239274 [([Provenance tr _], dyn)] -> l
240275 <> pure ([Provenance tr (length (NEL.last l) + 1)], dyn)
241276 _ -> error "should be impossible"
277+
278+
279+-- why isn't this in vinyl?
280+recordToList'
281+ :: (V.RecordToList as, V.RMap as)
282+ => (forall x. f x -> a)
283+ -> V.Rec f as
284+ -> [a]
285+recordToList' f = V.recordToList . V.rmap (V.Const . f)
src/Roboservant/Types/Breakdown.hs+30−11
@@ -1,11 +1,13 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
22{-# LANGUAGE DataKinds #-}
33{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE DeriveFunctor #-}
45{-# LANGUAGE DerivingStrategies #-}
56{-# LANGUAGE FlexibleInstances #-}
67{-# LANGUAGE GADTs #-}
78{-# LANGUAGE GeneralizedNewtypeDeriving #-}
89{-# LANGUAGE LambdaCase #-}
10+{-# LANGUAGE MultiParamTypeClasses #-}
911{-# LANGUAGE PolyKinds #-}
1012{-# LANGUAGE ScopedTypeVariables #-}
1113{-# LANGUAGE StandaloneDeriving #-}
@@ -25,16 +27,32 @@ import Data.Map.Strict (Map)
2527import Data.Maybe (fromMaybe)
2628import Data.Proxy (Proxy (..))
2729import Data.Typeable (TypeRep, Typeable, typeRep)
30+import qualified Data.Dependent.Map as DM
31+import Data.Dependent.Map (DMap)
32+import qualified Type.Reflection as R
33+import Data.Dependent.Sum
34+import Data.Kind
2835
2936data Provenance
30− = Provenance TypeRep Int
37+ = Provenance R.SomeTypeRep Int
3138 deriving (Show,Eq)
32−type Stash = Map TypeRep (NonEmpty ([Provenance], Dynamic))
3339
34−class Typeable x => BuildFrom x where
35− buildFrom :: Stash -> Maybe (NonEmpty ([Provenance],Dynamic))
36− default buildFrom :: Stash -> Maybe (NonEmpty ([Provenance], Dynamic))
37− buildFrom = Map.lookup (typeRep (Proxy @x))
40+newtype StashValue a = StashValue { getStashValue :: NonEmpty ([Provenance], a) }
41+ deriving (Functor, Show)
42+
43+-- wrap in newtype to give a custom Show instance, since the normal
44+-- instance for DMap is not happy since StashValue needs Show a to show
45+newtype Stash = Stash { getStash :: DMap R.TypeRep StashValue }
46+ deriving (Semigroup, Monoid)
47+
48+instance Show Stash where
49+ showsPrec i (Stash x) = showsPrec i $
50+ Map.fromList . map (\(tr :=> StashValue vs) -> (R.SomeTypeRep tr, fmap fst vs)) $ DM.toList x
51+
52+class Typeable x => BuildFrom (x :: Type) where
53+ buildFrom :: Stash -> Maybe (StashValue x)
54+ default buildFrom :: Stash -> Maybe (StashValue x)
55+ buildFrom = DM.lookup R.typeRep . getStash
3856
3957 -- (fmap promisedDyn . NEL.toList) . Map.lookup (typeRep (Proxy @x))
4058
@@ -50,11 +68,12 @@ promisedDyn = fromMaybe (error "internal error, typerep map misconstructed") . f
5068instance BuildFrom Bool
5169
5270instance (Typeable x, BuildFrom x) => BuildFrom (Maybe x) where
53− buildFrom dict = Just $ fmap toDyn <$> options
54− where options :: NonEmpty ([Provenance], Maybe x)
55− options = ([],Nothing) :|
56− (maybe [] NEL.toList . fmap (fmap (fmap (Just . promisedDyn @x)))
57− $ buildFrom @x dict)
71+ buildFrom dict = Just options
72+ where options :: StashValue (Maybe x)
73+ options = StashValue $
74+ ([],Nothing) :|
75+ (maybe [] (NEL.toList . getStashValue . fmap Just) $ buildFrom @x dict
76+ )
5877
5978class Breakdown x where
6079 breakdown :: x -> NonEmpty Dynamic
src/Roboservant/Types/ReifiedApi.hs+93−58
@@ -2,10 +2,13 @@
22{-# LANGUAGE AllowAmbiguousTypes #-}
33{-# LANGUAGE DataKinds #-}
44{-# LANGUAGE DerivingStrategies #-}
5+{-# LANGUAGE FlexibleContexts #-}
56{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GADTs #-}
68{-# LANGUAGE GeneralizedNewtypeDeriving #-}
79{-# LANGUAGE LambdaCase #-}
810{-# LANGUAGE PolyKinds #-}
11+{-# LANGUAGE RecordWildCards #-}
912{-# LANGUAGE ScopedTypeVariables #-}
1013{-# LANGUAGE StandaloneDeriving #-}
1114{-# LANGUAGE TupleSections #-}
@@ -18,45 +21,76 @@ module Roboservant.Types.ReifiedApi where
1821
1922
2023import Control.Monad.Except (runExceptT)
24+import Data.Bifunctor
25+import Data.Dependent.Sum
2126import Data.Dynamic (Dynamic, toDyn)
2227import Data.Function ((&))
28+import Data.Kind
29+import Data.List.NonEmpty (NonEmpty)
2330import Data.Typeable (TypeRep, Typeable, typeRep)
31+import GHC.Generics ((:*:)(..))
2432import GHC.TypeLits (Symbol)
33+import Roboservant.Types.Breakdown
34+import Roboservant.Types.FlattenServer
2535import Servant
2636import Servant.API.Modifiers(FoldRequired,FoldLenient)
27−import Roboservant.Types.FlattenServer
28−import Roboservant.Types.Breakdown
29−import Data.List.NonEmpty (NonEmpty)
37+import qualified Data.Text as T
38+import qualified Data.Vinyl as V
39+import qualified Data.Vinyl.Curry as V
40+import qualified Type.Reflection as R
3041
3142
3243newtype ApiOffset = ApiOffset Int
3344 deriving (Eq, Show, Ord)
3445 deriving newtype (Enum, Num)
3546
36−type ReifiedEndpoint = ([(TypeRep
37− , Stash -> Maybe (NonEmpty ([Provenance], Dynamic)))]
38− , Dynamic)
47+type TypedF = (:*:) R.TypeRep
48+
49+newtype Argument a = Argument
50+ { getArgument :: Stash -> Maybe (StashValue a)
51+ }
52+
53+data ReifiedEndpoint = forall as. (V.RecordToList as, V.RMap as) => ReifiedEndpoint
54+ { reArguments :: V.Rec (TypedF Argument) as
55+ , reEndpointFunc :: V.Curried as (IO (Either ServerError (NonEmpty Dynamic)))
56+ }
3957
4058type ReifiedApi = [(ApiOffset, ReifiedEndpoint)]
4159
60+tagType :: Typeable a => f a -> TypedF f a
61+tagType = (R.typeRep :*:)
4262
4363class ToReifiedApi (endpoints :: [*]) where
4464 toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
4565
46−class ToReifiedEndpoint (endpoint :: *) where
47− toReifiedEndpoint :: Dynamic -> Proxy endpoint -> ReifiedEndpoint
66+class ( V.Curried (EndpointArgs endpoint) (Handler (EndpointRes endpoint)) ~ Server endpoint
67+ , V.RecordToList (EndpointArgs endpoint)
68+ , V.RMap (EndpointArgs endpoint)
69+ ) => ToReifiedEndpoint (endpoint :: *) where
70+ type EndpointArgs endpoint :: [Type]
71+ type EndpointRes endpoint :: Type
72+
73+ reifiedEndpointArguments :: V.Rec (TypedF Argument) (EndpointArgs endpoint)
4874
4975instance ToReifiedApi '[] where
5076 toReifiedApi NoEndpoints _ = []
5177
5278instance
53− (Typeable (Normal (ServerT endpoint Handler)), NormalizeFunction (ServerT endpoint Handler), ToReifiedEndpoint endpoint, ToReifiedApi endpoints, Typeable (ServerT endpoint Handler)) =>
79+ ( Typeable (EndpointRes endpoint)
80+ , NormalizeFunction (ServerT endpoint Handler)
81+ , Normal (ServerT endpoint Handler) ~ V.Curried (EndpointArgs endpoint) (IO (Either ServerError (NonEmpty Dynamic)))
82+ , ToReifiedEndpoint endpoint
83+ , ToReifiedApi endpoints, Typeable (ServerT endpoint Handler)
84+ ) =>
5485 ToReifiedApi (endpoint : endpoints)
5586 where
5687 toReifiedApi (endpoint `AnEndpoint` endpoints) _ =
57− (0,) (toReifiedEndpoint (toDyn (normalize endpoint)) (Proxy @endpoint))
58− : map
59− (\(n, x) -> (n + 1, x))
88+ (0, ReifiedEndpoint
89+ { reArguments = reifiedEndpointArguments @endpoint
90+ , reEndpointFunc = normalize endpoint
91+ }
92+ )
93+ : (map . first) (+1)
6094 (toReifiedApi endpoints (Proxy @endpoints))
6195
6296class NormalizeFunction m where
@@ -73,35 +107,37 @@ instance (Typeable x, Breakdown x) => NormalizeFunction (Handler x) where
73107 Left serverError -> pure (Left serverError)
74108 Right x -> pure $ Right $ breakdown x
75109
76−-- pure (Right (typeRep (Proxy @x), toDyn x))
77−
78110instance
79111 (Typeable responseType, Breakdown responseType) =>
80112 ToReifiedEndpoint (Verb method statusCode contentTypes responseType)
81113 where
82− toReifiedEndpoint endpoint _ = id
83− ([], endpoint)
114+ type EndpointArgs (Verb method statusCode contentTypes responseType) = '[]
115+ type EndpointRes (Verb method statusCode contentTypes responseType) = responseType
116+ reifiedEndpointArguments = V.RNil
84117
85118instance
86119 (ToReifiedEndpoint endpoint) =>
87120 ToReifiedEndpoint ((x :: Symbol) :> endpoint)
88121 where
89− toReifiedEndpoint endpoint _ =
90− toReifiedEndpoint endpoint (Proxy @endpoint)
122+ type EndpointArgs ((x :: Symbol) :> endpoint) = EndpointArgs endpoint
123+ type EndpointRes ((x :: Symbol) :> endpoint) = EndpointRes endpoint
124+ reifiedEndpointArguments = reifiedEndpointArguments @endpoint
91125
92126instance
93127 (ToReifiedEndpoint endpoint) =>
94128 ToReifiedEndpoint (Description s :> endpoint)
95129 where
96− toReifiedEndpoint endpoint _ =
97− toReifiedEndpoint endpoint (Proxy @endpoint)
130+ type EndpointArgs (Description s :> endpoint) = EndpointArgs endpoint
131+ type EndpointRes (Description s :> endpoint) = EndpointRes endpoint
132+ reifiedEndpointArguments = reifiedEndpointArguments @endpoint
98133
99134instance
100135 (ToReifiedEndpoint endpoint) =>
101136 ToReifiedEndpoint (Summary s :> endpoint)
102137 where
103− toReifiedEndpoint endpoint _ =
104− toReifiedEndpoint endpoint (Proxy @endpoint)
138+ type EndpointArgs (Summary s :> endpoint) = EndpointArgs endpoint
139+ type EndpointRes (Summary s :> endpoint) = EndpointRes endpoint
140+ reifiedEndpointArguments = reifiedEndpointArguments @endpoint
105141
106142instance
107143 (Typeable requestType
@@ -109,58 +145,57 @@ instance
109145 ,ToReifiedEndpoint endpoint) =>
110146 ToReifiedEndpoint (QueryFlag name :> endpoint)
111147 where
112− toReifiedEndpoint endpoint _ =
113− toReifiedEndpoint endpoint (Proxy @endpoint)
114− & \(args, result) -> ((typeRep (Proxy @Bool),buildFrom @Bool) : args, result)
148+ type EndpointArgs (QueryFlag name :> endpoint) = Bool ': EndpointArgs endpoint
149+ type EndpointRes (QueryFlag name :> endpoint) = EndpointRes endpoint
150+ reifiedEndpointArguments = tagType (Argument (buildFrom @Bool)) V.:& reifiedEndpointArguments @endpoint
151+
152+type IfLenient s mods t = If (FoldLenient mods) (Either s t) t
153+type IfRequired mods t = If (FoldRequired mods) t (Maybe t)
154+type IfRequiredLenient s mods t = IfRequired mods (IfLenient s mods t)
115155
116156instance
117− ( Typeable (If (FoldRequired mods) paramType (Maybe paramType))
118− , BuildFrom (If (FoldRequired mods) paramType (Maybe paramType))
157+ ( BuildFrom (IfRequiredLenient T.Text mods paramType)
119158 , ToReifiedEndpoint endpoint
120− , SBoolI (FoldRequired mods)) =>
159+ ) =>
121160 ToReifiedEndpoint (QueryParam' mods name paramType :> endpoint)
122161 where
123− toReifiedEndpoint endpoint _ =
124− toReifiedEndpoint endpoint (Proxy @endpoint)
125− & \(args, result) ->
126− ((typeRep (Proxy @(If (FoldRequired mods) paramType (Maybe paramType)))
127− ,buildFrom @(If (FoldRequired mods) paramType (Maybe paramType)))
128− : args, result)
162+ type EndpointArgs (QueryParam' mods name paramType :> endpoint) = IfRequiredLenient T.Text mods paramType ': EndpointArgs endpoint
163+ type EndpointRes (QueryParam' mods name paramType :> endpoint) = EndpointRes endpoint
164+ reifiedEndpointArguments =
165+ tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods paramType)))
166+ V.:& reifiedEndpointArguments @endpoint
167+
129168
130169instance
131− ( Typeable (If (FoldRequired mods) headerType (Maybe headerType))
132− , BuildFrom (If (FoldRequired mods) headerType (Maybe headerType))
170+ ( BuildFrom (IfRequiredLenient T.Text mods headerType)
133171 , ToReifiedEndpoint endpoint
134− , SBoolI (FoldRequired mods)) =>
172+ ) =>
135173 ToReifiedEndpoint (Header' mods headerName headerType :> endpoint)
136174 where
137− toReifiedEndpoint endpoint _ =
138− toReifiedEndpoint endpoint (Proxy @endpoint)
139− & \(args, result) -> ((typeRep (Proxy @(If (FoldRequired mods) headerType (Maybe headerType)))
140− ,buildFrom @(If (FoldRequired mods) headerType (Maybe headerType)))
141−
142− : args, result)
175+ type EndpointArgs (Header' mods headerName headerType :> endpoint) = IfRequiredLenient T.Text mods headerType ': EndpointArgs endpoint
176+ type EndpointRes (Header' mods headerName headerType :> endpoint) = EndpointRes endpoint
177+ reifiedEndpointArguments =
178+ tagType (Argument (buildFrom @(IfRequiredLenient T.Text mods headerType)))
179+ V.:& reifiedEndpointArguments @endpoint
143180
144181instance
145− ( Typeable captureType
146− , BuildFrom captureType
182+ ( BuildFrom (IfLenient String mods captureType)
147183 , ToReifiedEndpoint endpoint) =>
148184 ToReifiedEndpoint (Capture' mods name captureType :> endpoint)
149185 where
150− toReifiedEndpoint endpoint _ =
151− toReifiedEndpoint endpoint (Proxy @endpoint)
152− & \(args, result) -> ((typeRep (Proxy @captureType)
153− ,buildFrom @captureType)
154− : args, result)
186+ type EndpointArgs (Capture' mods name captureType :> endpoint) = IfLenient String mods captureType ': EndpointArgs endpoint
187+ type EndpointRes (Capture' mods name captureType :> endpoint) = EndpointRes endpoint
188+ reifiedEndpointArguments =
189+ tagType (Argument (buildFrom @(IfLenient String mods captureType)))
190+ V.:& reifiedEndpointArguments @endpoint
155191
156192instance
157− ( Typeable (If (FoldLenient mods) (Either String requestType) requestType)
158− , BuildFrom (If (FoldLenient mods) (Either String requestType) requestType)
159− , ToReifiedEndpoint endpoint, SBoolI (FoldLenient mods)) =>
193+ ( BuildFrom (IfLenient String mods requestType)
194+ , ToReifiedEndpoint endpoint) =>
160195 ToReifiedEndpoint (ReqBody' mods contentTypes requestType :> endpoint)
161196 where
162− toReifiedEndpoint endpoint _ =
163− toReifiedEndpoint endpoint (Proxy @endpoint)
164− & \(args, result) -> ((typeRep (Proxy @(If (FoldLenient mods) (Either String requestType) requestType))
165− ,buildFrom @(If (FoldLenient mods) (Either String requestType) requestType))
166− : args, result)
197+ type EndpointArgs (ReqBody' mods contentTypes requestType :> endpoint) = IfLenient String mods requestType ': EndpointArgs endpoint
198+ type EndpointRes (ReqBody' mods contentTypes requestType :> endpoint) = EndpointRes endpoint
199+ reifiedEndpointArguments =
200+ tagType (Argument (buildFrom @(IfLenient String mods requestType)))
201+ V.:& reifiedEndpointArguments @endpoint
stack.yaml+4
@@ -13,6 +13,10 @@ extra-deps:
1313- QuickCheck-2.14.1@sha256:01e46d7b0a8d3148288ec977625f62d5516ebb5031a50c63f0453863301b4a79,7736
1414- random-1.2.0
1515- splitmix-0.1.0.1@sha256:22f9662e7e8b173421872241edd39350078a9ed4bb9e9f503948c5b483c79276,5253
16+- vinyl-0.13.0
17+- dependent-sum-0.7.1.0
18+- constraints-extras-0.3.0.2
19+- dependent-map-0.4.0.0
1620- git: https://github.com/hedgehogqa/haskell-hedgehog.git
1721 commit: 03714682586e43b5ddf5c00391035471a6e01238
1822 subdirs:
stack.yaml.lock+28
@@ -67,6 +67,34 @@ packages:
6767 sha256: b9a0c60e2d4786bbb276ce6606027a99615bf703fa3e0f01911e8c6a656c5161
6868 original:
6969 hackage: splitmix-0.1.0.1@sha256:22f9662e7e8b173421872241edd39350078a9ed4bb9e9f503948c5b483c79276,5253
70+- completed:
71+ hackage: vinyl-0.13.0@sha256:0f247cd3f8682b30881a07de18e6fec52d540646fbcb328420049cc8d63cd407,3724
72+ pantry-tree:
73+ size: 1857
74+ sha256: 860fb95820b595161cdbdec5f376100ebae2d14e5ef0dbe311546202f7525d01
75+ original:
76+ hackage: vinyl-0.13.0
77+- completed:
78+ hackage: dependent-sum-0.7.1.0@sha256:5599aa89637db434431b1dd3fa7c34bc3d565ee44f0519bfbc877be1927c2531,2068
79+ pantry-tree:
80+ size: 290
81+ sha256: 9cbfb32b5a8a782b7a1c941803fd517633cb699159b851c1d82267a9e9391b50
82+ original:
83+ hackage: dependent-sum-0.7.1.0
84+- completed:
85+ hackage: constraints-extras-0.3.0.2@sha256:013b8d0392582c6ca068e226718a4fe8be8e22321cc0634f6115505bf377ad26,1853
86+ pantry-tree:
87+ size: 594
88+ sha256: 3ce1012bfb02e4d7def9df19ce80b8cd2b472c691b25b181d9960638673fecd1
89+ original:
90+ hackage: constraints-extras-0.3.0.2
91+- completed:
92+ hackage: dependent-map-0.4.0.0@sha256:ca2b131046f4340a1c35d138c5a003fe4a5be96b14efc26291ed35fd08c62221,1657
93+ pantry-tree:
94+ size: 551
95+ sha256: 5defa30010904d2ad05a036f3eaf83793506717c93cbeb599f40db1a3632cfc5
96+ original:
97+ hackage: dependent-map-0.4.0.0
7098- completed:
7199 subdir: hedgehog
72100 name: hedgehog