@@ -1,3 +1,9 @@
1+{-# LANGUAGE UndecidableInstances #-}
2+{-# LANGUAGE TypeFamilies #-}
4+{-# LANGUAGE PolyKinds #-}
5+{-# LANGUAGE TypeApplications #-}
6+{-# LANGUAGE MultiParamTypeClasses #-}
1{-# LANGUAGE AllowAmbiguousTypes #-}
7{-# LANGUAGE AllowAmbiguousTypes #-}
2{-# LANGUAGE DataKinds #-}
8{-# LANGUAGE DataKinds #-}
3{-# LANGUAGE DeriveAnyClass #-}
9{-# LANGUAGE DeriveAnyClass #-}
@@ -29,6 +35,10 @@ import Control.Arrow (second)
29--import Test.QuickCheck (Property)
35--import Test.QuickCheck (Property)
30-- import Test.StateMachine
36-- import Test.StateMachine
38+import Type.Reflection (SomeTypeRep)
39+import Servant.API.Flatten (Flat)
40+import GHC.TypeLits (Nat, Symbol)
41+import Data.Function ((&))
32import Control.Concurrent (forkIO)
42import Control.Concurrent (forkIO)
33import Control.Monad ((<=<))
43import Control.Monad ((<=<))
34import Control.Monad.IO.Class
44import Control.Monad.IO.Class
@@ -47,6 +57,7 @@ import Debug.Trace (traceM, traceShowM)
47import GHC.Generics (Generic)
57import GHC.Generics (Generic)
48import GHC.Generics
58import GHC.Generics
49import GHC.IORef (readIORef)
59import GHC.IORef (readIORef)
60+import Control.Monad.Except (runExceptT)
50import Hedgehog
61import Hedgehog
51import Hedgehog
62import Hedgehog
52import qualified Hedgehog.Gen as Gen
63import qualified Hedgehog.Gen as Gen
@@ -94,8 +105,95 @@ data State v
94 { stateRefs :: Map TypeRep (NonEmpty (Var (Opaque (IORef Dynamic)) v))
105 { stateRefs :: Map TypeRep (NonEmpty (Var (Opaque (IORef Dynamic)) v))
108+class FlattenServer api where
109+ flattenServer :: Server api -> Bundled (Endpoints api)
112+ ( Endpoints (endpoint :<|> api) ~ (endpoint ': Endpoints api)
113+ , Server (endpoint :<|> api) ~ (Server endpoint :<|> Server api)
115+ ) => FlattenServer (endpoint :<|> api) where
116+ flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
119+ ( HasServer (x :> api) '[]
120+ , Endpoints (x :> api) ~ '[x :> api]
121+ ) => FlattenServer (x :> api) where
122+ flattenServer server = server `AnEndpoint` NoEndpoints
125+ ( HasServer (Verb method statusCode contentTypes responseType) '[],
126+ Endpoints (Verb method statusCode contentTypes responseType) ~ '[Verb method statusCode contentTypes responseType]
127+ ) => FlattenServer (Verb method statusCode contentTypes responseType) where
128+ flattenServer server = server `AnEndpoint` NoEndpoints
130+type ReifiedEndpoint = ([TypeRep], TypeRep, Dynamic)
97type ReifiedApi = [(ApiOffset, [TypeRep], TypeRep, Dynamic)]
132type ReifiedApi = [(ApiOffset, [TypeRep], TypeRep, Dynamic)]
134+data Bundled endpoints where
135+ AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
136+ NoEndpoints :: Bundled '[]
138+class ToReifiedApi (endpoints :: [*]) where
139+ toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
141+class ToReifiedEndpoint (endpoint :: *) where
142+ toReifiedEndpoint :: Dynamic -> Proxy endpoint -> ReifiedEndpoint
144+instance ToReifiedApi '[] where
145+ toReifiedApi NoEndpoints _ = []
147+instance (Typeable (Normal (ServerT endpoint Handler)), NormalizeFunction (ServerT endpoint Handler), ToReifiedEndpoint endpoint, ToReifiedApi endpoints, Typeable (ServerT endpoint Handler))
148+ => ToReifiedApi (endpoint : endpoints)
150+ toReifiedApi (endpoint `AnEndpoint` endpoints) _ =
151+ withOffset (toReifiedEndpoint (toDyn (normalize endpoint)) (Proxy @endpoint))
152+ : map (\(n, x, y, z) -> (n + 1, x, y, z))
153+ (toReifiedApi endpoints (Proxy @endpoints))
155+ withOffset (x, y, z) = (0, x, y, z)
157+class NormalizeFunction m where
159+ normalize :: m -> Normal m
161+instance NormalizeFunction x => NormalizeFunction (r -> x) where
162+ type Normal (r -> x) = r -> Normal x
163+ normalize = fmap normalize
165+instance Typeable x => NormalizeFunction (Handler x) where
166+ type Normal (Handler x) = IO (Either ServerError (TypeRep, Dynamic))
167+ normalize handler = (runExceptT . runHandler') handler >>= \case
168+ Left serverError -> pure (Left serverError)
169+ Right x -> pure (Right (typeRep (Proxy @x), toDyn x))
171+instance Typeable responseType
172+ => ToReifiedEndpoint (Verb method statusCode contentTypes responseType)
174+ toReifiedEndpoint endpoint _ =
175+ ([], typeRep (Proxy @responseType), endpoint)
177+instance (ToReifiedEndpoint endpoint)
178+ => ToReifiedEndpoint ((x :: Symbol) :> endpoint)
180+ toReifiedEndpoint endpoint _ =
181+ toReifiedEndpoint endpoint (Proxy @endpoint)
183+instance (Typeable requestType, ToReifiedEndpoint endpoint)
184+ => ToReifiedEndpoint (Capture name requestType :> endpoint)
186+ toReifiedEndpoint endpoint _ =
187+ toReifiedEndpoint endpoint (Proxy @endpoint)
188+ & \(args, result, typeRepMap) -> (typeRep (Proxy @requestType) : args, result, typeRepMap)
190+instance (Typeable requestType, ToReifiedEndpoint endpoint)
191+ => ToReifiedEndpoint (ReqBody contentTypes requestType :> endpoint)
193+ toReifiedEndpoint endpoint _ =
194+ toReifiedEndpoint endpoint (Proxy @endpoint)
195+ & \(args, result, typeRepMap) -> (typeRep (Proxy @requestType) : args, result, typeRepMap)
99newtype ApiOffset = ApiOffset Int
197newtype ApiOffset = ApiOffset Int
100 deriving (Eq, Show)
198 deriving (Eq, Show)
101 deriving newtype (Enum, Num)
199 deriving newtype (Enum, Num)
@@ -156,8 +254,10 @@ callEndpoint staticRoutes =
156 Nothing -> error ("all screwed up: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
254 Nothing -> error ("all screwed up: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
157 Just (f') -> do
255 Just (f') -> do
158 case fromDynamic f' of
256 case fromDynamic f' of
159− Nothing -> error "failed on casting final function"
257+ Nothing -> error ("all screwed up: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
160− Just f -> liftIO f >>= newIORef -- >>= either (const $ error "blah") pure
258+ Just f -> liftIO f >>= \case
259+ Left (serverError :: ServerError) -> error (show serverError)
260+ Right (typeRep :: SomeTypeRep, (dyn :: Dynamic)) -> newIORef dyn
161 in Command
261 in Command
@@ -190,7 +290,7 @@ prop_sm_sequential reifiedApi = do
190 executeSequential initialState actions
290 executeSequential initialState actions
192newtype Foo = Foo Int
292newtype Foo = Foo Int
193− deriving (Generic, Eq, Show)
293+ deriving (Generic, Eq, Show, Typeable)
194 deriving newtype (FromHttpApiData, ToHttpApiData)
294 deriving newtype (FromHttpApiData, ToHttpApiData)
196instance ToJSON Foo
296instance ToJSON Foo
@@ -219,6 +319,9 @@ fooServer =
219 :<|> combine
319 :<|> combine
220 :<|> eliminate
320 :<|> eliminate
322+bundledFooServer :: Bundled (Endpoints FooApi)
323+bundledFooServer = flattenServer @FooApi fooServer
222runServer :: IO ()
325runServer :: IO ()
223runServer = do
326runServer = do
224 let port = 3000
327 let port = 3000
@@ -235,44 +338,5 @@ introC :<|> combineC :<|> eliminateC = Servant.Client.client fooApi
236tests :: IO Bool
339tests :: IO Bool
237tests = do
340tests = do
238− withServantServer fooApi (pure fooServer) $ \burl -> do
341+ let reifiedApi = toReifiedApi (flattenServer @FooApi fooServer) (Proxy @(Endpoints FooApi))
239− manager <- newManager defaultManagerSettings
240− let clientEnv = mkClientEnv manager burl
241− -- faking this out for now.
242− footype = typeOf Foo
243− handleError :: forall a b m. (Show b, Applicative m) => (Either b a) -> m a
244− handleError = either (error . show) pure
246− [ (ApiOffset 0, [], footype, toDyn $ toDyn <$> (runClientM introC clientEnv >>= handleError)),
247− (ApiOffset 1, [footype, footype], footype, toDyn $ \f1 f2 -> toDyn <$> (runClientM (combineC f1 f2) clientEnv >>= handleError)),
248− (ApiOffset 2, [footype], (typeOf ()), toDyn $ \f -> toDyn <$> (runClientM (eliminateC f) clientEnv >>= handleError))
250− -- type ReifiedApi = [(ApiOffset, [TypeRep], TypeRep, Dynamic)]
251− checkParallel $ Group "props" [("aprop", prop_sm_sequential reifiedApi)]
342+ checkParallel $ Group "props" [("aprop", prop_sm_sequential reifiedApi)]
253−-- | Start a servant application on an open port, run the provided function,
254−-- then stop the application.
261− (BaseUrl -> IO r) ->
263−withServantServer api = withServantServerAndContext api EmptyContext
265−-- | Like 'withServantServer', but allows passing in a 'Context' to the
269−withServantServerAndContext ::
270− (HasServer a ctx) =>
274− (BaseUrl -> IO r) ->
276−withServantServerAndContext api ctx server t =
277− withApplication (return . serveWithContext api ctx =<< server) $ \port ->
278− t (BaseUrl Http "localhost" port "")