git.iko.soy/servant-quickcheck
on master help sign in
Fix overlapping issues and improve tutorial.
commitbf4efbcb8c6a9ef9e58fc27bf32bd933564c36ff authorJulian K. Arni <jkarni@gmail.com> dateFri 22 Apr 2016, 17:25 (11 years ago) parent0d455a9
5 files changed+90−97
doc/Main.hs+4 doc/ServersEqual.lhs+50−91 doc/doc.cabal+23−1 src/Servant/QuickCheck.hs+5 src/Servant/QuickCheck/Internal/Testable.hs+8−5
doc/Main.hsnew file+4
@@ -0,0 +1,4 @@
1+import ServersEqual
2+
3+main :: IO ()
4+main = main1
doc/ServersEqual.lhs+50−91
@@ -20,98 +20,34 @@ don't need to know anything about Django or Python to follow along; indeed,
2020part of the fun of it is using `serversEqual` to guide you through
2121re-implementing code you may not entirely understand.
2222
23−Looking at the code, we can see the routes in `urls.py`:
24−
25−``` python
26−urlpatterns = patterns('',
27− url(r'^$', RedirectView.as_view(url='/todos')),
28− url(r'^todos$', views.TodoList.as_view()),
29− url(r'^todo/(?P<pk>[0-9]+)$', views.Todo.as_view()),
30−)
31−```
32−
33−And the handlers in `views.py`:
34−
35−``` python
36−class TodoList(APIView):
37− def get(self, request, format=None):
38− todo_items = TodoItem.objects.all()
39− serializer = TodoItemSerializer(todo_items, many=True)
40− return JSONResponse(serializer.data, status=status.HTTP_200_OK)
41−
42− def post(self, request, format=None):
43− serializer = TodoItemSerializer(data=request.DATA)
44− if serializer.is_valid():
45− saved_item = serializer.save()
46− saved_item.url = request.build_absolute_uri('/todo/' + str(saved_item.id))
47− saved_item.save()
48− serializer = TodoItemSerializer(instance=saved_item)
49− return JSONResponse(serializer.data, status=status.HTTP_201_CREATED)
50− return JSONResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
51−
52− def delete(self, request, format=None):
53− TodoItem.objects.all().delete()
54− return JSONResponse(None, status=status.HTTP_204_NO_CONTENT)
55−
56−class Todo(APIView):
57− def get(self, request, pk, format=None):
58− try:
59− todoItem = TodoItem.objects.get(pk=pk)
60− serializer = TodoItemSerializer(todoItem)
61− except TodoItem.DoesNotExist:
62− return JSONResponse(None, status=status.HTTP_400_BAD_REQUEST)
63− return JSONResponse(serializer.data, status=status.HTTP_200_OK)
64−
65− def delete(self, request, pk, format=None):
66− try:
67− todoItem = TodoItem.objects.get(pk=pk)
68− todoItem.delete()
69− except TodoItem.DoesNotExist:
70− return JSONResponse(None, status=status.HTTP_400_BAD_REQUEST)
71− return JSONResponse(None, status=status.HTTP_204_NO_CONTENT)
72−
73− def patch(self, request, pk, format=None):
74− try:
75− todoItem = TodoItem.objects.get(pk=pk)
76− except TodoItem.DoesNotExist:
77− return JSONResponse(None, status=status.HTTP_400_BAD_REQUEST)
78− serializer = TodoItemSerializer(data=request.DATA, instance=todoItem, partial=True)
79− if serializer.is_valid():
80− serializer.save()
81− return JSONResponse(serializer.data, status=status.HTTP_200_OK)
82− return JSONResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
83−```
84−
85−And from `models.py`:
86−
87−``` python
88−
89−class TodoItem(models.Model):
90− title = models.CharField(max_length=256, null=True, blank=True)
91− completed = models.NullBooleanField(null=True, blank=True, default=False)
92− url = models.CharField(max_length=256, null=True, blank=True)
93− order = models.IntegerField(null=True, blank=True)
94−
95−```
96−
97−So as a first pass, let's try:
23+Looking around the codebase (`urls.py`, `models.py` and `views.py` in
24+particular) may lead us to a first attempt at a rewrite:
9825
9926``` haskell
10027{-# LANGUAGE DataKinds #-}
28+{-# LANGUAGE DeriveAnyClass #-}
29+{-# LANGUAGE DeriveGeneric #-}
10130{-# LANGUAGE TypeOperators #-}
10231
32+module ServersEqual where
33+
34+import Control.Concurrent (forkIO, killThread)
35+import Control.Concurrent.STM as STM
36+import Control.Monad.IO.Class
37+import Data.Aeson (ToJSON, FromJSON)
38+import Data.IntMap as M
39+import GHC.Generics (Generic)
40+import Network.Wai.Handler.Warp (run)
10341import Servant
104−import Servant.QuickCheck
105−import STMContainers.Map as M
106−import GHC.Conc (atomically)
107−import Test.QuickCheck
42+import Servant.QuickCheck (serversEqual, BaseUrl(..), Scheme(..))
43+import Test.QuickCheck (Arbitrary(..))
10844
10945data Todo = Todo
11046 { title :: String
11147 , completed :: Bool
11248 , url :: String
11349 , order :: Int
114− } deriving (Eq, Show, Read)
50+ } deriving (Eq, Show, Read, Generic, ToJSON, FromJSON)
11551
11652type API = TodosAPI :<|> TodoAPI
11753
@@ -125,30 +61,44 @@ type TodoAPI
12561 = "todo" :> Capture "id " Int :>
12662 ( Get '[JSON] Todo
12763 :<|> ReqBody '[JSON] Todo :> Patch '[JSON] ()
128− :<|> Delete '[JSON} ())
64+ :<|> Delete '[JSON] ())
12965
130−serverTodos :: Server TodosAPI
66+serverTodos :: DB -> Server TodosAPI
13167serverTodos tvar = getTodos tvar
13268 :<|> postTodos tvar
13369 :<|> deleteAllTodos tvar
13470
71+{-
13572serverTodo :: Server TodoAPI
13673serverTodo id' = getTodo tvar id'
13774 :<|> patchTodo tvar id'
13875 :<|> deleteTodo tvar id'
76+-}
77+
78+type DB = TVar (M.IntMap Todo)
13979
140−getTodos :: Map Int Todo -> Handler [Todo]
141−getTodos m = liftIO . atomically . toList $ S.stream m
80+getTodos :: DB -> Handler [Todo]
81+getTodos m = liftIO $ M.elems <$> STM.readTVarIO m
14282
143−postTodos :: Map Int Todo -> Todo -> Handler ()
144−postTodos m t = liftIO . atomically $ S.insert m t
83+postTodos :: DB -> Todo -> Handler ()
84+postTodos m t = liftIO . STM.atomically $ STM.modifyTVar' m (M.insert (order t) t)
14585
146−deleteTodos :: Map Int Todo -> Todo -> Handler ()
147−deleteTodos m t = liftIO . atomically $ S.insert m t
86+deleteAllTodos :: DB -> Handler ()
87+deleteAllTodos m = liftIO . STM.atomically $ STM.writeTVar m M.empty
88+
89+main1 :: IO ()
90+main1 = do
91+ db <- STM.newTVarIO M.empty
92+ tId <- forkIO $ run 8000 $ serve (Proxy :: Proxy TodosAPI) $ serverTodos db
93+ _ <- serversEqual (Proxy :: Proxy TodosAPI)
94+ (BaseUrl Http "localhost" 8000 "")
95+ (BaseUrl Http "localhost" 8001 "")
96+ 1000
97+ killThread tId
14898```
14999
150−(We're keeping the `Todo`s in memory for simplicity - if this were a production
151− application, we'd likely want to use a database.)
100+(We're keeping the `Todo`s in an `MVar` for simplicity. If this were a
101+ production application, we'd likely want to use a database.)
152102
153103Notice that we split up the API into two sub-APIs. Partly this makes things
154104cleaner and more readable, but there's also a more concrete benefit: we can
@@ -160,7 +110,16 @@ In order to check how we're doing, we need to add an `Arbitrary` instance for
160110
161111``` haskell
162112instance Arbitrary Todo where
163− arbitrary = Todo <$> arbitrary <$> arbitrary <$> arbitrary <$> arbitrary
113+ arbitrary = Todo <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
164114```
165115
116+Let's try it out. First we need to get the old application running:
117+
118+``` bash
119+git clone https://github.com/mihirk/todo-backend-django.git
120+cd todo-backend-django
121+virtualenv venv
122+. ./venv/bin/activate
123+python manage.py runserver localhost:8001
124+```
166125
doc/doc.cabal+23−1
@@ -12,6 +12,28 @@ library
1212 other-extensions: DataKinds, TypeOperators
1313 build-depends: base >=4.8 && <4.9
1414 , servant-server == 0.7.*
15−
15+ , servant-quickcheck
16+ , servant-client
17+ , QuickCheck
18+ , stm
19+ , containers
20+ , transformers
21+ , warp
22+ , aeson
1623 ghc-options: -Wall -Werror -pgmL markdown-unlit
1724 default-language: Haskell2010
25+
26+executable doc
27+ main-is: Main.hs
28+ build-depends: base >=4.8 && <4.9
29+ , servant-server == 0.7.*
30+ , servant-quickcheck
31+ , servant-client
32+ , QuickCheck
33+ , stm
34+ , containers
35+ , transformers
36+ , warp
37+ , aeson
38+ default-language: Haskell2010
39+ ghc-options: -Wall -Werror -pgmL markdown-unlit
src/Servant/QuickCheck.hs+5
@@ -89,6 +89,11 @@ module Servant.QuickCheck
8989 , never500s
9090 , onlyJsonObjects
9191
92+ -- ** Re-exports
93+ , BaseUrl(..)
94+ , Scheme(..)
95+
9296 ) where
9397
9498import Servant.QuickCheck.Internal
99+import Servant.Client (BaseUrl(..), Scheme(..))
src/Servant/QuickCheck/Internal/Testable.hs+8−5
@@ -27,7 +27,8 @@ data ShouldMatch a = ShouldMatch
2727 , smBaseUrls :: (BaseUrl, BaseUrl)
2828 } deriving (Functor, Generic)
2929
30−instance (Show a, Eq a) => Testable (ShouldMatch (FinalClient a)) where
30+instance {-# OVERLAPPING #-} (Show a, Eq a)
31+ => Testable (ShouldMatch (FinalClient a)) where
3132 property sm = ioProperty $ do
3233 let (burl1, burl2) = smBaseUrls sm
3334 e1' <- runExceptT $ smClient sm (smManager sm) burl1
@@ -44,7 +45,7 @@ instance (Show a, Eq a) => Testable (ShouldMatch (FinalClient a)) where
4445 ++ "\nLHS:\n" ++ show err1
4546 ++ "\nRHS:\n" ++ show err2
4647
47−instance (Arbitrary a, Show a, Testable (ShouldMatch b))
48+instance {-# OVERLAPPABLE #-} (Arbitrary a, Show a, Testable (ShouldMatch b))
4849 => Testable (ShouldMatch (a -> b)) where
4950 property sm = forAllShrink arbitrary shrink go
5051 where go x = ($ x) <$> sm
@@ -63,7 +64,8 @@ data ShouldSatisfy filter expect a = ShouldSatisfy
6364 , ssBaseUrl :: BaseUrl
6465 } deriving (Functor)
6566
66−instance (Show a, Eq a, HasPredicate expect (Either ServantError a))
67+instance {-# OVERLAPPING #-}
68+ (Show a, Eq a, HasPredicate expect (Either ServantError a))
6769 => Testable (ShouldSatisfy filter expect (FinalClient a)) where
6870 property ss = ioProperty $ do
6971 a' <- runExceptT $ ssVal ss (ssManager ss) (ssBaseUrl ss)
@@ -72,8 +74,9 @@ instance (Show a, Eq a, HasPredicate expect (Either ServantError a))
7274 Just (x', _) -> return $ Just (x', show a')
7375 return $ getPredicate (ssExpect ss) a'
7476
75−instance ( Arbitrary a, Show a, Testable (ShouldSatisfy filter expect b)
76− , HasPredicate filter a)
77+instance {-# OVERLAPPABLE #-}
78+ ( Arbitrary a, Show a, Testable (ShouldSatisfy filter expect b)
79+ , HasPredicate filter a)
7780 => Testable (ShouldSatisfy filter expect (a -> b)) where
7881 property ss = forAllShrink arbitrary shrink go
7982 where go x | getPredicate (ssFilter ss) x = ($ x) <$> ss