@@ -20,98 +20,34 @@ don't need to know anything about Django or Python to follow along; indeed,
20part of the fun of it is using `serversEqual` to guide you through
20part of the fun of it is using `serversEqual` to guide you through
21re-implementing code you may not entirely understand.
21re-implementing code you may not entirely understand.
23−Looking at the code, we can see the routes in `urls.py`:
23+Looking around the codebase (`urls.py`, `models.py` and `views.py` in
24−
24+particular) may lead us to a first attempt at a rewrite:
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()),
33−And the handlers in `views.py`:
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)
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))
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)
52− def delete(self, request, format=None):
53− TodoItem.objects.all().delete()
54− return JSONResponse(None, status=status.HTTP_204_NO_CONTENT)
57− def get(self, request, pk, format=None):
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)
65− def delete(self, request, pk, format=None):
67− todoItem = TodoItem.objects.get(pk=pk)
69− except TodoItem.DoesNotExist:
70− return JSONResponse(None, status=status.HTTP_400_BAD_REQUEST)
71− return JSONResponse(None, status=status.HTTP_204_NO_CONTENT)
73− def patch(self, request, pk, format=None):
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():
81− return JSONResponse(serializer.data, status=status.HTTP_200_OK)
82− return JSONResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
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)
97−So as a first pass, let's try:
99``` haskell
26``` haskell
100{-# LANGUAGE DataKinds #-}
27{-# LANGUAGE DataKinds #-}
28+{-# LANGUAGE DeriveAnyClass #-}
29+{-# LANGUAGE DeriveGeneric #-}
101{-# LANGUAGE TypeOperators #-}
30{-# LANGUAGE TypeOperators #-}
32+module ServersEqual where
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)
103import Servant
41import Servant
104−import Servant.QuickCheck
42+import Servant.QuickCheck (serversEqual, BaseUrl(..), Scheme(..))
105−import STMContainers.Map as M
106−import GHC.Conc (atomically)
107−import Test.QuickCheck
43+import Test.QuickCheck (Arbitrary(..))
109data Todo = Todo
45data Todo = Todo
110 { title :: String
46 { title :: String
111 , completed :: Bool
47 , completed :: Bool
112 , url :: String
48 , url :: String
113 , order :: Int
49 , order :: Int
114− } deriving (Eq, Show, Read)
50+ } deriving (Eq, Show, Read, Generic, ToJSON, FromJSON)
116type API = TodosAPI :<|> TodoAPI
52type API = TodosAPI :<|> TodoAPI
@@ -125,30 +61,44 @@ type TodoAPI
125 = "todo" :> Capture "id " Int :>
61 = "todo" :> Capture "id " Int :>
126 ( Get '[JSON] Todo
62 ( Get '[JSON] Todo
127 :<|> ReqBody '[JSON] Todo :> Patch '[JSON] ()
63 :<|> ReqBody '[JSON] Todo :> Patch '[JSON] ()
128− :<|> Delete '[JSON} ())
64+ :<|> Delete '[JSON] ())
130−serverTodos :: Server TodosAPI
66+serverTodos :: DB -> Server TodosAPI
131serverTodos tvar = getTodos tvar
67serverTodos tvar = getTodos tvar
132 :<|> postTodos tvar
68 :<|> postTodos tvar
133 :<|> deleteAllTodos tvar
69 :<|> deleteAllTodos tvar
135serverTodo :: Server TodoAPI
72serverTodo :: Server TodoAPI
136serverTodo id' = getTodo tvar id'
73serverTodo id' = getTodo tvar id'
137 :<|> patchTodo tvar id'
74 :<|> patchTodo tvar id'
138 :<|> deleteTodo tvar id'
75 :<|> deleteTodo tvar id'
78+type DB = TVar (M.IntMap Todo)
140−getTodos :: Map Int Todo -> Handler [Todo]
80+getTodos :: DB -> Handler [Todo]
141−getTodos m = liftIO . atomically . toList $ S.stream m
81+getTodos m = liftIO $ M.elems <$> STM.readTVarIO m
143−postTodos :: Map Int Todo -> Todo -> Handler ()
83+postTodos :: DB -> Todo -> Handler ()
144−postTodos m t = liftIO . atomically $ S.insert m t
84+postTodos m t = liftIO . STM.atomically $ STM.modifyTVar' m (M.insert (order t) t)
146−deleteTodos :: Map Int Todo -> Todo -> Handler ()
86+deleteAllTodos :: DB -> Handler ()
147−deleteTodos m t = liftIO . atomically $ S.insert m t
87+deleteAllTodos m = liftIO . STM.atomically $ STM.writeTVar m M.empty
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 "")
150−(We're keeping the `Todo`s in memory for simplicity - if this were a production
100+(We're keeping the `Todo`s in an `MVar` for simplicity. If this were a
151− application, we'd likely want to use a database.)
101+ production application, we'd likely want to use a database.)
153Notice that we split up the API into two sub-APIs. Partly this makes things
103Notice that we split up the API into two sub-APIs. Partly this makes things
154cleaner and more readable, but there's also a more concrete benefit: we can
104cleaner 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
161``` haskell
111``` haskell
162instance Arbitrary Todo where
112instance Arbitrary Todo where
163− arbitrary = Todo <$> arbitrary <$> arbitrary <$> arbitrary <$> arbitrary
113+ arbitrary = Todo <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
116+Let's try it out. First we need to get the old application running:
119+git clone https://github.com/mihirk/todo-backend-django.git
120+cd todo-backend-django
122+. ./venv/bin/activate
123+python manage.py runserver localhost:8001