@@ -0,0 +1,255 @@
1+{-# LANGUAGE AllowAmbiguousTypes #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DerivingStrategies #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE LambdaCase #-}
9+{-# LANGUAGE PolyKinds #-}
10+{-# LANGUAGE RecordWildCards #-}
11+{-# LANGUAGE ScopedTypeVariables #-}
12+{-# LANGUAGE StandaloneDeriving #-}
13+{-# LANGUAGE TupleSections #-}
14+{-# LANGUAGE TypeApplications #-}
15+{-# LANGUAGE TypeFamilies #-}
16+{-# LANGUAGE TypeOperators #-}
17+{-# LANGUAGE UndecidableInstances #-}
19+module Roboservant.StateMachine
24+import Control.Monad.Except (runExceptT)
25+import Control.Monad.IO.Class (MonadIO, liftIO)
26+import Data.Dynamic (Dynamic, dynApply, dynTypeRep, fromDynamic, toDyn)
27+import Data.Function ((&))
28+import Data.IORef (IORef, newIORef)
29+import qualified Data.List.NonEmpty as NEL
30+import Data.List.NonEmpty (NonEmpty)
31+import Data.Map.Strict (Map)
32+import qualified Data.Map.Strict as Map
33+import Data.Maybe (mapMaybe)
34+import Data.Typeable (TypeRep, Typeable, typeRep)
35+import GHC.IORef (readIORef)
36+import GHC.TypeLits (Symbol)
38+import qualified Hedgehog.Gen as Gen
39+import qualified Hedgehog.Range as Range
41+import Type.Reflection (SomeTypeRep)
45+ { stateRefs :: Map TypeRep (NonEmpty (Var (Opaque (IORef Dynamic)) v))
48+class FlattenServer api where
49+ flattenServer :: Server api -> Bundled (Endpoints api)
52+ ( Endpoints (endpoint :<|> api) ~ (endpoint ': Endpoints api),
53+ Server (endpoint :<|> api) ~ (Server endpoint :<|> Server api),
56+ FlattenServer (endpoint :<|> api)
58+ flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
61+ ( HasServer (x :> api) '[],
62+ Endpoints (x :> api) ~ '[x :> api]
64+ FlattenServer (x :> api)
66+ flattenServer server = server `AnEndpoint` NoEndpoints
69+ ( HasServer (Verb method statusCode contentTypes responseType) '[],
70+ Endpoints (Verb method statusCode contentTypes responseType) ~ '[Verb method statusCode contentTypes responseType]
72+ FlattenServer (Verb method statusCode contentTypes responseType)
74+ flattenServer server = server `AnEndpoint` NoEndpoints
76+type ReifiedEndpoint = ([TypeRep], TypeRep, Dynamic)
78+type ReifiedApi = [(ApiOffset, [TypeRep], TypeRep, Dynamic)]
80+data Bundled endpoints where
81+ AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
82+ NoEndpoints :: Bundled '[]
84+class ToReifiedApi (endpoints :: [*]) where
85+ toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
87+class ToReifiedEndpoint (endpoint :: *) where
88+ toReifiedEndpoint :: Dynamic -> Proxy endpoint -> ReifiedEndpoint
90+instance ToReifiedApi '[] where
91+ toReifiedApi NoEndpoints _ = []
94+ (Typeable (Normal (ServerT endpoint Handler)), NormalizeFunction (ServerT endpoint Handler), ToReifiedEndpoint endpoint, ToReifiedApi endpoints, Typeable (ServerT endpoint Handler)) =>
95+ ToReifiedApi (endpoint : endpoints)
97+ toReifiedApi (endpoint `AnEndpoint` endpoints) _ =
98+ withOffset (toReifiedEndpoint (toDyn (normalize endpoint)) (Proxy @endpoint))
100+ (\(n, x, y, z) -> (n + 1, x, y, z))
101+ (toReifiedApi endpoints (Proxy @endpoints))
103+ withOffset (x, y, z) = (0, x, y, z)
105+class NormalizeFunction m where
107+ normalize :: m -> Normal m
109+instance NormalizeFunction x => NormalizeFunction (r -> x) where
110+ type Normal (r -> x) = r -> Normal x
111+ normalize = fmap normalize
113+instance Typeable x => NormalizeFunction (Handler x) where
114+ type Normal (Handler x) = IO (Either ServerError (TypeRep, Dynamic))
115+ normalize handler = (runExceptT . runHandler') handler >>= \case
116+ Left serverError -> pure (Left serverError)
117+ Right x -> pure (Right (typeRep (Proxy @x), toDyn x))
120+ Typeable responseType =>
121+ ToReifiedEndpoint (Verb method statusCode contentTypes responseType)
123+ toReifiedEndpoint endpoint _ =
124+ ([], typeRep (Proxy @responseType), endpoint)
127+ (ToReifiedEndpoint endpoint) =>
128+ ToReifiedEndpoint ((x :: Symbol) :> endpoint)
130+ toReifiedEndpoint endpoint _ =
131+ toReifiedEndpoint endpoint (Proxy @endpoint)
134+ (Typeable requestType, ToReifiedEndpoint endpoint) =>
135+ ToReifiedEndpoint (Capture name requestType :> endpoint)
137+ toReifiedEndpoint endpoint _ =
138+ toReifiedEndpoint endpoint (Proxy @endpoint)
139+ & \(args, result, typeRepMap) -> (typeRep (Proxy @requestType) : args, result, typeRepMap)
142+ (Typeable requestType, ToReifiedEndpoint endpoint) =>
143+ ToReifiedEndpoint (ReqBody contentTypes requestType :> endpoint)
145+ toReifiedEndpoint endpoint _ =
146+ toReifiedEndpoint endpoint (Proxy @endpoint)
147+ & \(args, result, typeRepMap) -> (typeRep (Proxy @requestType) : args, result, typeRepMap)
149+newtype ApiOffset = ApiOffset Int
151+ deriving newtype (Enum, Num)
153+-- | we need to specify an offset because it's entirely possible to have two
154+-- functions with the same arguments that do different things.
155+data Op (v :: * -> *) = Op ApiOffset [(TypeRep, Var (Opaque (IORef Dynamic)) v)]
157+deriving instance Show (Op Symbolic)
159+instance HTraversable Op where
160+ htraverse r (Op offset args) = Op offset <$> traverse (\(t, v) -> (t,) <$> htraverse r v) args
162+callEndpoint :: (MonadGen n, MonadIO m) => ReifiedApi -> Command n m State
163+callEndpoint staticRoutes =
164+ let gen :: MonadGen n => State Symbolic -> Maybe (n (Op Symbolic))
166+ | any null options = Nothing
167+ | otherwise = Just $ do
168+ uncurry Op <$> chooseOne options
173+ [(TypeRep, [Var (Opaque (IORef Dynamic)) Symbolic])]
178+ [(TypeRep, Var (Opaque (IORef Dynamic)) Symbolic)]
181+ (offset, args) <- Gen.element opts
182+ (offset,) <$> mapM (\(tr, argOpts) -> (tr,) <$> Gen.element argOpts) args
183+ options :: [(ApiOffset, [(TypeRep, [Var (Opaque (IORef Dynamic)) Symbolic])])]
186+ ( \(offset, argreps, _retType, _dynCall) -> (offset,) <$> do
187+ mapM (\x -> (x,) <$> fillableWith x) argreps
190+ fillableWith :: TypeRep -> Maybe [Var (Opaque (IORef Dynamic)) Symbolic]
191+ fillableWith tr = NEL.toList <$> Map.lookup tr stateRefs
195+ m (Opaque (IORef Dynamic))
196+ execute (Op (ApiOffset offset) args) = do
197+ -- traceM (show (offset, args))
198+ fmap Opaque . liftIO $ do
199+ realArgs <- mapM (\(_tr, v) -> readIORef (opaque v)) args
200+ let (_offset, _staticArgs, _ret, endpoint) = staticRoutes !! offset
201+ -- now, magic happens: we apply some dynamic arguments to a dynamic
202+ -- function and hopefully somtehing useful pops out the end.
203+ func = foldr (\arg curr -> flip dynApply arg =<< curr) (Just endpoint) realArgs
204+ let showable = map dynTypeRep (endpoint : realArgs)
206+ Nothing -> error ("all screwed up: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
208+ case fromDynamic f' of
209+ Nothing -> error ("all screwed up: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
210+ Just f -> liftIO f >>= \case
211+ Left (serverError :: ServerError) -> error (show serverError)
212+ Right (_typeRep :: SomeTypeRep, (dyn :: Dynamic)) -> newIORef dyn
216+ [ Update $ \s@State {..} (Op (ApiOffset offset) _args) o' ->
219+ let (_, _, tr, _) = staticRoutes !! offset
226+ -- , Ensure (no-500 here)
229+prop_sequential :: forall api. (FlattenServer api, ToReifiedApi (Endpoints api)) => Server api -> Property
230+prop_sequential server = do
231+ let reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api))
233+ let initialState = State mempty
237+ (Range.linear 1 100)
239+ [callEndpoint reifiedApi]
240+ executeSequential initialState actions
242+prop_concurrent :: forall api. (FlattenServer api, ToReifiedApi (Endpoints api)) => Server api -> Property
243+prop_concurrent server =
244+ let reifiedApi = toReifiedApi (flattenServer @api server) (Proxy @(Endpoints api)) in
245+ let initialState = State mempty
246+ in withTests 1000 . withRetries 10 . property $ do
253+ [callEndpoint reifiedApi]
255+ executeParallel initialState actions