@@ -3,8 +3,10 @@
33{-# LANGUAGE DeriveGeneric #-}
44{-# LANGUAGE FlexibleInstances #-}
55{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE PolyKinds #-}
67{-# LANGUAGE ScopedTypeVariables #-}
78{-# LANGUAGE TypeOperators #-}
9+{-# LANGUAGE TypeFamilies #-}
810{-# LANGUAGE TypeSynonymInstances #-}
911{-# LANGUAGE FlexibleInstances #-}
1012
@@ -13,7 +15,7 @@ module Servant.ServerSpec where
1315#if !MIN_VERSION_base(4,8,0)
1416import Control.Applicative ((<$>))
1517#endif
16−import Control.Monad (forM_, when)
18+import Control.Monad (forM_, when, unless)
1719import Control.Monad.Trans.Except (ExceptT, throwE)
1820import Data.Aeson (FromJSON, ToJSON, decode', encode)
1921import Data.ByteString.Conversion ()
@@ -23,82 +25,144 @@ import Data.String (fromString)
2325import Data.String.Conversions (cs)
2426import qualified Data.Text as T
2527import GHC.Generics (Generic)
26−import Network.HTTP.Types (hAccept, hContentType,
27− methodDelete, methodGet, methodHead,
28− methodPatch, methodPost, methodPut,
29− ok200, parseQuery, Status(..))
28+import Network.HTTP.Types (Status (..), hAccept, hContentType,
29+ methodDelete, methodGet,
30+ methodHead, methodPatch,
31+ methodPost, methodPut, ok200,
32+ parseQuery)
3033import Network.Wai (Application, Request, pathInfo,
3134 queryString, rawQueryString,
32− responseLBS, responseBuilder)
33−import Network.Wai.Internal (Response(ResponseBuilder))
35+ responseBuilder, responseLBS)
36+import Network.Wai.Internal (Response (ResponseBuilder))
3437import Network.Wai.Test (defaultRequest, request,
35− runSession, simpleBody)
38+ runSession, simpleBody,
39+ simpleHeaders, simpleStatus)
3640import Servant.API ((:<|>) (..), (:>), Capture, Delete,
37− Get, Header (..), Headers,
38− HttpVersion, IsSecure (..), JSON,
39− Patch, PlainText, Post, Put,
41+ Get, Header (..),
42+ Headers, HttpVersion,
43+ IsSecure (..), JSON,
44+ NoContent (..), Patch, PlainText,
45+ Post, Put,
4046 QueryFlag, QueryParam, QueryParams,
41− Raw, RemoteHost, ReqBody, GetNoContent,
42− PostNoContent, addHeader, NoContent(..))
43−import Servant.Server (Server, serve, ServantErr(..), err404)
44−import Test.Hspec (Spec, describe, it, shouldBe)
47+ Raw, RemoteHost, ReqBody,
48+ StdMethod (..), Verb, addHeader)
49+import Servant.Server (ServantErr (..), Server, err404,
50+ serve)
51+import Test.Hspec (Spec, context, describe, it,
52+ shouldBe, shouldContain)
4553import Test.Hspec.Wai (get, liftIO, matchHeaders,
46− matchStatus, post, request,
54+ matchStatus, request,
4755 shouldRespondWith, with, (<:>))
48−import Servant.Server.Internal.RoutingApplication (toApplication, RouteResult(..))
56+
57+import Servant.Server.Internal.RoutingApplication
58+ (toApplication, RouteResult(..))
4959import Servant.Server.Internal.Router
5060 (tweakResponse, runRouter,
5161 Router, Router'(LeafRouter))
5262
5363
54−-- * test data types
55−
56−data Person = Person {
57− name :: String,
58− age :: Integer
59− }
60− deriving (Eq, Show, Generic)
61−
62−instance ToJSON Person
63−instance FromJSON Person
64−
65−alice :: Person
66−alice = Person "Alice" 42
67−
68−data Animal = Animal {
69− species :: String,
70− numberOfLegs :: Integer
71− }
72− deriving (Eq, Show, Generic)
73−
74−instance ToJSON Animal
75−instance FromJSON Animal
76−
77−jerry :: Animal
78−jerry = Animal "Mouse" 4
79−
80−tweety :: Animal
81−tweety = Animal "Bird" 2
82−
83−
84−-- * specs
64+-- * Specs
8565
8666spec :: Spec
8767spec = do
68+ verbSpec
8869 captureSpec
89− getSpec
90− headSpec
91− postSpec
92− putSpec
93− patchSpec
9470 queryParamSpec
71+ reqBodySpec
9572 headerSpec
9673 rawSpec
97− unionSpec
98− routerSpec
74+ alternativeSpec
9975 responseHeadersSpec
100− miscReqCombinatorsSpec
101−
76+ routerSpec
77+ miscCombinatorSpec
78+
79+------------------------------------------------------------------------------
80+-- * verbSpec {{{
81+------------------------------------------------------------------------------
82+
83+type VerbApi method status
84+ = Verb method status '[JSON] Person
85+ :<|> "noContent" :> Verb method status '[JSON] NoContent
86+ :<|> "header" :> Verb method status '[JSON] (Headers '[Header "H" Int] Person)
87+ :<|> "headerNC" :> Verb method status '[JSON] (Headers '[Header "H" Int] NoContent)
88+
89+verbSpec :: Spec
90+verbSpec = describe "Servant.API.Verb" $ do
91+ let server :: Server (VerbApi method status)
92+ server = return alice
93+ :<|> return NoContent
94+ :<|> return (addHeader 5 alice)
95+ :<|> return (addHeader 10 NoContent)
96+ get200 = Proxy :: Proxy (VerbApi 'GET 200)
97+ post210 = Proxy :: Proxy (VerbApi 'POST 210)
98+ put203 = Proxy :: Proxy (VerbApi 'PUT 203)
99+ delete280 = Proxy :: Proxy (VerbApi 'DELETE 280)
100+ patch214 = Proxy :: Proxy (VerbApi 'PATCH 214)
101+ wrongMethod m = if m == methodPatch then methodPost else methodPatch
102+ test desc api method (status :: Int) = context desc $
103+
104+ with (return $ serve api server) $ do
105+
106+ -- HEAD and 214/215 need not return bodies
107+ unless (status `elem` [214, 215] || method == methodHead) $
108+ it "returns the person" $ do
109+ response <- Test.Hspec.Wai.request method "/" [] ""
110+ liftIO $ statusCode (simpleStatus response) `shouldBe` status
111+ liftIO $ decode' (simpleBody response) `shouldBe` Just alice
112+
113+ it "returns no content on NoContent" $ do
114+ response <- Test.Hspec.Wai.request method "/noContent" [] ""
115+ liftIO $ statusCode (simpleStatus response) `shouldBe` status
116+ liftIO $ simpleBody response `shouldBe` ""
117+
118+ -- HEAD should not return body
119+ when (method == methodHead) $
120+ it "HEAD returns no content body" $ do
121+ response <- Test.Hspec.Wai.request method "/" [] ""
122+ liftIO $ simpleBody response `shouldBe` ""
123+
124+ it "throws 405 on wrong method " $ do
125+ Test.Hspec.Wai.request (wrongMethod method) "/" [] ""
126+ `shouldRespondWith` 405
127+
128+ it "returns headers" $ do
129+ response1 <- Test.Hspec.Wai.request method "/header" [] ""
130+ liftIO $ statusCode (simpleStatus response1) `shouldBe` status
131+ liftIO $ simpleHeaders response1 `shouldContain` [("H", "5")]
132+
133+ response2 <- Test.Hspec.Wai.request method "/header" [] ""
134+ liftIO $ statusCode (simpleStatus response2) `shouldBe` status
135+ liftIO $ simpleHeaders response2 `shouldContain` [("H", "5")]
136+
137+ it "handles trailing '/' gracefully" $ do
138+ response <- Test.Hspec.Wai.request method "/headerNC/" [] ""
139+ liftIO $ statusCode (simpleStatus response) `shouldBe` status
140+
141+ it "returns 406 if the Accept header is not supported" $ do
142+ Test.Hspec.Wai.request method "" [(hAccept, "crazy/mime")] ""
143+ `shouldRespondWith` 406
144+
145+ it "responds if the Accept header is supported" $ do
146+ response <- Test.Hspec.Wai.request method ""
147+ [(hAccept, "application/json")] ""
148+ liftIO $ statusCode (simpleStatus response) `shouldBe` status
149+
150+ it "sets the Content-Type header" $ do
151+ response <- Test.Hspec.Wai.request method "" [] ""
152+ liftIO $ simpleHeaders response `shouldContain`
153+ [("Content-Type", "application/json")]
154+
155+ test "GET 200" get200 methodGet 200
156+ test "POST 210" post210 methodPost 210
157+ test "PUT 203" put203 methodPut 203
158+ test "DELETE 280" delete280 methodDelete 280
159+ test "PATCH 214" patch214 methodPatch 214
160+ test "GET 200 with HEAD" get200 methodHead 200
161+
162+-- }}}
163+------------------------------------------------------------------------------
164+-- * captureSpec {{{
165+------------------------------------------------------------------------------
102166
103167type CaptureApi = Capture "legs" Integer :> Get '[JSON] Animal
104168captureApi :: Proxy CaptureApi
@@ -128,68 +192,10 @@ captureSpec = do
128192 it "strips the captured path snippet from pathInfo" $ do
129193 get "/captured/foo" `shouldRespondWith` (fromString (show ["foo" :: String]))
130194
131−
132−type GetApi = Get '[JSON] Person
133− :<|> "empty" :> GetNoContent '[JSON] NoContent
134− :<|> "emptyWithHeaders" :> GetNoContent '[JSON] (Headers '[Header "H" Int] NoContent)
135− :<|> "post" :> PostNoContent '[JSON] NoContent
136−
137−getApi :: Proxy GetApi
138−getApi = Proxy
139−
140−getSpec :: Spec
141−getSpec = do
142− describe "Servant.API.Get" $ do
143− let server = return alice
144− :<|> return NoContent
145− :<|> return (addHeader 5 NoContent)
146− :<|> return NoContent
147−
148− with (return $ serve getApi server) $ do
149−
150− it "allows to GET a Person" $ do
151− response <- get "/"
152− return response `shouldRespondWith` 200
153− liftIO $ decode' (simpleBody response) `shouldBe` Just alice
154−
155− it "throws 405 (wrong method) on POSTs" $ do
156− post "/" "" `shouldRespondWith` 405
157− post "/empty" "" `shouldRespondWith` 405
158−
159− it "returns headers" $ do
160− get "/emptyWithHeaders" `shouldRespondWith` 204 { matchHeaders = [ "H" <:> "5" ] }
161−
162− it "returns 406 if the Accept header is not supported" $ do
163− Test.Hspec.Wai.request methodGet "" [(hAccept, "crazy/mime")] ""
164− `shouldRespondWith` 406
165−
166−
167−headSpec :: Spec
168−headSpec = do
169− describe "Servant.API.Head" $ do
170− let server = return alice
171− :<|> return NoContent
172− :<|> return (addHeader 5 NoContent)
173− :<|> return NoContent
174− with (return $ serve getApi server) $ do
175−
176− it "allows to GET a Person" $ do
177− response <- Test.Hspec.Wai.request methodHead "/" [] ""
178− return response `shouldRespondWith` 200
179− liftIO $ decode' (simpleBody response) `shouldBe` (Nothing :: Maybe Person)
180−
181− it "does not allow HEAD to POST route" $ do
182− response <- Test.Hspec.Wai.request methodHead "/post" [] ""
183− return response `shouldRespondWith` 405
184−
185− it "throws 405 (wrong method) on POSTs" $ do
186− post "/" "" `shouldRespondWith` 405
187− post "/empty" "" `shouldRespondWith` 405
188−
189− it "returns 406 if the Accept header is not supported" $ do
190− Test.Hspec.Wai.request methodHead "" [(hAccept, "crazy/mime")] ""
191− `shouldRespondWith` 406
192−
195+-- }}}
196+------------------------------------------------------------------------------
197+-- * queryParamSpec {{{
198+------------------------------------------------------------------------------
193199
194200type QueryParamApi = QueryParam "name" String :> Get '[JSON] Person
195201 :<|> "a" :> QueryParams "names" String :> Get '[JSON] Person
@@ -274,122 +280,41 @@ queryParamSpec = do
274280 name = "Alice"
275281 }
276282
277−type PostApi =
278− ReqBody '[JSON] Person :> Post '[JSON] Integer
279− :<|> "bla" :> ReqBody '[JSON] Person :> Post '[JSON] Integer
280− :<|> "empty" :> Post '[JSON] ()
281−
282−postApi :: Proxy PostApi
283−postApi = Proxy
284−
285−postSpec :: Spec
286−postSpec = do
287− describe "Servant.API.Post and .ReqBody" $ do
288− let server = return . age :<|> return . age :<|> return ()
289− with (return $ serve postApi server) $ do
290− let post' x = Test.Hspec.Wai.request methodPost x [(hContentType
291− , "application/json;charset=utf-8")]
292−
293− it "allows to POST a Person" $ do
294− post' "/" (encode alice) `shouldRespondWith` "42"{
295− matchStatus = 200
296− }
297−
298− it "allows alternative routes if all have request bodies" $ do
299− post' "/bla" (encode alice) `shouldRespondWith` "42"{
300− matchStatus = 200
301− }
302−
303− it "handles trailing '/' gracefully" $ do
304− post' "/bla/" (encode alice) `shouldRespondWith` "42"{
305− matchStatus = 200
306− }
283+-- }}}
284+------------------------------------------------------------------------------
285+-- * reqBodySpec {{{
286+------------------------------------------------------------------------------
287+type ReqBodyApi = ReqBody '[JSON] Person :> Post '[JSON] Person
288+ :<|> "blah" :> ReqBody '[JSON] Person :> Put '[JSON] Integer
307289
308− it "correctly rejects invalid request bodies with status 400" $ do
309− post' "/" "some invalid body" `shouldRespondWith` 400
310−
311− it "responds with 415 if the request body media type is unsupported" $ do
312− let post'' x = Test.Hspec.Wai.request methodPost x [(hContentType
313− , "application/nonsense")]
314− post'' "/" "anything at all" `shouldRespondWith` 415
315−
316−type PutApi =
317− ReqBody '[JSON] Person :> Put '[JSON] Integer
318− :<|> "bla" :> ReqBody '[JSON] Person :> Put '[JSON] Integer
319− :<|> "empty" :> Put '[JSON] ()
320−
321−putApi :: Proxy PutApi
322−putApi = Proxy
323−
324−putSpec :: Spec
325−putSpec = do
326− describe "Servant.API.Put and .ReqBody" $ do
327− let server = return . age :<|> return . age :<|> return ()
328− with (return $ serve putApi server) $ do
329− let put' x = Test.Hspec.Wai.request methodPut x [(hContentType
330− , "application/json;charset=utf-8")]
331−
332− it "allows to put a Person" $ do
333− put' "/" (encode alice) `shouldRespondWith` "42"{
334− matchStatus = 200
335− }
290+reqBodyApi :: Proxy ReqBodyApi
291+reqBodyApi = Proxy
336292
337− it "allows alternative routes if all have request bodies" $ do
338− put' "/bla" (encode alice) `shouldRespondWith` "42"{
339− matchStatus = 200
340− }
293+reqBodySpec :: Spec
294+reqBodySpec = describe "Servant.API.ReqBody" $ do
341295
342− it "handles trailing '/' gracefully" $ do
343− put' "/bla/" (encode alice) `shouldRespondWith` "42"{
344− matchStatus = 200
345− }
296+ let server :: Server ReqBodyApi
297+ server = return :<|> return . age
298+ mkReq method x = Test.Hspec.Wai.request method x
299+ [(hContentType, "application/json;charset=utf-8")]
346300
347− it "correctly rejects invalid request bodies with status 400" $ do
348− put' "/" "some invalid body" `shouldRespondWith` 400
349−
350− it "responds with 415 if the request body media type is unsupported" $ do
351− let put'' x = Test.Hspec.Wai.request methodPut x [(hContentType
352− , "application/nonsense")]
353− put'' "/" "anything at all" `shouldRespondWith` 415
354−
355−type PatchApi =
356− ReqBody '[JSON] Person :> Patch '[JSON] Integer
357− :<|> "bla" :> ReqBody '[JSON] Person :> Patch '[JSON] Integer
358− :<|> "empty" :> Patch '[JSON] ()
359−
360−patchApi :: Proxy PatchApi
361−patchApi = Proxy
362−
363−patchSpec :: Spec
364−patchSpec = do
365− describe "Servant.API.Patch and .ReqBody" $ do
366− let server = return . age :<|> return . age :<|> return ()
367− with (return $ serve patchApi server) $ do
368− let patch' x = Test.Hspec.Wai.request methodPatch x [(hContentType
369− , "application/json;charset=utf-8")]
370−
371− it "allows to patch a Person" $ do
372− patch' "/" (encode alice) `shouldRespondWith` "42"{
373− matchStatus = 200
374− }
301+ with (return $ serve reqBodyApi server) $ do
375302
376− it "allows alternative routes if all have request bodies" $ do
377− patch' "/bla" (encode alice) `shouldRespondWith` "42"{
378− matchStatus = 200
379− }
303+ it "passes the argument to the handler" $ do
304+ response <- mkReq methodPost "" (encode alice)
305+ liftIO $ decode' (simpleBody response) `shouldBe` Just alice
380306
381− it "handles trailing '/' gracefully" $ do
382− patch' "/bla/" (encode alice) `shouldRespondWith` "42"{
383− matchStatus = 200
384− }
307+ it "rejects invalid request bodies with status 400" $ do
308+ mkReq methodPut "/blah" "some invalid body" `shouldRespondWith` 400
385309
386− it "correctly rejects invalid request bodies with status 400" $ do
387− patch' "/" "some invalid body" `shouldRespondWith` 400
310+ it "responds with 415 if the request body media type is unsupported" $ do
311+ Test.Hspec.Wai.request methodPost "/"
312+ [(hContentType, "application/nonsense")] "" `shouldRespondWith` 415
388313
389− it "responds with 415 if the request body media type is unsupported" $ do
390− let patch'' x = Test.Hspec.Wai.request methodPatch x [(hContentType
391− , "application/nonsense")]
392− patch'' "/" "anything at all" `shouldRespondWith` 415
314+-- }}}
315+------------------------------------------------------------------------------
316+-- * headerSpec {{{
317+------------------------------------------------------------------------------
393318
394319type HeaderApi a = Header "MyHeader" a :> Delete '[JSON] ()
395320headerApi :: Proxy (HeaderApi a)
@@ -418,12 +343,19 @@ headerSpec = describe "Servant.API.Header" $ do
418343 it "passes the header to the handler (String)" $
419344 delete' "/" "" `shouldRespondWith` 200
420345
346+-- }}}
347+------------------------------------------------------------------------------
348+-- * rawSpec {{{
349+------------------------------------------------------------------------------
421350
422351type RawApi = "foo" :> Raw
352+
423353rawApi :: Proxy RawApi
424354rawApi = Proxy
355+
425356rawApplication :: Show a => (Request -> a) -> Application
426−rawApplication f request_ respond = respond $ responseLBS ok200 [] (cs $ show $ f request_)
357+rawApplication f request_ respond = respond $ responseLBS ok200 []
358+ (cs $ show $ f request_)
427359
428360rawSpec :: Spec
429361rawSpec = do
@@ -444,7 +376,10 @@ rawSpec = do
444376 liftIO $ do
445377 simpleBody response `shouldBe` cs (show ["bar" :: String])
446378
447−
379+-- }}}
380+------------------------------------------------------------------------------
381+-- * alternativeSpec {{{
382+------------------------------------------------------------------------------
448383type AlternativeApi =
449384 "foo" :> Get '[JSON] Person
450385 :<|> "bar" :> Get '[JSON] Animal
@@ -452,11 +387,12 @@ type AlternativeApi =
452387 :<|> "bar" :> Post '[JSON] Animal
453388 :<|> "bar" :> Put '[JSON] Animal
454389 :<|> "bar" :> Delete '[JSON] ()
455−unionApi :: Proxy AlternativeApi
456−unionApi = Proxy
457390
458−unionServer :: Server AlternativeApi
459−unionServer =
391+alternativeApi :: Proxy AlternativeApi
392+alternativeApi = Proxy
393+
394+alternativeServer :: Server AlternativeApi
395+alternativeServer =
460396 return alice
461397 :<|> return jerry
462398 :<|> return "a string"
@@ -464,10 +400,10 @@ unionServer =
464400 :<|> return jerry
465401 :<|> return ()
466402
467−unionSpec :: Spec
468−unionSpec = do
403+alternativeSpec :: Spec
404+alternativeSpec = do
469405 describe "Servant.API.Alternative" $ do
470− with (return $ serve unionApi unionServer) $ do
406+ with (return $ serve alternativeApi alternativeServer) $ do
471407
472408 it "unions endpoints" $ do
473409 response <- get "/foo"
@@ -484,7 +420,10 @@ unionSpec = do
484420
485421 it "returns 404 if the path does not exist" $ do
486422 get "/nonexistent" `shouldRespondWith` 404
487−
423+-- }}}
424+------------------------------------------------------------------------------
425+-- * responseHeaderSpec {{{
426+------------------------------------------------------------------------------
488427type ResponseHeadersApi =
489428 Get '[JSON] (Headers '[Header "H1" Int, Header "H2" String] String)
490429 :<|> Post '[JSON] (Headers '[Header "H1" Int, Header "H2" String] String)
@@ -501,26 +440,29 @@ responseHeadersSpec :: Spec
501440responseHeadersSpec = describe "ResponseHeaders" $ do
502441 with (return $ serve (Proxy :: Proxy ResponseHeadersApi) responseHeadersServer) $ do
503442
504− let methods = [(methodGet, 200), (methodPost, 200), (methodPut, 200), (methodPatch, 200)]
443+ let methods = [methodGet, methodPost, methodPut, methodPatch]
505444
506445 it "includes the headers in the response" $
507− forM_ methods $ \(method, expected) ->
446+ forM_ methods $ \method ->
508447 Test.Hspec.Wai.request method "/" [] ""
509448 `shouldRespondWith` "\"hi\""{ matchHeaders = ["H1" <:> "5", "H2" <:> "kilroy"]
510− , matchStatus = expected
449+ , matchStatus = 200
511450 }
512451
513452 it "responds with not found for non-existent endpoints" $
514− forM_ methods $ \(method,_) ->
453+ forM_ methods $ \method ->
515454 Test.Hspec.Wai.request method "blahblah" [] ""
516455 `shouldRespondWith` 404
517456
518457 it "returns 406 if the Accept header is not supported" $
519− forM_ methods $ \(method,_) ->
458+ forM_ methods $ \method ->
520459 Test.Hspec.Wai.request method "" [(hAccept, "crazy/mime")] ""
521460 `shouldRespondWith` 406
522461
523−
462+-- }}}
463+------------------------------------------------------------------------------
464+-- * routerSpec {{{
465+------------------------------------------------------------------------------
524466routerSpec :: Spec
525467routerSpec = do
526468 describe "Servant.Server.Internal.Router" $ do
@@ -539,6 +481,10 @@ routerSpec = do
539481 it "calls f on route result" $ do
540482 get "" `shouldRespondWith` 202
541483
484+-- }}}
485+------------------------------------------------------------------------------
486+-- * miscCombinatorSpec {{{
487+------------------------------------------------------------------------------
542488type MiscCombinatorsAPI
543489 = "version" :> HttpVersion :> Get '[JSON] String
544490 :<|> "secure" :> IsSecure :> Get '[JSON] String
@@ -557,8 +503,8 @@ miscServ = versionHandler
557503 secureHandler NotSecure = return "not secure"
558504 hostHandler = return . show
559505
560−miscReqCombinatorsSpec :: Spec
561−miscReqCombinatorsSpec = with (return $ serve miscApi miscServ) $
506+miscCombinatorSpec :: Spec
507+miscCombinatorSpec = with (return $ serve miscApi miscServ) $
562508 describe "Misc. combinators for request inspection" $ do
563509 it "Successfully gets the HTTP version specified in the request" $
564510 go "/version" "\"HTTP/1.0\""
@@ -570,3 +516,35 @@ miscReqCombinatorsSpec = with (return $ serve miscApi miscServ) $
570516 go "/host" "\"0.0.0.0:0\""
571517
572518 where go path res = Test.Hspec.Wai.get path `shouldRespondWith` res
519+-- }}}
520+------------------------------------------------------------------------------
521+-- * Test data types {{{
522+------------------------------------------------------------------------------
523+
524+data Person = Person {
525+ name :: String,
526+ age :: Integer
527+ }
528+ deriving (Eq, Show, Generic)
529+
530+instance ToJSON Person
531+instance FromJSON Person
532+
533+alice :: Person
534+alice = Person "Alice" 42
535+
536+data Animal = Animal {
537+ species :: String,
538+ numberOfLegs :: Integer
539+ }
540+ deriving (Eq, Show, Generic)
541+
542+instance ToJSON Animal
543+instance FromJSON Animal
544+
545+jerry :: Animal
546+jerry = Animal "Mouse" 4
547+
548+tweety :: Animal
549+tweety = Animal "Bird" 2
550+-- }}}