on master help sign in
Merge pull request #1 from mwotton/sam/typerepmap

Sam/typerepmap

commit3bd7a4939c0668b2c9b5e4814a3cd414ed185490 authorMark Wotton <mwotton@gmail.com> dateSun 13 Sep 2020, 11:41 (6 years ago) parent54a83a656f705f
8 files changed+600−174
Makefile+1−1 package.yaml+8−1 roboservant.cabal+19−4 src/Roboservant/ContextualGenRequest.hs+195−159 src/Roboservant/Hedgehog.hs+41 src/Roboservant/StateMachine.hs+333−7 stack.yaml+1−1 test/Spec.hs+2−1
Makefile+1−1
@@ -1,2 +1,2 @@
11testwatch:
2− ghcid -c 'stack repl --test --ghc-options=-fobject-code' --allow-eval --restart="stack.yaml" --restart="package.yaml"
2+ ghcid -c 'stack repl --test --ghc-options=-fobject-code roboservant' --allow-eval --restart="stack.yaml" --restart="package.yaml" -W
package.yaml+8−1
@@ -24,10 +24,14 @@ dependencies:
2424
2525
2626- bytestring
27+- hedgehog
28+- constrained-dynamic
29+- containers
2730- http-client
2831- http-media
2932- QuickCheck
30−- quickcheck-state-machine
33+- quickcheck-state-machine >= 0.7
34+- mtl
3135- servant >= 0.17
3236- servant-client >= 0.17
3337- servant-flatten
@@ -36,6 +40,9 @@ dependencies:
3640# test deps
3741- hspec
3842- QuickCheck
43+- warp
44+- aeson
45+- wai
3946library:
4047 source-dirs: src
4148
roboservant.cabal+19−4
@@ -1,10 +1,10 @@
11cabal-version: 1.12
22
3−-- This file has been generated from package.yaml by hpack version 0.33.0.
3+-- This file has been generated from package.yaml by hpack version 0.31.2.
44--
55-- see: https://github.com/sol/hpack
66--
7−-- hash: e066fe1fb50c5d775ba730812d8afb9db9b9d14e08b2c25ca2277a1d7205d852
7+-- hash: 9d15f4554f9f8ea8913a6766643b63ee6633016374fc98164e81058799204a87
88
99name: roboservant
1010version: 0.1.0.0
@@ -29,6 +29,7 @@ library
2929 exposed-modules:
3030 Roboservant
3131 Roboservant.ContextualGenRequest
32+ Roboservant.Hedgehog
3233 Roboservant.StateMachine
3334 other-modules:
3435 Paths_roboservant
@@ -36,17 +37,24 @@ library
3637 src
3738 build-depends:
3839 QuickCheck
40+ , aeson
3941 , base >=4.7 && <5
4042 , bytestring
43+ , constrained-dynamic
44+ , containers
45+ , hedgehog
4146 , hspec
4247 , http-client
4348 , http-media
44− , quickcheck-state-machine
49+ , mtl
50+ , quickcheck-state-machine >=0.7
4551 , servant >=0.17
4652 , servant-client >=0.17
4753 , servant-flatten
4854 , servant-server >=0.17
4955 , string-conversions
56+ , wai
57+ , warp
5058 default-language: Haskell2010
5159
5260test-suite roboservant-test
@@ -59,16 +67,23 @@ test-suite roboservant-test
5967 ghc-options: -threaded -rtsopts -with-rtsopts=-N
6068 build-depends:
6169 QuickCheck
70+ , aeson
6271 , base >=4.7 && <5
6372 , bytestring
73+ , constrained-dynamic
74+ , containers
75+ , hedgehog
6476 , hspec
6577 , http-client
6678 , http-media
67− , quickcheck-state-machine
79+ , mtl
80+ , quickcheck-state-machine >=0.7
6881 , roboservant
6982 , servant >=0.17
7083 , servant-client >=0.17
7184 , servant-flatten
7285 , servant-server >=0.17
7386 , string-conversions
87+ , wai
88+ , warp
7489 default-language: Haskell2010
src/Roboservant/ContextualGenRequest.hs+195−159
@@ -4,12 +4,14 @@
44{-# LANGUAGE FlexibleInstances #-}
55{-# LANGUAGE GADTs #-}
66{-# LANGUAGE KindSignatures #-}
7+{-# LANGUAGE LambdaCase #-}
78{-# LANGUAGE MultiParamTypeClasses #-}
89{-# LANGUAGE OverloadedStrings #-}
910{-# LANGUAGE PolyKinds #-}
1011{-# LANGUAGE QuantifiedConstraints #-}
1112{-# LANGUAGE RankNTypes #-}
1213{-# LANGUAGE ScopedTypeVariables #-}
14+{-# LANGUAGE TupleSections #-}
1315{-# LANGUAGE TypeApplications #-}
1416{-# LANGUAGE TypeFamilies #-}
1517{-# LANGUAGE TypeOperators #-}
@@ -32,171 +34,205 @@ import Servant
3234import Servant.API
3335import Servant.API.ContentTypes (AllMimeRender, allMimeRender)
3436import Servant.Client
35−import Test.QuickCheck (Arbitrary, Gen, elements, frequency)
36−
37−class HasContextualGenRequest a b where
38− genContextualRequest :: Proxy a -> (Int, Gen (BaseUrl -> b -> Request))
39−
40−-- -- | Generatable is functionally equivalent to Arbitrary with a default.
41−-- -- It is separate because we want to allow overwriting at will.
42−-- class Generatable a where
43−-- generator :: Gen [a]
44−-- generator = pure []
45−
46−instance ( HasContextualGenRequest a st
47− , HasContextualGenRequest b st)
48− => HasContextualGenRequest (a :<|> b) st where
49− genContextualRequest _
50− = (lf + rf, frequency [l, r])
37+-- import Test.QuickCheck (Arbitrary, Gen, elements, frequency)
38+
39+import Data.Dynamic (Dynamic, fromDynamic)
40+import Data.Map.Strict (Map)
41+import qualified Data.Map.Strict as Map
42+import Data.Maybe
43+import Data.Typeable (TypeRep, Typeable, typeRep)
44+import Hedgehog
45+import qualified Hedgehog.Gen as Gen
46+import Roboservant.Hedgehog
47+
48+class HasContextualGenRequest a where
49+ genContextualRequest :: Proxy a -> Map TypeRep Dynamic -> Maybe (Int, Gen ( BaseUrl -> Request))
50+
51+-- | Generatable is functionally equivalent to Arbitrary with a default.
52+-- It is separate because we want to allow overwriting at will.
53+class Generatable a where
54+ generator :: Gen [a]
55+ generator = pure []
56+
57+instance ( HasContextualGenRequest a
58+ , HasContextualGenRequest b)
59+ => HasContextualGenRequest (a :<|> b) where
60+ genContextualRequest _ store
61+ = case sub of
62+ [] -> Nothing
63+ _ -> Just (newfreq, Gen.frequency sub)
5164 where
52− l@(lf, _) = genContextualRequest (Proxy :: Proxy a)
53− r@(rf, _) = genContextualRequest (Proxy :: Proxy b)
54−
55−
56−instance (KnownSymbol path, HasContextualGenRequest b st)
57− => HasContextualGenRequest (path :> b) st where
58− genContextualRequest _ = (1, do
59−
60− old' <- (_old :: _)
61− return $ \burl st ->
62− let r = old' burl
63− oldPath = path r
64− oldPath' = BS.dropWhile (== BS.c2w '/') oldPath
65− paths = filter (not . BS.null) [new, oldPath']
66− in r { path = "/" <> BS.intercalate "/" paths })
65+ newfreq = sum (map fst sub)
66+ sub = catMaybes [l,r]
67+ l = genContextualRequest (Proxy :: Proxy a) store
68+ r = genContextualRequest (Proxy :: Proxy b) store
69+
70+withGeneratable p store cont = cont <$> genContextualRequest p store
71+
72+getCandidates p st cont =
73+ case Map.lookup (typeRep p) st of
74+ Nothing -> (0,error "shouldn't ever be invoked")
75+ Just x -> do
76+ case fromDynamic x of
77+ Nothing -> (0,error "shouldn't ever be invoked")
78+ Just candidates -> cont candidates
79+
80+instance (KnownSymbol path, HasContextualGenRequest b )
81+ => HasContextualGenRequest (path :> b) where
82+ genContextualRequest _ store =
83+ withGeneratable (Proxy :: Proxy b) store $ \(oldf,old') -> do
84+ ( oldf, do
85+ old <- old'
86+ pure $ \burl ->
87+ let r = old burl
88+ oldPath = path r
89+ oldPath' = BS.dropWhile (== BS.c2w '/') oldPath
90+ paths = filter (not . BS.null) [new, oldPath']
91+ in r { path = "/" <> BS.intercalate "/" paths }
92+
93+ )
6794 where
68− -- (oldf, old) = genContextualRequest (Proxy :: Proxy b)
95+ -- (oldf, old) = genContextualRequest (Proxy :: Proxy b) store
6996 new = cs $ symbolVal (Proxy :: Proxy path)
7097
71−instance HasContextualGenRequest EmptyAPI st where
72− genContextualRequest _ = (0, error "EmptyAPIs cannot be queried.")
98+instance HasContextualGenRequest EmptyAPI where
99+ genContextualRequest _ _ = Nothing
73100
74−instance HasContextualGenRequest api st => HasContextualGenRequest (Summary d :> api) st where
101+instance HasContextualGenRequest api => HasContextualGenRequest (Summary d :> api) where
75102 genContextualRequest _ = genContextualRequest (Proxy :: Proxy api)
76103
77−instance HasContextualGenRequest api st => HasContextualGenRequest (Description d :> api) st where
104+instance HasContextualGenRequest api => HasContextualGenRequest (Description d :> api) where
78105 genContextualRequest _ = genContextualRequest (Proxy :: Proxy api)
79106
80−instance (HasContextualGenRequest b st, ToHttpApiData c )
81− => HasContextualGenRequest (Capture' mods x c :> b) st where
82− genContextualRequest _ = (oldf, do
83− old' <- old
84− new' <- toUrlPiece <$> _generator
85− return $ \burl st -> let r = old' burl st in r { path = cs new' <> path r })
86− where
87− (oldf, old) = genContextualRequest (Proxy :: Proxy b)
88−
89−instance (HasContextualGenRequest b st, ToHttpApiData c )
90− => HasContextualGenRequest (CaptureAll x c :> b) st where
91− genContextualRequest _ = (oldf, do
92− old' <- old
93− new' <- fmap (cs . toUrlPiece) <$> new
94− let new'' = BS.intercalate "/" new'
95− return $ \burl st -> let r = old' burl st
96− in r { path = new'' <> path r })
97− where
98− (oldf, old) = genContextualRequest (Proxy :: Proxy b)
99− new = _arbitrary :: Gen [c]
100−
101−instance (KnownSymbol h, HasContextualGenRequest b st, ToHttpApiData c)
102− => HasContextualGenRequest (Header' mods h c :> b) st where
103− genContextualRequest _ = (oldf, do
104− old' <- old
105− new' <- toUrlPiece <$> new -- TODO: generate lenient or/and optional
106− return $ \burl st -> let r = old' burl st in r {
107− requestHeaders = (hdr, cs new') : requestHeaders r })
108− where
109− (oldf, old) = genContextualRequest (Proxy :: Proxy b)
110− hdr = fromString $ symbolVal (Proxy :: Proxy h)
111− new = _arbitrary :: Gen c
112−
113−instance (AllMimeRender x c, HasContextualGenRequest b st)
114− => HasContextualGenRequest (ReqBody' mods x c :> b) st where
115− genContextualRequest _ = (oldf, do
116− old' <- old -- TODO: generate lenient
117− new' <- new
118− (ct, bd) <- elements $ allMimeRender (Proxy :: Proxy x) new'
119− return $ \burl st -> let r = old' burl st in r {
120− requestBody = RequestBodyLBS bd
121− , requestHeaders = ("Content-Type", renderHeader ct) : requestHeaders r
122− })
123− where
124− (oldf, old) = genContextualRequest (Proxy :: Proxy b)
125− new = _arbitrary :: Gen c
126−
127−instance (KnownSymbol x, ToHttpApiData c, HasContextualGenRequest b st)
128− => HasContextualGenRequest (QueryParam' mods x c :> b) st where
129− genContextualRequest _ = (oldf, do
130− new' <- new -- TODO: generate lenient or/and optional
131− old' <- old
132− return $ \burl st -> let
133− r = old' burl st
134− newExpr = param <> "=" <> cs (toQueryParam new')
135− qs = queryString r in r {
136− queryString = if BS.null qs then newExpr else newExpr <> "&" <> qs })
137− where
138− (oldf, old) = genContextualRequest (Proxy :: Proxy b)
139− param = cs $ symbolVal (Proxy :: Proxy x)
140− new = _arbitrary :: Gen c
141−
142−instance (KnownSymbol x, ToHttpApiData c, HasContextualGenRequest b st)
143− => HasContextualGenRequest (QueryParams x c :> b) st where
144− genContextualRequest _ = (oldf, do
145− new' <- _fetch
146− old' <- old
147− return $ \burl st -> let r = old' burl st in r {
148− queryString = queryString r
149− <> if length new' > 0 then fold (toParam <$> new') else ""})
150− where
151− (oldf, old) = genContextualRequest (Proxy :: Proxy b)
152− param = cs $ symbolVal (Proxy :: Proxy x)
153− toParam c = param <> "[]=" <> cs (toQueryParam c)
154− fold = foldr1 (\a b -> a <> "&" <> b)
155−
156−instance (KnownSymbol x, HasContextualGenRequest b st)
157− => HasContextualGenRequest (QueryFlag x :> b) st where
158− genContextualRequest _ = (oldf, do
159− old' <- old
160− return $ \burl st -> let r = old' burl st
161− qs = queryString r in r {
162− queryString = if BS.null qs then param else param <> "&" <> qs })
163− where
164− (oldf, old) = genContextualRequest (Proxy :: Proxy b)
165− param = cs $ symbolVal (Proxy :: Proxy x)
166−
167−instance (ReflectMethod method)
168− => HasContextualGenRequest (Verb (method :: k) (status :: Nat) (cts :: [*]) a) st where
169− genContextualRequest _ = (1, return $ \burl st -> defaultRequest
170− { host = cs $ baseUrlHost burl
171− , port = baseUrlPort burl
172− , secure = baseUrlScheme burl == Https
173− , method = reflectMethod (Proxy :: Proxy method)
174− })
175−
176−instance (ReflectMethod method)
177− => HasContextualGenRequest (NoContentVerb (method :: k)) st where
178− genContextualRequest _ = (1, return $ \burl st -> defaultRequest
179− { host = cs $ baseUrlHost burl
180− , port = baseUrlPort burl
181− , secure = baseUrlScheme burl == Https
182− , method = reflectMethod (Proxy :: Proxy method)
183− })
184−
185−instance (HasContextualGenRequest a st) => HasContextualGenRequest (RemoteHost :> a) st where
186− genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
187−
188−instance (HasContextualGenRequest a st) => HasContextualGenRequest (IsSecure :> a) st where
189− genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
190−
191−instance (HasContextualGenRequest a st) => HasContextualGenRequest (HttpVersion :> a) st where
192− genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
193−
194−instance (HasContextualGenRequest a st) => HasContextualGenRequest (Vault :> a) st where
195− genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
196−
197−instance (HasContextualGenRequest a st) => HasContextualGenRequest (WithNamedContext x y a) st where
198− genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
199−
200−-- TODO: Try logging in
201−instance (HasContextualGenRequest a st) => HasContextualGenRequest (BasicAuth x y :> a) st where
202− genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
107+instance (HasContextualGenRequest b, Typeable c, ToHttpApiData c )
108+ => HasContextualGenRequest (Capture' mods x c :> b) where
109+ genContextualRequest _ st = withGeneratable (Proxy :: Proxy b) st $ \(oldf,old) ->
110+ getCandidates (Proxy :: Proxy c) st $ \candidates ->
111+ (oldf
112+ ,do
113+ piece :: c <- Gen.choice candidates
114+ old' <- old
115+ pure $ \burl ->
116+ let r = old' burl
117+ in r { path = cs (toUrlPiece piece) <> path r })
118+
119+-- unsure how CaptureAll works, finish later.
120+-- instance (HasContextualGenRequest b, ToHttpApiData c, Typeable c )
121+-- => HasContextualGenRequest (CaptureAll x c :> b) where
122+-- genContextualRequest _ st = withGeneratable (Proxy :: Proxy b) st $ \(oldf,old) ->
123+-- getCandidates (Proxy :: Proxy c) st $ \candidates ->
124+-- (oldf, do
125+-- old' <- old
126+-- piece :: c <- Gen.choice candidates
127+-- let new'' = BS.intercalate "/" . fmap (cs . toUrlPiece) $ piece
128+
129+
130+
131+-- return $ \burl -> let r = old' burl
132+-- in r { path = new'' <> path r })
133+
134+-- -- (oldf, old) = genContextualRequest (Proxy :: Proxy b)
135+-- -- new = _arbitrary :: Gen [c]
136+
137+-- instance (KnownSymbol h, HasContextualGenRequest b st, ToHttpApiData c)
138+-- => HasContextualGenRequest (Header' mods h c :> b) st where
139+-- genContextualRequest _ = (oldf, do
140+-- old' <- old
141+-- new' <- toUrlPiece <$> new -- TODO: generate lenient or/and optional
142+-- return $ \burl st -> let r = old' burl st in r {
143+-- requestHeaders = (hdr, cs new') : requestHeaders r })
144+-- where
145+-- (oldf, old) = genContextualRequest (Proxy :: Proxy b)
146+-- hdr = fromString $ symbolVal (Proxy :: Proxy h)
147+-- new = _arbitrary :: Gen c
148+
149+-- instance (AllMimeRender x c, HasContextualGenRequest b st)
150+-- => HasContextualGenRequest (ReqBody' mods x c :> b) st where
151+-- genContextualRequest _ = (oldf, do
152+-- old' <- old -- TODO: generate lenient
153+-- new' <- new
154+-- (ct, bd) <- elements $ allMimeRender (Proxy :: Proxy x) new'
155+-- return $ \burl st -> let r = old' burl st in r {
156+-- requestBody = RequestBodyLBS bd
157+-- , requestHeaders = ("Content-Type", renderHeader ct) : requestHeaders r
158+-- })
159+-- where
160+-- (oldf, old) = genContextualRequest (Proxy :: Proxy b)
161+-- new = _arbitrary :: Gen c
162+
163+-- instance (KnownSymbol x, ToHttpApiData c, HasContextualGenRequest b st)
164+-- => HasContextualGenRequest (QueryParam' mods x c :> b) st where
165+-- genContextualRequest _ = (oldf, do
166+-- new' <- new -- TODO: generate lenient or/and optional
167+-- old' <- old
168+-- return $ \burl st -> let
169+-- r = old' burl st
170+-- newExpr = param <> "=" <> cs (toQueryParam new')
171+-- qs = queryString r in r {
172+-- queryString = if BS.null qs then newExpr else newExpr <> "&" <> qs })
173+-- where
174+-- (oldf, old) = genContextualRequest (Proxy :: Proxy b)
175+-- param = cs $ symbolVal (Proxy :: Proxy x)
176+-- new = _arbitrary :: Gen c
177+
178+-- instance (KnownSymbol x, ToHttpApiData c, HasContextualGenRequest b st)
179+-- => HasContextualGenRequest (QueryParams x c :> b) st where
180+-- genContextualRequest _ = (oldf, do
181+-- (new' :: c) <- _fetch
182+-- old' <- old
183+-- return $ \burl st -> let r = old' burl st in r {
184+-- queryString = queryString r
185+-- <> if length new' > 0 then fold (toParam <$> new') else ""})
186+-- where
187+-- (oldf, old) = genContextualRequest (Proxy :: Proxy b)
188+-- param = cs $ symbolVal (Proxy :: Proxy x)
189+-- toParam c = param <> "[]=" <> cs (toQueryParam c)
190+-- fold = foldr1 (\a b -> a <> "&" <> b)
191+
192+-- instance (KnownSymbol x, HasContextualGenRequest b st)
193+-- => HasContextualGenRequest (QueryFlag x :> b) st where
194+-- genContextualRequest _ = (oldf, do
195+-- old' <- old
196+-- return $ \burl st -> let r = old' burl st
197+-- qs = queryString r in r {
198+-- queryString = if BS.null qs then param else param <> "&" <> qs })
199+-- where
200+-- (oldf, old) = genContextualRequest (Proxy :: Proxy b)
201+-- param = cs $ symbolVal (Proxy :: Proxy x)
202+
203+-- instance (ReflectMethod method)
204+-- => HasContextualGenRequest (Verb (method :: k) (status :: Nat) (cts :: [*]) a) st where
205+-- genContextualRequest _ = (1, return $ \burl st -> defaultRequest
206+-- { host = cs $ baseUrlHost burl
207+-- , port = baseUrlPort burl
208+-- , secure = baseUrlScheme burl == Https
209+-- , method = reflectMethod (Proxy :: Proxy method)
210+-- })
211+
212+-- instance (ReflectMethod method)
213+-- => HasContextualGenRequest (NoContentVerb (method :: k)) st where
214+-- genContextualRequest _ = (1, return $ \burl st -> defaultRequest
215+-- { host = cs $ baseUrlHost burl
216+-- , port = baseUrlPort burl
217+-- , secure = baseUrlScheme burl == Https
218+-- , method = reflectMethod (Proxy :: Proxy method)
219+-- })
220+
221+-- instance (HasContextualGenRequest a st) => HasContextualGenRequest (RemoteHost :> a) st where
222+-- genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
223+
224+-- instance (HasContextualGenRequest a st) => HasContextualGenRequest (IsSecure :> a) st where
225+-- genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
226+
227+-- instance (HasContextualGenRequest a st) => HasContextualGenRequest (HttpVersion :> a) st where
228+-- genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
229+
230+-- instance (HasContextualGenRequest a st) => HasContextualGenRequest (Vault :> a) st where
231+-- genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
232+
233+-- instance (HasContextualGenRequest a st) => HasContextualGenRequest (WithNamedContext x y a) st where
234+-- genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
235+
236+-- -- TODO: Try logging in
237+-- instance (HasContextualGenRequest a st) => HasContextualGenRequest (BasicAuth x y :> a) st where
238+-- genContextualRequest _ = genContextualRequest (Proxy :: Proxy a)
src/Roboservant/Hedgehog.hsnew file+41
@@ -0,0 +1,41 @@
1+{-# LANGUAGE LambdaCase #-}
2+module Roboservant.Hedgehog where
3+
4+import Hedgehog
5+import Hedgehog.Gen
6+import qualified Hedgehog.Range as Range
7+
8+-- | Uses a weighted distribution to randomly select one of the generators in
9+-- the list.
10+--
11+-- This generator shrinks towards the first generator in the list.
12+--
13+-- This is to be used when the generator returning a Nothing indicates
14+-- that it will never succeed so should be abandoned entirely.
15+--
16+-- /The input list must be non-empty./
17+--
18+frequencyM :: MonadGen m => [(Int, m (Maybe a))] -> m (Maybe a)
19+frequencyM = \case
20+ [] -> pure Nothing
21+ -- error "Hedgehog.Gen.frequency: used with empty list"
22+ xs0 -> do
23+ let
24+ pick acc n = \case
25+ [] -> pure Nothing
26+-- error "Hedgehog.Gen.frequency/pick: used with empty list"
27+ (k, x) : xs ->
28+ if n <= k then
29+ x >>= \case
30+ Nothing -> do
31+ -- this one will never come out right, so reassemble list without it
32+ frequencyM (acc <> xs)
33+ Just y -> pure $ Just y
34+ else
35+ pick ((k,x):acc) (n - k) xs
36+
37+ total =
38+ sum (fmap fst xs0)
39+
40+ n <- integral $ Range.constant 1 total
41+ pick [] n xs0
src/Roboservant/StateMachine.hs+333−7
@@ -1,16 +1,342 @@
1+{-# LANGUAGE UndecidableInstances #-}
2+{-# LANGUAGE TypeFamilies #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE PolyKinds #-}
5+{-# LANGUAGE TypeApplications #-}
6+{-# LANGUAGE MultiParamTypeClasses #-}
17{-# LANGUAGE AllowAmbiguousTypes #-}
2−{-# LANGUAGE RankNTypes #-}
8+{-# LANGUAGE DataKinds #-}
9+{-# LANGUAGE DeriveAnyClass #-}
10+{-# LANGUAGE DeriveGeneric #-}
11+{-# LANGUAGE DerivingStrategies #-}
12+{-# LANGUAGE ExistentialQuantification #-}
13+{-# LANGUAGE FlexibleContexts #-}
14+{-# LANGUAGE FlexibleInstances #-}
15+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
16+{-# LANGUAGE KindSignatures #-}
17+{-# LANGUAGE LambdaCase #-}
18+{-# LANGUAGE OverloadedStrings #-}
19+{-# LANGUAGE PartialTypeSignatures #-}
20+{-# LANGUAGE RankNTypes #-}
21+{-# LANGUAGE RecordWildCards #-}
22+{-# LANGUAGE ScopedTypeVariables #-}
23+{-# LANGUAGE StandaloneDeriving #-}
24+{-# LANGUAGE TemplateHaskell #-}
25+{-# LANGUAGE TupleSections #-}
26+{-# LANGUAGE TypeOperators #-}
27+
28+-- for servant
29+
330module Roboservant.StateMachine where
431
5−import Servant.Server (Server)
6−import Test.QuickCheck (Property)
7−import Test.StateMachine
8−import Roboservant.ContextualGenRequest
32+import Control.Arrow (second)
33+-- import Data.Dynamic (Dynamic, fromDynamic)
934
35+--import Test.QuickCheck (Property)
36+-- import Test.StateMachine
37+
38+import Type.Reflection (SomeTypeRep)
39+import Servant.API.Flatten (Flat)
40+import GHC.TypeLits (Nat, Symbol)
41+import Data.Function ((&))
42+import Control.Concurrent (forkIO)
43+import Control.Monad ((<=<))
44+import Control.Monad.IO.Class
45+import Data.Aeson
46+import Data.Dynamic
47+import Data.IORef (IORef, newIORef)
48+import qualified Data.List.NonEmpty as NEL
49+import Data.List.NonEmpty (NonEmpty)
50+import Data.Map.Strict (Map)
51+import qualified Data.Map.Strict as Map
52+import Data.Maybe
53+import Data.Type.HasClass
54+import Data.Type.HasClassPreludeInstances
55+import Data.Typeable (TypeRep, Typeable, typeOf, typeRep)
56+import Debug.Trace (traceM, traceShowM)
57+import GHC.Generics (Generic)
58+import GHC.Generics
59+import GHC.IORef (readIORef)
60+import Control.Monad.Except (runExceptT)
61+import Hedgehog
62+import Hedgehog
63+import qualified Hedgehog.Gen as Gen
64+import qualified Hedgehog.Range as Range
65+import Network.HTTP.Client (defaultManagerSettings, newManager)
66+import Network.Wai
67+import Network.Wai.Handler.Warp
68+import Roboservant.ContextualGenRequest
69+import Servant
70+import Servant.Client
71+import Servant.Server (Server)
1072
1173-- | can then use either `forAllParallelCommands` or `forAllCommands` to turn
1274-- this into a property
1375--
1476-- TODO: how do we tie the model, command and response types to the Server?
15−genStateMachine :: HasContextualGenRequest a b => Server a -> StateMachine model command IO response
16−genStateMachine = undefined
77+-- genStateMachine :: HasContextualGenRequest a => Server a -> StateMachine Model Command IO Response
78+-- genStateMachine = StateMachine initModel _transition _precondition _postcondition
79+-- Nothing _generator _shrinker _semantics _mock
80+
81+-- initModel :: Model r
82+-- initModel = Model mempty
83+
84+-- newtype Model r = Model (Map TypeRep Dynamic)
85+
86+-- data Command r
87+-- = CallEndpoint
88+-- | Read (Reference (Opaque (IORef Int)) r)
89+-- | Write (Reference (Opaque (IORef Int)) r) Int
90+-- | Increment (Reference (Opaque (IORef Int)) r)
91+
92+-- data Response r
93+-- = Created (Reference (Opaque (IORef Int)) r)
94+-- | ReadValue Int
95+-- | Written
96+-- | Incremented
97+
98+-- deriving Show
99+
100+-- instance Eq MyDyn where
101+-- MyDyn a == MyDyn b = show a == show b
102+
103+data State v
104+ = State
105+ { stateRefs :: Map TypeRep (NonEmpty (Var (Opaque (IORef Dynamic)) v))
106+ }
107+
108+class FlattenServer api where
109+ flattenServer :: Server api -> Bundled (Endpoints api)
110+
111+instance
112+ ( Endpoints (endpoint :<|> api) ~ (endpoint ': Endpoints api)
113+ , Server (endpoint :<|> api) ~ (Server endpoint :<|> Server api)
114+ , FlattenServer api
115+ ) => FlattenServer (endpoint :<|> api) where
116+ flattenServer (endpoint :<|> server) = endpoint `AnEndpoint` flattenServer @api server
117+
118+instance
119+ ( HasServer (x :> api) '[]
120+ , Endpoints (x :> api) ~ '[x :> api]
121+ ) => FlattenServer (x :> api) where
122+ flattenServer server = server `AnEndpoint` NoEndpoints
123+
124+instance
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
129+
130+type ReifiedEndpoint = ([TypeRep], TypeRep, Dynamic)
131+
132+type ReifiedApi = [(ApiOffset, [TypeRep], TypeRep, Dynamic)]
133+
134+data Bundled endpoints where
135+ AnEndpoint :: Server endpoint -> Bundled endpoints -> Bundled (endpoint ': endpoints)
136+ NoEndpoints :: Bundled '[]
137+
138+class ToReifiedApi (endpoints :: [*]) where
139+ toReifiedApi :: Bundled endpoints -> Proxy endpoints -> ReifiedApi
140+
141+class ToReifiedEndpoint (endpoint :: *) where
142+ toReifiedEndpoint :: Dynamic -> Proxy endpoint -> ReifiedEndpoint
143+
144+instance ToReifiedApi '[] where
145+ toReifiedApi NoEndpoints _ = []
146+
147+instance (Typeable (Normal (ServerT endpoint Handler)), NormalizeFunction (ServerT endpoint Handler), ToReifiedEndpoint endpoint, ToReifiedApi endpoints, Typeable (ServerT endpoint Handler))
148+ => ToReifiedApi (endpoint : endpoints)
149+ where
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))
154+ where
155+ withOffset (x, y, z) = (0, x, y, z)
156+
157+class NormalizeFunction m where
158+ type Normal m
159+ normalize :: m -> Normal m
160+
161+instance NormalizeFunction x => NormalizeFunction (r -> x) where
162+ type Normal (r -> x) = r -> Normal x
163+ normalize = fmap normalize
164+
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))
170+
171+instance Typeable responseType
172+ => ToReifiedEndpoint (Verb method statusCode contentTypes responseType)
173+ where
174+ toReifiedEndpoint endpoint _ =
175+ ([], typeRep (Proxy @responseType), endpoint)
176+
177+instance (ToReifiedEndpoint endpoint)
178+ => ToReifiedEndpoint ((x :: Symbol) :> endpoint)
179+ where
180+ toReifiedEndpoint endpoint _ =
181+ toReifiedEndpoint endpoint (Proxy @endpoint)
182+
183+instance (Typeable requestType, ToReifiedEndpoint endpoint)
184+ => ToReifiedEndpoint (Capture name requestType :> endpoint)
185+ where
186+ toReifiedEndpoint endpoint _ =
187+ toReifiedEndpoint endpoint (Proxy @endpoint)
188+ & \(args, result, typeRepMap) -> (typeRep (Proxy @requestType) : args, result, typeRepMap)
189+
190+instance (Typeable requestType, ToReifiedEndpoint endpoint)
191+ => ToReifiedEndpoint (ReqBody contentTypes requestType :> endpoint)
192+ where
193+ toReifiedEndpoint endpoint _ =
194+ toReifiedEndpoint endpoint (Proxy @endpoint)
195+ & \(args, result, typeRepMap) -> (typeRep (Proxy @requestType) : args, result, typeRepMap)
196+
197+newtype ApiOffset = ApiOffset Int
198+ deriving (Eq, Show)
199+ deriving newtype (Enum, Num)
200+
201+-- | we need to specify an offset because it's entirely possible to have two
202+-- functions with the same arguments that do different things.
203+data Op (v :: * -> *) = Op ApiOffset [(TypeRep, Var (Opaque (IORef Dynamic)) v)]
204+
205+deriving instance Show (Op Symbolic)
206+
207+instance HTraversable Op where
208+ htraverse r op@(Op offset args) = Op offset <$> traverse (\(t, v) -> (t,) <$> htraverse r v) args
209+
210+callEndpoint :: (MonadGen n, MonadIO m) => ReifiedApi -> Command n m State
211+callEndpoint staticRoutes =
212+ let gen :: MonadGen n => State Symbolic -> Maybe (n (Op Symbolic))
213+ gen State {..}
214+ | any null options = Nothing
215+ | otherwise = Just $ do
216+ uncurry Op <$> chooseOne options
217+ where
218+ chooseOne ::
219+ MonadGen n =>
220+ [ ( ApiOffset,
221+ [(TypeRep, [Var (Opaque (IORef Dynamic)) Symbolic])]
222+ )
223+ ] ->
224+ n
225+ ( ApiOffset,
226+ [(TypeRep, Var (Opaque (IORef Dynamic)) Symbolic)]
227+ )
228+ chooseOne opts = do
229+ (offset, args) <- Gen.element opts
230+ (offset,) <$> mapM (\(tr, argOpts) -> (tr,) <$> Gen.element argOpts) args
231+ options :: [(ApiOffset, [(TypeRep, [Var (Opaque (IORef Dynamic)) Symbolic])])]
232+ options =
233+ mapMaybe
234+ ( \(offset, argreps, retType, dynCall) -> (offset,) <$> do
235+ mapM (\x -> (x,) <$> fillableWith x) argreps
236+ )
237+ staticRoutes
238+ fillableWith :: TypeRep -> Maybe [Var (Opaque (IORef Dynamic)) Symbolic]
239+ fillableWith tr = NEL.toList <$> Map.lookup tr stateRefs
240+ execute ::
241+ (MonadIO m) =>
242+ Op Concrete ->
243+ m (Opaque (IORef Dynamic))
244+ execute (Op (ApiOffset offset) args) = do
245+ -- traceM (show (offset, args))
246+ fmap Opaque . liftIO $ do
247+ realArgs <- mapM (\(tr, v) -> readIORef (opaque v)) args
248+ let (_offset, staticArgs, ret, endpoint) = staticRoutes !! offset
249+ -- now, magic happens: we apply some dynamic arguments to a dynamic
250+ -- function and hopefully somtehing useful pops out the end.
251+ func = foldr (\arg curr -> flip dynApply arg =<< curr) (Just endpoint) realArgs
252+ let showable = map dynTypeRep (endpoint : realArgs)
253+ case func of
254+ Nothing -> error ("all screwed up: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
255+ Just (f') -> do
256+ case fromDynamic f' of
257+ Nothing -> error ("all screwed up: " <> maybe ("nothing: " <> show showable) (show . dynTypeRep) func)
258+ Just f -> liftIO f >>= \case
259+ Left (serverError :: ServerError) -> error (show serverError)
260+ Right (typeRep :: SomeTypeRep, (dyn :: Dynamic)) -> newIORef dyn
261+ in Command
262+ gen
263+ execute
264+ [ Update $ \s@State {..} (Op (ApiOffset offset) args) o' ->
265+ -- let foo :: Var (Opaque Dynamic) v -> Var (Opaque (IORef Dynamic)) v
266+ -- foo = _ . opaque
267+ -- in
268+ s
269+ { stateRefs =
270+ let (_, _, tr, _) = staticRoutes !! offset
271+ in Map.insertWith
272+ (<>)
273+ tr
274+ (pure o')
275+ stateRefs
276+ }
277+ -- , Ensure (no-500 here)
278+ ]
279+
280+prop_sm_sequential :: ReifiedApi -> Property
281+prop_sm_sequential reifiedApi = do
282+ property $ do
283+ let initialState = State mempty
284+ actions <-
285+ forAll $
286+ Gen.sequential
287+ (Range.linear 1 100)
288+ initialState
289+ [callEndpoint reifiedApi]
290+ executeSequential initialState actions
291+
292+newtype Foo = Foo Int
293+ deriving (Generic, Eq, Show, Typeable)
294+ deriving newtype (FromHttpApiData, ToHttpApiData)
295+
296+instance ToJSON Foo
297+
298+instance FromJSON Foo
299+
300+type FooApi =
301+ "item" :> Get '[JSON] Foo
302+ :<|> "itemAdd" :> Capture "one" Foo :> Capture "two" Foo :> Get '[JSON] Foo
303+ :<|> "item" :> Capture "itemId" Foo :> Get '[JSON] ()
304+
305+intro = pure (Foo 1)
306+
307+combine = (\(Foo a) (Foo b) -> pure (Foo (a + b)))
308+
309+eliminate =
310+ ( \(Foo a) ->
311+ if a > 10
312+ then error "eliminate blew up, oh no!"
313+ else pure ()
314+ )
315+
316+fooServer :: Server FooApi
317+fooServer =
318+ intro
319+ :<|> combine
320+ :<|> eliminate
321+
322+bundledFooServer :: Bundled (Endpoints FooApi)
323+bundledFooServer = flattenServer @FooApi fooServer
324+
325+runServer :: IO ()
326+runServer = do
327+ let port = 3000
328+ settings =
329+ setPort port
330+ $ setBeforeMainLoop (putStrLn ("listening on port " ++ show port))
331+ $ defaultSettings
332+ runSettings settings (serve fooApi fooServer)
333+
334+fooApi :: Proxy FooApi
335+fooApi = Proxy
336+
337+introC :<|> combineC :<|> eliminateC = Servant.Client.client fooApi
338+
339+tests :: IO Bool
340+tests = do
341+ let reifiedApi = toReifiedApi (flattenServer @FooApi fooServer) (Proxy @(Endpoints FooApi))
342+ checkParallel $ Group "props" [("aprop", prop_sm_sequential reifiedApi)]
stack.yaml+1−1
@@ -10,4 +10,4 @@ extra-deps:
1010- servant-client-0.17
1111- servant-client-core-0.17
1212- servant-server-0.17
13−
13+- constrained-dynamic-0.1.0.0@sha256:3516d3b95c8180f8904671aa982af2fae55a185b18642192f5c8f50657108e82,1207
test/Spec.hs+2−1
@@ -17,6 +17,7 @@
1717import Roboservant
1818import Servant.API
1919import Test.Hspec
20+import qualified Roboservant.StateMachine as SM
2021
2122newtype Foo = Foo Int
2223 deriving (Show)
@@ -48,4 +49,4 @@ test' = blah
4849storeOfOurApi = storeOfApi @Api
4950
5051main :: IO ()
51−main = putStrLn "Test suite not yet implemented"
52+main = SM.tests >>= print