git.iko.soy/typeable/servant
on master help sign in
Merge pull request #9 from haskell-servant/jkarni/content-types

Jkarni/content types

commit5f1e8c3607e974ed9793a4c06c724b6614a6c174 authorJulian Arni <jkarni@gmail.com> dateFri 20 Feb 2015, 15:37 (12 years ago) parent784660cb02b67b
15 files changed+526−486
default.nix+12 servant.cabal+29−4 src/Servant/API.hs+5−3 src/Servant/API/ContentTypes.hs+220 src/Servant/API/Get.hs+4−2 src/Servant/API/Patch.hs+5−3 src/Servant/API/Post.hs+4−2 src/Servant/API/Put.hs+4−2 src/Servant/API/Raw.hs+3−1 src/Servant/API/ReqBody.hs+3−2 src/Servant/QQ.hs−228 src/Servant/Utils/Links.hs+34−19 test/Servant/API/ContentTypesSpec.hs+182 test/Servant/QQSpec.hs−209 test/Servant/Utils/LinksSpec.hs+21−11
default.nixnew file+12
@@ -0,0 +1,12 @@
1+{ pkgs ? import <nixpkgs> { config.allowUnfree = true; }
2+, src ? builtins.filterSource (path: type:
3+ type != "unknown" &&
4+ baseNameOf path != ".git" &&
5+ baseNameOf path != "result" &&
6+ baseNameOf path != "dist") ./.
7+}:
8+pkgs.haskellPackages.buildLocalCabalWithArgs {
9+ name = "servant";
10+ inherit src;
11+ args = {};
12+}
servant.cabal+29−4
@@ -28,6 +28,7 @@ library
2828 Servant.API
2929 Servant.API.Alternative
3030 Servant.API.Capture
31+ Servant.API.ContentTypes
3132 Servant.API.Delete
3233 Servant.API.Get
3334 Servant.API.Header
@@ -40,17 +41,39 @@ library
4041 Servant.API.ReqBody
4142 Servant.API.Sub
4243 Servant.Common.Text
43− Servant.QQ
4444 Servant.Utils.Links
4545 build-depends:
4646 base >=4.7 && <5
47− , text >= 1
48− , template-haskell
47+ , aeson >= 0.7
48+ , bytestring == 0.10.*
49+ , http-media >= 0.4 && < 0.6
50+ , http-types == 0.8.*
51+ , text >= 1 && < 2
52+ , template-haskell >= 2.7 && < 2.10
4953 , parsec >= 3.1
50− , string-conversions >= 0.3
54+ , string-conversions >= 0.3 && < 0.4
5155 , network-uri >= 2.6
5256 hs-source-dirs: src
5357 default-language: Haskell2010
58+ other-extensions: ConstraintKinds
59+ , DataKinds
60+ , DeriveDataTypeable
61+ , FlexibleInstances
62+ , FunctionalDependencies
63+ , GADTs
64+ , KindSignatures
65+ , MultiParamTypeClasses
66+ , OverlappingInstances
67+ , OverloadedStrings
68+ , PolyKinds
69+ , QuasiQuotes
70+ , RecordWildCards
71+ , ScopedTypeVariables
72+ , TemplateHaskell
73+ , TypeFamilies
74+ , TypeOperators
75+ , TypeSynonymInstances
76+ , UndecidableInstances
5477 ghc-options: -Wall
5578
5679test-suite spec
@@ -62,6 +85,8 @@ test-suite spec
6285 main-is: Spec.hs
6386 build-depends:
6487 base == 4.*
88+ , aeson
89+ , bytestring
6590 , hspec == 2.*
6691 , QuickCheck
6792 , parsec
src/Servant/API.hs+5−3
@@ -30,19 +30,22 @@ module Servant.API (
3030 -- | PATCH requests
3131 module Servant.API.Patch,
3232
33+ -- * Content Types
34+ module Servant.API.ContentTypes,
35+
3336 -- * Untyped endpoints
3437 -- | Plugging in a wai 'Network.Wai.Application', serving directories
3538 module Servant.API.Raw,
3639
3740 -- * Utilities
38− -- | QuasiQuotes for endpoints
39− module Servant.QQ,
4041 -- | Type-safe internal URIs
4142 module Servant.Utils.Links,
4243 ) where
4344
4445import Servant.API.Alternative ( (:<|>)(..) )
4546import Servant.API.Capture ( Capture )
47+import Servant.API.ContentTypes ( JSON , PlainText, OctetStream
48+ , MimeRender(..) , MimeUnrender(..))
4649import Servant.API.Delete ( Delete )
4750import Servant.API.Get ( Get )
4851import Servant.API.Header ( Header )
@@ -54,5 +57,4 @@ import Servant.API.MatrixParam ( MatrixFlag, MatrixParams, MatrixParam )
5457import Servant.API.Raw ( Raw )
5558import Servant.API.ReqBody ( ReqBody )
5659import Servant.API.Sub ( (:>)(..) )
57−import Servant.QQ ( sitemap )
5860import Servant.Utils.Links ( safeLink, URI(..), IsElem, IsElem', HasLink(..) )
src/Servant/API/ContentTypes.hsnew file+220
@@ -0,0 +1,220 @@
1+{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DeriveDataTypeable #-}
4+{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE MultiParamTypeClasses #-}
6+{-# LANGUAGE OverloadedStrings #-}
7+{-# LANGUAGE PolyKinds #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TypeFamilies #-}
10+{-# LANGUAGE TypeOperators #-}
11+{-# LANGUAGE UndecidableInstances #-}
12+module Servant.API.ContentTypes where
13+
14+import Control.Arrow (left)
15+import Data.Aeson (FromJSON, ToJSON, eitherDecode,
16+ encode)
17+import qualified Data.ByteString as BS
18+import Data.ByteString.Lazy (ByteString, fromStrict, toStrict)
19+import Data.String.Conversions (cs)
20+import qualified Data.Text.Lazy as TextL
21+import qualified Data.Text.Lazy.Encoding as TextL
22+import qualified Data.Text as TextS
23+import qualified Data.Text.Encoding as TextS
24+import Data.Typeable
25+import GHC.Exts (Constraint)
26+import qualified Network.HTTP.Media as M
27+
28+-- * Provided content types
29+data JSON deriving Typeable
30+data PlainText deriving Typeable
31+data OctetStream deriving Typeable
32+
33+-- * Accept class
34+
35+-- | Instances of 'Accept' represent mimetypes. They are used for matching
36+-- against the @Accept@ HTTP header of the request, and for setting the
37+-- @Content-Type@ header of the response
38+--
39+-- Example:
40+--
41+-- > instance Accept HTML where
42+-- > contentType _ = "text" // "html"
43+--
44+class Accept ctype where
45+ contentType :: Proxy ctype -> M.MediaType
46+
47+-- | @application/json@
48+instance Accept JSON where
49+ contentType _ = "application" M.// "json"
50+
51+-- | @text/plain;charset=utf-8@
52+instance Accept PlainText where
53+ contentType _ = "text" M.// "plain" M./: ("charset", "utf-8")
54+
55+-- | @application/octet-stream@
56+instance Accept OctetStream where
57+ contentType _ = "application" M.// "octet-stream"
58+
59+newtype AcceptHeader = AcceptHeader BS.ByteString
60+ deriving (Eq, Show)
61+
62+-- * Render (serializing)
63+
64+-- | Instantiate this class to register a way of serializing a type based
65+-- on the @Accept@ header.
66+--
67+-- Example:
68+--
69+-- > data MyContentType
70+-- >
71+-- > instance Accept MyContentType where
72+-- > contentType _ = "example" // "prs.me.mine" /: ("charset", "utf-8")
73+-- >
74+-- > instance Show a => MimeRender MyContentType where
75+-- > toByteString _ val = pack ("This is MINE! " ++ show val)
76+-- >
77+-- > type MyAPI = "path" :> Get '[MyContentType] Int
78+--
79+class Accept ctype => MimeRender ctype a where
80+ toByteString :: Proxy ctype -> a -> ByteString
81+
82+class AllCTRender list a where
83+ -- If the Accept header can be matched, returns (Just) a tuple of the
84+ -- Content-Type and response (serialization of @a@ into the appropriate
85+ -- mimetype).
86+ handleAcceptH :: Proxy list -> AcceptHeader -> a -> Maybe (ByteString, ByteString)
87+
88+instance ( AllMimeRender ctyps a, IsNonEmpty ctyps
89+ ) => AllCTRender ctyps a where
90+ handleAcceptH _ (AcceptHeader accept) val = M.mapAcceptMedia lkup accept
91+ where pctyps = Proxy :: Proxy ctyps
92+ amrs = allMimeRender pctyps val
93+ lkup = fmap (\(a,b) -> (a, (cs $ show a, b))) amrs
94+
95+
96+--------------------------------------------------------------------------
97+-- * Unrender
98+
99+-- | Instantiate this class to register a way of deserializing a type based
100+-- on the request's @Content-Type@ header.
101+--
102+-- > data MyContentType = MyContentType String
103+-- >
104+-- > instance Accept MyContentType where
105+-- > contentType _ = "example" // "prs.me.mine" /: ("charset", "utf-8")
106+-- >
107+-- > instance Show a => MimeUnrender MyContentType where
108+-- > fromByteString _ bs = MyContentType $ unpack bs
109+-- >
110+-- > type MyAPI = "path" :> ReqBody '[MyContentType] :> Get '[JSON] Int
111+--
112+class Accept ctype => MimeUnrender ctype a where
113+ fromByteString :: Proxy ctype -> ByteString -> Either String a
114+
115+class (IsNonEmpty list) => AllCTUnrender list a where
116+ handleCTypeH :: Proxy list
117+ -> ByteString -- Content-Type header
118+ -> ByteString -- Request body
119+ -> Maybe (Either String a)
120+
121+instance ( AllMimeUnrender ctyps a, IsNonEmpty ctyps
122+ ) => AllCTUnrender ctyps a where
123+ handleCTypeH _ ctypeH body = M.mapContentMedia lkup (cs ctypeH)
124+ where lkup = allMimeUnrender (Proxy :: Proxy ctyps) body
125+
126+--------------------------------------------------------------------------
127+-- * Utils (Internal)
128+
129+
130+--------------------------------------------------------------------------
131+-- Check that all elements of list are instances of MimeRender
132+--------------------------------------------------------------------------
133+class AllMimeRender ls a where
134+ allMimeRender :: Proxy ls
135+ -> a -- value to serialize
136+ -> [(M.MediaType, ByteString)] -- content-types/response pairs
137+
138+instance ( MimeRender ctyp a ) => AllMimeRender '[ctyp] a where
139+ allMimeRender _ a = [(contentType pctyp, toByteString pctyp a)]
140+ where pctyp = Proxy :: Proxy ctyp
141+
142+instance ( MimeRender ctyp a
143+ , AllMimeRender (ctyp' ': ctyps) a
144+ ) => AllMimeRender (ctyp ': ctyp' ': ctyps) a where
145+ allMimeRender _ a = (contentType pctyp, toByteString pctyp a)
146+ :(allMimeRender pctyps a)
147+ where pctyp = Proxy :: Proxy ctyp
148+ pctyps = Proxy :: Proxy (ctyp' ': ctyps)
149+
150+
151+instance AllMimeRender '[] a where
152+ allMimeRender _ _ = []
153+
154+--------------------------------------------------------------------------
155+-- Check that all elements of list are instances of MimeUnrender
156+--------------------------------------------------------------------------
157+class AllMimeUnrender ls a where
158+ allMimeUnrender :: Proxy ls -> ByteString -> [(M.MediaType, Either String a)]
159+
160+instance AllMimeUnrender '[] a where
161+ allMimeUnrender _ _ = []
162+
163+instance ( MimeUnrender ctyp a
164+ , AllMimeUnrender ctyps a
165+ ) => AllMimeUnrender (ctyp ': ctyps) a where
166+ allMimeUnrender _ val = (contentType pctyp, fromByteString pctyp val)
167+ :(allMimeUnrender pctyps val)
168+ where pctyp = Proxy :: Proxy ctyp
169+ pctyps = Proxy :: Proxy ctyps
170+
171+type family IsNonEmpty (ls::[*]) :: Constraint where
172+ IsNonEmpty (x ': xs) = ()
173+
174+
175+--------------------------------------------------------------------------
176+-- * MimeRender Instances
177+
178+-- | `encode`
179+instance ToJSON a => MimeRender JSON a where
180+ toByteString _ = encode
181+
182+-- | `TextL.encodeUtf8`
183+instance MimeRender PlainText TextL.Text where
184+ toByteString _ = TextL.encodeUtf8
185+
186+-- | `fromStrict . TextS.encodeUtf8`
187+instance MimeRender PlainText TextS.Text where
188+ toByteString _ = fromStrict . TextS.encodeUtf8
189+
190+-- | `id`
191+instance MimeRender OctetStream ByteString where
192+ toByteString _ = id
193+
194+-- | `toStrict`
195+instance MimeRender OctetStream BS.ByteString where
196+ toByteString _ = fromStrict
197+
198+
199+--------------------------------------------------------------------------
200+-- * MimeUnrender Instances
201+
202+-- | `eitherDecode`
203+instance FromJSON a => MimeUnrender JSON a where
204+ fromByteString _ = eitherDecode
205+
206+-- | `left show . TextL.decodeUtf8'`
207+instance MimeUnrender PlainText TextL.Text where
208+ fromByteString _ = left show . TextL.decodeUtf8'
209+
210+-- | `left show . TextS.decodeUtf8' . toStrict`
211+instance MimeUnrender PlainText TextS.Text where
212+ fromByteString _ = left show . TextS.decodeUtf8' . toStrict
213+
214+-- | `Right . id`
215+instance MimeUnrender OctetStream ByteString where
216+ fromByteString _ = Right . id
217+
218+-- | `Right . toStrict`
219+instance MimeUnrender OctetStream BS.ByteString where
220+ fromByteString _ = Right . toStrict
src/Servant/API/Get.hs+4−2
@@ -1,4 +1,6 @@
11{-# LANGUAGE DeriveDataTypeable #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE KindSignatures #-}
24module Servant.API.Get where
35
46import Data.Typeable ( Typeable )
@@ -7,6 +9,6 @@ import Data.Typeable ( Typeable )
79--
810-- Example:
911--
10−-- > type MyApi = "books" :> Get [Book]
11−data Get a
12+-- > type MyApi = "books" :> Get '[JSON] [Book]
13+data Get (contentTypes::[*]) a
1214 deriving Typeable
src/Servant/API/Patch.hs+5−3
@@ -1,4 +1,6 @@
11{-# LANGUAGE DeriveDataTypeable #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE KindSignatures #-}
24module Servant.API.Patch where
35
46import Data.Typeable ( Typeable )
@@ -11,9 +13,9 @@ import Data.Typeable ( Typeable )
1113--
1214-- Example:
1315--
14−-- > -- POST /books
16+-- > -- PATCH /books
1517-- > -- with a JSON encoded Book as the request body
1618-- > -- returning the just-created Book
17−-- > type MyApi = "books" :> ReqBody Book :> Post Book
18−data Patch a
19+-- > type MyApi = "books" :> ReqBody Book :> Patch '[JSON] Book
20+data Patch (contentTypes::[*]) a
1921 deriving Typeable
src/Servant/API/Post.hs+4−2
@@ -1,4 +1,6 @@
1+{-# LANGUAGE DataKinds #-}
12{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE KindSignatures #-}
24module Servant.API.Post where
35
46import Data.Typeable ( Typeable )
@@ -12,6 +14,6 @@ import Data.Typeable ( Typeable )
1214-- > -- POST /books
1315-- > -- with a JSON encoded Book as the request body
1416-- > -- returning the just-created Book
15−-- > type MyApi = "books" :> ReqBody Book :> Post Book
16−data Post a
17+-- > type MyApi = "books" :> ReqBody Book :> Post '[JSON] Book
18+data Post (contentTypes::[*]) a
1719 deriving Typeable
src/Servant/API/Put.hs+4−2
@@ -1,4 +1,6 @@
1+{-# LANGUAGE DataKinds #-}
12{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE KindSignatures #-}
24module Servant.API.Put where
35
46import Data.Typeable ( Typeable )
@@ -10,6 +12,6 @@ import Data.Typeable ( Typeable )
1012--
1113-- > -- PUT /books/:isbn
1214-- > -- with a Book as request body, returning the updated Book
13−-- > type MyApi = "books" :> Capture "isbn" Text :> ReqBody Book :> Put Book
14−data Put a
15+-- > type MyApi = "books" :> Capture "isbn" Text :> ReqBody Book :> Put '[JSON] Book
16+data Put (contentTypes::[*]) a
1517 deriving Typeable
src/Servant/API/Raw.hs+3−1
@@ -1,5 +1,7 @@
1+{-# LANGUAGE DeriveDataTypeable #-}
12module Servant.API.Raw where
23
4+import Data.Typeable (Typeable)
35-- | Endpoint for plugging in your own Wai 'Application's.
46--
57-- The given 'Application' will get the request as received by the server, potentially with
@@ -9,4 +11,4 @@ module Servant.API.Raw where
911-- this can also be used with 'Servant.Utils.StaticFiles.serveDirectory' to serve
1012-- static files stored in a particular directory on your filesystem, or to serve
1113-- your API's documentation with 'Servant.Utils.StaticFiles.serveDocumentation'.
12−data Raw
14+data Raw deriving Typeable
src/Servant/API/ReqBody.hs+3−2
@@ -1,3 +1,4 @@
1+{-# LANGUAGE DataKinds #-}
12{-# LANGUAGE PolyKinds #-}
23module Servant.API.ReqBody where
34
@@ -6,5 +7,5 @@ module Servant.API.ReqBody where
67-- Example:
78--
89-- > -- POST /books
9−-- > type MyApi = "books" :> ReqBody Book :> Post Book
10−data ReqBody a
10+-- > type MyApi = "books" :> ReqBody '[JSON] Book :> Post Book
11+data ReqBody (contentTypes::[*]) a
src/Servant/QQ.hsdeleted−228
@@ -1,228 +0,0 @@
1−{-# LANGUAGE MultiParamTypeClasses #-}
2−{-# LANGUAGE FunctionalDependencies #-}
3−{-# LANGUAGE DataKinds #-}
4−{-# LANGUAGE FlexibleInstances #-}
5−{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE TemplateHaskell #-}
7−{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
8−-- | QuasiQuoting utilities for API types.
9−--
10−-- 'sitemap' allows you to write your type in a very natural way:
11−--
12−-- @
13−-- [sitemap|
14−-- PUT hello String -> ()
15−-- POST hello/p:Int String -> ()
16−-- GET hello/?name:String Int
17−-- |]
18−-- @
19−--
20−-- Will generate:
21−--
22−-- @
23−-- "hello" :> ReqBody String :> Put ()
24−-- :\<|> "hello" :> Capture "p" Int :> ReqBody String :> Post ()
25−-- :\<|> "hello" :> QueryParam "name" String :> Get Int
26−-- @
27−--
28−-- Note the @/@ before a @QueryParam@!
29−module Servant.QQ (sitemap) where
30−
31−import Control.Monad ( void )
32−import Language.Haskell.TH.Quote ( QuasiQuoter(..) )
33−import Language.Haskell.TH
34− ( mkName, Type(AppT, ConT, LitT), TyLit(StrTyLit) )
35−import Text.ParserCombinators.Parsec
36− ( try,
37− Parser,
38− manyTill,
39− endBy,
40− sepBy1,
41− optional,
42− optionMaybe,
43− string,
44− anyChar,
45− char,
46− spaces,
47− noneOf,
48− parse,
49− skipMany,
50− many,
51− lookAhead,
52− (<|>),
53− (<?>) )
54−import Servant.API.Capture ( Capture )
55−import Servant.API.Get ( Get )
56−import Servant.API.Post ( Post )
57−import Servant.API.Put ( Put )
58−import Servant.API.Delete ( Delete )
59−import Servant.API.QueryParam ( QueryParam )
60−import Servant.API.MatrixParam ( MatrixParam )
61−import Servant.API.ReqBody ( ReqBody )
62−import Servant.API.Sub ( (:>) )
63−import Servant.API.Alternative ( (:<|>) )
64−
65−-- | Finally-tagless encoding for our DSL.
66−-- Keeping 'repr'' and 'repr' distinct when writing functions with an
67−-- @ExpSYM@ context ensures certain invariants (for instance, that there is
68−-- only one of 'get', 'post', 'put', and 'delete' in a value), but
69−-- sometimes requires a little more work.
70−class ExpSYM repr' repr | repr -> repr', repr' -> repr where
71− lit :: String -> repr' -> repr
72− capture :: String -> String -> repr -> repr
73− reqBody :: String -> repr -> repr
74− queryParam :: String -> String -> repr -> repr
75− matrixParam :: String -> String -> repr -> repr
76− conj :: repr' -> repr -> repr
77− get :: String -> repr
78− post :: String -> repr
79− put :: String -> repr
80− delete :: String -> repr
81−
82−
83−infixr 6 >:
84−
85−(>:) :: Type -> Type -> Type
86−(>:) = conj
87−
88−
89−instance ExpSYM Type Type where
90− lit name r = LitT (StrTyLit name) >: r
91− capture name typ r = AppT (AppT (ConT ''Capture) (LitT (StrTyLit name)))
92− (ConT $ mkName typ) >: r
93− reqBody typ r = AppT (ConT ''ReqBody) (ConT $ mkName typ) >: r
94− queryParam name typ r = AppT (AppT (ConT ''QueryParam) (LitT (StrTyLit name)))
95− (ConT $ mkName typ) >: r
96− matrixParam name typ r = AppT (AppT (ConT ''MatrixParam) (LitT (StrTyLit name)))
97− (ConT $ mkName typ) >: r
98− conj x = AppT (AppT (ConT ''(:>)) x)
99− get typ = AppT (ConT ''Get) (ConT $ mkName typ)
100− post typ = AppT (ConT ''Post) (ConT $ mkName typ)
101− put typ = AppT (ConT ''Put) (ConT $ mkName typ)
102− delete "()" = ConT ''Delete
103− delete _ = error "Delete does not return a request body"
104−
105−parseMethod :: ExpSYM repr' repr => Parser (String -> repr)
106−parseMethod = try (string "GET" >> return get)
107− <|> try (string "POST" >> return post)
108− <|> try (string "PUT" >> return put)
109− <|> try (string "DELETE" >> return delete)
110−
111−parseUrlSegment :: ExpSYM repr repr => Parser (repr -> repr)
112−parseUrlSegment = try parseCapture
113− <|> try parseQueryParam
114− <|> try parseLit
115− where
116− parseCapture = do
117− cname <- many (noneOf " ?/:;")
118− char ':'
119− ctyp <- many (noneOf " ?/:;")
120− mx <- many parseMatrixParam
121− return $ capture cname ctyp . foldr (.) id mx
122− parseQueryParam = do
123− char '?'
124− cname <- many (noneOf " ?/:;")
125− char ':'
126− ctyp <- many (noneOf " ?/:;")
127− return $ queryParam cname ctyp
128− parseLit = do
129− lt <- many (noneOf " ?/:;")
130− mx <- many parseMatrixParam
131− return $ lit lt . foldr (.) id mx
132− parseMatrixParam = do
133− char ';'
134− cname <- many (noneOf " ?/:;")
135− char ':'
136− ctyp <- many (noneOf " ?/:;")
137− return $ matrixParam cname ctyp
138−
139−parseUrl :: ExpSYM repr repr => Parser (repr -> repr)
140−parseUrl = do
141− optional $ char '/'
142− url <- parseUrlSegment `sepBy1` char '/'
143− return $ foldr1 (.) url
144−
145−data Typ = Val String
146− | ReqArgVal String String
147−
148−parseTyp :: Parser Typ
149−parseTyp = do
150− f <- many (noneOf "-{\n\r")
151− spaces
152− s <- optionMaybe (try parseRet)
153− try $ optional inlineComment
154− try $ optional blockComment
155− case s of
156− Nothing -> return $ Val (stripTr f)
157− Just s' -> return $ ReqArgVal (stripTr f) (stripTr s')
158− where
159− parseRet :: Parser String
160− parseRet = do
161− string "->"
162− spaces
163− many (noneOf "-{\n\r")
164− stripTr = reverse . dropWhile (== ' ') . reverse
165−
166−
167−parseEntry :: ExpSYM repr repr => Parser repr
168−parseEntry = do
169− met <- parseMethod
170− spaces
171− url <- parseUrl
172− spaces
173− typ <- parseTyp
174− case typ of
175− Val s -> return $ url (met s)
176− ReqArgVal i o -> return $ url $ reqBody i (met o)
177−
178−blockComment :: Parser ()
179−blockComment = do
180− string "{-"
181− manyTill anyChar (try $ string "-}")
182− return ()
183−
184−inlineComment :: Parser ()
185−inlineComment = do
186− string "--"
187− manyTill anyChar (try $ lookAhead eol)
188− return ()
189−
190−eol :: Parser String
191−eol = try (string "\n\r")
192− <|> try (string "\r\n")
193− <|> string "\n"
194− <|> string "\r"
195− <?> "end of line"
196−
197−eols :: Parser ()
198−eols = skipMany $ void eol <|> blockComment <|> inlineComment
199−
200−parseAll :: Parser Type
201−parseAll = do
202− eols
203− entries <- parseEntry `endBy` eols
204− return $ foldr1 union entries
205− where union :: Type -> Type -> Type
206− union a = AppT (AppT (ConT ''(:<|>)) a)
207−
208−-- | The sitemap QuasiQuoter.
209−--
210−-- * @.../<var>:<type>/...@ becomes a capture
211−-- * @.../?<var>:<type>@ becomes a query parameter
212−-- * @<method> ... <typ>@ becomes a method returning @<typ>@
213−-- * @<method> ... <typ1> -> <typ2>@ becomes a method with request
214−-- body of @<typ1>@ and returning @<typ2>@
215−--
216−-- Comments are allowed, and have the standard Haskell format
217−--
218−-- * @--@ for inline
219−-- * @{- ... -}@ for block
220−--
221−sitemap :: QuasiQuoter
222−sitemap = QuasiQuoter { quoteExp = undefined
223− , quotePat = undefined
224− , quoteType = \x -> case parse parseAll "" x of
225− Left err -> error $ show err
226− Right st -> return st
227− , quoteDec = undefined
228− }
src/Servant/Utils/Links.hs+34−19
@@ -2,10 +2,11 @@
22{-# LANGUAGE ConstraintKinds #-}
33{-# LANGUAGE DataKinds #-}
44{-# LANGUAGE FlexibleInstances #-}
5−{-# LANGUAGE TypeFamilies #-}
6−{-# LANGUAGE TypeOperators #-}
75{-# LANGUAGE FunctionalDependencies #-}
6+{-# LANGUAGE PolyKinds #-}
87{-# LANGUAGE ScopedTypeVariables #-}
8+{-# LANGUAGE TypeFamilies #-}
9+{-# LANGUAGE TypeOperators #-}
910{-# LANGUAGE UndecidableInstances #-}
1011
1112-- | Type safe generation of internal links.
@@ -19,7 +20,7 @@
1920-- >>>
2021-- >>>
2122-- >>>
22−-- >>> type Hello = "hello" :> Get Int
23+-- >>> type Hello = "hello" :> Get '[JSON] Int
2324-- >>> type Bye = "bye" :> QueryParam "name" String :> Delete
2425-- >>> type API = Hello :<|> Bye
2526-- >>> let api = Proxy :: Proxy API
@@ -39,7 +40,7 @@
3940-- function that accepts that input and generates a link. This is best shown
4041-- with an example. Here, a link is generated with no parameters:
4142--
42−-- >>> let hello = Proxy :: Proxy ("hello" :> Get Int)
43+-- >>> let hello = Proxy :: Proxy ("hello" :> Get '[JSON] Int)
4344-- >>> print (safeLink api hello :: URI)
4445-- hello
4546--
@@ -73,7 +74,7 @@
7374-- <BLANKLINE>
7475-- <interactive>:64:1:
7576-- Could not deduce (Or
76−-- (IsElem' Delete (Get Int))
77+-- (IsElem' Delete (Get '[JSON] Int))
7778-- (IsElem'
7879-- ("hello" :> Delete)
7980-- ("bye" :> (QueryParam "name" String :> Delete))))
@@ -122,11 +123,23 @@ import Servant.API.Sub ( type (:>) )
122123import Servant.API.Raw ( Raw )
123124import Servant.API.Alternative ( type (:<|>) )
124125
126+-- | A safe link datatype.
127+-- The only way of constructing a 'Link' is using 'safeLink', which means any
128+-- 'Link' is guaranteed to be part of the mentioned API.
129+data Link = Link
130+ { _segments :: [String] -- ^ Segments of "foo/bar" would be ["foo", "bar"]
131+ , _queryParams :: [Param Query]
132+ } deriving Show
133+
125134-- | If either a or b produce an empty constraint, produce an empty constraint.
126135type family Or (a :: Constraint) (b :: Constraint) :: Constraint where
127136 Or () b = ()
128137 Or a () = ()
129138
139+-- | If both a or b produce an empty constraint, produce an empty constraint.
140+type family And (a :: Constraint) (b :: Constraint) :: Constraint where
141+ And () () = ()
142+
130143-- | You may use this type family to tell the type checker that your custom type
131144-- may be skipped as part of a link. This is useful for things like
132145-- 'QueryParam' that are optional in a URI and do not affect them if they are
@@ -147,24 +160,26 @@ type family IsElem endpoint api :: Constraint where
147160 IsElem e (sa :<|> sb) = Or (IsElem e sa) (IsElem e sb)
148161 IsElem (e :> sa) (e :> sb) = IsElem sa sb
149162 IsElem sa (Header x :> sb) = IsElem sa sb
150− IsElem sa (ReqBody x :> sb) = IsElem sa sb
163+ IsElem sa (ReqBody y x :> sb) = IsElem sa sb
164+ IsElem (e :> sa) (Capture x y :> sb) = IsElem sa sb
151165 IsElem sa (QueryParam x y :> sb) = IsElem sa sb
152166 IsElem sa (QueryParams x y :> sb) = IsElem sa sb
153167 IsElem sa (QueryFlag x :> sb) = IsElem sa sb
154168 IsElem sa (MatrixParam x y :> sb) = IsElem sa sb
155169 IsElem sa (MatrixParams x y :> sb) = IsElem sa sb
156170 IsElem sa (MatrixFlag x :> sb) = IsElem sa sb
171+ IsElem (Get ct typ) (Get ct' typ) = IsSubList ct ct'
172+ IsElem (Post ct typ) (Post ct' typ) = IsSubList ct ct'
173+ IsElem (Put ct typ) (Put ct' typ) = IsSubList ct ct'
157174 IsElem e e = ()
158175 IsElem e a = IsElem' e a
159176
160−-- | A safe link datatype.
161−-- The only way of constructing a 'Link' is using 'safeLink', which means any
162−-- 'Link' is guaranteed to be part of the mentioned API.
163−data Link = Link
164− { _segments :: [String] -- ^ Segments of "foo/bar" would be ["foo", "bar"]
165− , _queryParams :: [Param Query]
166− } deriving Show
167177
178+type family IsSubList a b :: Constraint where
179+ IsSubList '[] b = ()
180+ IsSubList '[x] (x ': xs) = ()
181+ IsSubList '[x] (y ': ys) = IsSubList '[x] ys
182+ IsSubList (x ': xs) y = IsSubList '[x] y `And` IsSubList xs y
168183
169184-- Phantom types for Param
170185data Matrix
@@ -317,16 +332,16 @@ instance (ToText v, HasLink sub)
317332 addSegment (escape . unpack $ toText v) l
318333
319334-- Verb (terminal) instances
320−instance HasLink (Get r) where
321− type MkLink (Get r) = URI
335+instance HasLink (Get y r) where
336+ type MkLink (Get y r) = URI
322337 toLink _ = linkURI
323338
324−instance HasLink (Post r) where
325− type MkLink (Post r) = URI
339+instance HasLink (Post y r) where
340+ type MkLink (Post y r) = URI
326341 toLink _ = linkURI
327342
328−instance HasLink (Put r) where
329− type MkLink (Put r) = URI
343+instance HasLink (Put y r) where
344+ type MkLink (Put y r) = URI
330345 toLink _ = linkURI
331346
332347instance HasLink Delete where
test/Servant/API/ContentTypesSpec.hsnew file+182
@@ -0,0 +1,182 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE MultiParamTypeClasses #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+{-# OPTIONS_GHC -fno-warn-orphans #-}
6+module Servant.API.ContentTypesSpec where
7+
8+import Control.Applicative
9+import Data.Aeson
10+import Data.Function (on)
11+import Data.Proxy
12+
13+import Data.ByteString.Char8
14+import qualified Data.ByteString.Lazy as BSL
15+import Data.List (maximumBy)
16+import Data.Maybe (fromJust, isJust, isNothing)
17+import Data.String (IsString (..))
18+import Data.String.Conversions (cs)
19+import qualified Data.Text as TextS
20+import qualified Data.Text.Lazy as TextL
21+import GHC.Generics
22+import Test.Hspec
23+import Test.QuickCheck
24+
25+import Servant.API.ContentTypes
26+
27+spec :: Spec
28+spec = describe "Servant.API.ContentTypes" $ do
29+
30+ describe "The JSON Content-Type type" $ do
31+
32+ it "has fromByteString reverse toByteString for valid top-level json ([Int]) " $ do
33+ let p = Proxy :: Proxy JSON
34+ property $ \x -> fromByteString p (toByteString p x) == Right (x::[Int])
35+
36+ it "has fromByteString reverse toByteString for valid top-level json " $ do
37+ let p = Proxy :: Proxy JSON
38+ property $ \x -> fromByteString p (toByteString p x) == Right (x::SomeData)
39+
40+ describe "The PlainText Content-Type type" $ do
41+
42+ it "has fromByteString reverse toByteString (lazy Text)" $ do
43+ let p = Proxy :: Proxy PlainText
44+ property $ \x -> fromByteString p (toByteString p x) == Right (x::TextL.Text)
45+
46+ it "has fromByteString reverse toByteString (strict Text)" $ do
47+ let p = Proxy :: Proxy PlainText
48+ property $ \x -> fromByteString p (toByteString p x) == Right (x::TextS.Text)
49+
50+ describe "The OctetStream Content-Type type" $ do
51+
52+ it "is id (Lazy ByteString)" $ do
53+ let p = Proxy :: Proxy OctetStream
54+ property $ \x -> toByteString p x == (x :: BSL.ByteString)
55+ && fromByteString p x == Right x
56+
57+ it "is fromStrict/toStrict (Strict ByteString)" $ do
58+ let p = Proxy :: Proxy OctetStream
59+ property $ \x -> toByteString p x == BSL.fromStrict (x :: ByteString)
60+ && fromByteString p (BSL.fromStrict x) == Right x
61+
62+ describe "handleAcceptH" $ do
63+
64+ it "returns Nothing if the 'Accept' header doesn't match" $ do
65+ handleAcceptH (Proxy :: Proxy '[JSON]) "text/plain" (3 :: Int)
66+ `shouldSatisfy` isNothing
67+
68+ it "returns Just if the 'Accept' header matches" $ do
69+ handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (3 :: Int)
70+ `shouldSatisfy` isJust
71+ handleAcceptH (Proxy :: Proxy '[PlainText, JSON]) "application/json" (3 :: Int)
72+ `shouldSatisfy` isJust
73+ handleAcceptH (Proxy :: Proxy '[PlainText, JSON, OctetStream])
74+ "application/octet-stream" ("content" :: ByteString)
75+ `shouldSatisfy` isJust
76+
77+ it "returns the Content-Type as the first element of the tuple" $ do
78+ handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (3 :: Int)
79+ `shouldSatisfy` ((== "application/json") . fst . fromJust)
80+ handleAcceptH (Proxy :: Proxy '[PlainText, JSON]) "application/json" (3 :: Int)
81+ `shouldSatisfy` ((== "application/json") . fst . fromJust)
82+ handleAcceptH (Proxy :: Proxy '[PlainText, JSON, OctetStream])
83+ "application/octet-stream" ("content" :: ByteString)
84+ `shouldSatisfy` ((== "application/octet-stream") . fst . fromJust)
85+
86+ it "returns the appropriately serialized representation" $ do
87+ property $ \x -> handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (x :: SomeData)
88+ == Just ("application/json", encode x)
89+
90+ it "respects the Accept spec ordering" $ do
91+ let highest a b c = maximumBy (compare `on` snd)
92+ [ ("application/octet-stream", a)
93+ , ("application/json", b)
94+ , ("text/plain;charset=utf-8", c)
95+ ]
96+ let acceptH a b c = addToAccept (Proxy :: Proxy OctetStream) a $
97+ addToAccept (Proxy :: Proxy JSON) b $
98+ addToAccept (Proxy :: Proxy PlainText ) c ""
99+ let val a b c i = handleAcceptH (Proxy :: Proxy '[OctetStream, JSON, PlainText])
100+ (acceptH a b c) (i :: Int)
101+ property $ \a b c i -> fst (fromJust $ val a b c i) == fst (highest a b c)
102+
103+ describe "handleCTypeH" $ do
104+
105+ it "returns Nothing if the 'Content-Type' header doesn't match" $ do
106+ handleCTypeH (Proxy :: Proxy '[JSON]) "text/plain" "𝓽𝓱𝓮 𝓽𝓲𝓶𝓮 𝓱𝓪𝓼 𝓬𝓸𝓶𝓮, 𝓽𝓱𝓮 𝔀𝓪𝓵𝓻𝓾𝓼 𝓼𝓪𝓲𝓭 "
107+ `shouldBe` (Nothing :: Maybe (Either String Value))
108+
109+ context "the 'Content-Type' header matches" $ do
110+ it "returns Just if the parameter matches" $ do
111+ handleCTypeH (Proxy :: Proxy '[JSON]) "application/json"
112+ "𝕥𝕠 𝕥𝕒𝕝𝕜 𝕠𝕗 𝕞𝕒𝕟𝕪 𝕥𝕙𝕚𝕟𝕘𝕤 "
113+ `shouldSatisfy` (isJust :: Maybe (Either String Value) -> Bool)
114+
115+ it "returns Just if there is no parameter" $ do
116+ handleCTypeH (Proxy :: Proxy '[JSON]) "application/json"
117+ "𝕥𝕠 𝕥𝕒𝕝𝕜 𝕠𝕗 𝕞𝕒𝕟𝕪 𝕥𝕙𝕚𝕟𝕘𝕤 "
118+ `shouldSatisfy` (isJust :: Maybe (Either String Value) -> Bool)
119+
120+ it "returns Just Left if the decoding fails" $ do
121+ let isJustLeft :: Maybe (Either String Value) -> Bool
122+ isJustLeft (Just (Left _)) = True
123+ isJustLeft _ = False
124+ handleCTypeH (Proxy :: Proxy '[JSON]) "application/json"
125+ "𝕺𝖋 𝖘𝖍𝖔𝖊𝖘--𝖆𝖓𝖉 𝖘𝖍𝖎𝖕𝖘--𝖆𝖓𝖉 𝖘𝖊𝖆𝖑𝖎𝖓𝖌-𝖜𝖆𝖝-- "
126+ `shouldSatisfy` isJustLeft
127+
128+ it "returns Just (Right val) if the decoding succeeds" $ do
129+ let val = SomeData "Of cabbages--and kings" 12
130+ handleCTypeH (Proxy :: Proxy '[JSON]) "application/json"
131+ (encode val)
132+ `shouldBe` Just (Right val)
133+
134+
135+data SomeData = SomeData { record1 :: String, record2 :: Int }
136+ deriving (Generic, Eq, Show)
137+
138+newtype ZeroToOne = ZeroToOne Float
139+ deriving (Eq, Show, Ord)
140+
141+instance FromJSON SomeData
142+
143+instance ToJSON SomeData
144+
145+instance Arbitrary SomeData where
146+ arbitrary = SomeData <$> arbitrary <*> arbitrary
147+
148+instance Arbitrary TextL.Text where
149+ arbitrary = TextL.pack <$> arbitrary
150+
151+instance Arbitrary TextS.Text where
152+ arbitrary = TextS.pack <$> arbitrary
153+
154+instance Arbitrary ZeroToOne where
155+ arbitrary = ZeroToOne <$> elements [ x / 10 | x <- [1..10]]
156+
157+instance MimeRender OctetStream Int where
158+ toByteString _ = cs . show
159+
160+instance MimeRender PlainText Int where
161+ toByteString _ = cs . show
162+
163+instance MimeRender PlainText ByteString where
164+ toByteString _ = cs
165+
166+instance ToJSON ByteString where
167+ toJSON x = object [ "val" .= x ]
168+
169+instance IsString AcceptHeader where
170+ fromString = AcceptHeader . fromString
171+
172+instance Arbitrary BSL.ByteString where
173+ arbitrary = cs <$> (arbitrary :: Gen String)
174+
175+instance Arbitrary ByteString where
176+ arbitrary = cs <$> (arbitrary :: Gen String)
177+
178+addToAccept :: Accept a => Proxy a -> ZeroToOne -> AcceptHeader -> AcceptHeader
179+addToAccept p (ZeroToOne f) (AcceptHeader h) = AcceptHeader (cont h)
180+ where new = cs (show $ contentType p) `append` "; q=" `append` pack (show f)
181+ cont "" = new
182+ cont old = old `append` ", " `append` new
test/Servant/QQSpec.hsdeleted−209
@@ -1,209 +0,0 @@
1−{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE FlexibleInstances #-}
3−{-# LANGUAGE FunctionalDependencies #-}
4−{-# LANGUAGE MultiParamTypeClasses #-}
5−{-# LANGUAGE OverlappingInstances #-}
6−{-# LANGUAGE QuasiQuotes #-}
7−{-# LANGUAGE TypeFamilies #-}
8−{-# LANGUAGE TypeOperators #-}
9−{-# LANGUAGE UndecidableInstances #-}
10−module Servant.QQSpec where
11−
12−import Test.Hspec ( Expectation, Spec, shouldBe, it, describe )
13−
14−import Servant.API
15− ( (:<|>),
16− ReqBody,
17− QueryParam,
18− MatrixParam,
19− Put,
20− Get,
21− Post,
22− Capture,
23− (:>),
24− sitemap )
25−
26−--------------------------------------------------------------------------
27−-- Types for testing
28−--------------------------------------------------------------------------
29−
30−-- Methods ---------------------------------------------------------------
31−type SimpleGet = [sitemap|
32−GET hello ()
33−|]
34−type SimpleGet' = "hello" :> Get ()
35−type SimpleGet'' = "hello" :> Get Bool
36−
37−type SimpleGet2 = [sitemap|
38−GET hello Bool
39−|]
40−type SimpleGet2' = "hello" :> Get Bool
41−type SimpleGet2'' = "hello" :> Get Int
42−
43−type SimplePost = [sitemap|
44−POST hello ()
45−|]
46−type SimplePost' = "hello" :> Post ()
47−type SimplePost'' = "hello" :> Post Bool
48−
49−type SimplePost2 = [sitemap|
50−POST hello Bool
51−|]
52−type SimplePost2' = "hello" :> Post Bool
53−type SimplePost2'' = "hello" :> Post ()
54−
55−type SimplePut = [sitemap|
56−PUT hello ()
57−|]
58−type SimplePut' = "hello" :> Put ()
59−type SimplePut'' = "hello" :> Put Bool
60−
61−type SimplePut2 = [sitemap|
62−PUT hello Bool
63−|]
64−type SimplePut2' = "hello" :> Put Bool
65−type SimplePut2'' = "hello" :> Put ()
66−
67−-- Parameters ------------------------------------------------------------
68−
69−type SimpleReqBody = [sitemap|
70−POST hello () -> Bool
71−|]
72−type SimpleReqBody' = "hello" :> ReqBody () :> Post Bool
73−type SimpleReqBody'' = "hello" :> ReqBody Bool :> Post ()
74−
75−type SimpleCapture = [sitemap|
76−POST hello/p:Int Bool
77−|]
78−type SimpleCapture' = "hello" :> Capture "p" Int :> Post Bool
79−type SimpleCapture'' = "hello" :> Capture "r" Int :> Post Bool
80−type SimpleCapture''' = "hello" :> Capture "p" Bool :> Post Bool
81−
82−type SimpleQueryParam = [sitemap|
83−POST hello/?p:Int Bool
84−|]
85−type SimpleQueryParam' = "hello" :> QueryParam "p" Int :> Post Bool
86−type SimpleQueryParam'' = "hello" :> QueryParam "r" Int :> Post Bool
87−type SimpleQueryParam''' = "hello" :> QueryParam "p" Bool :> Post Bool
88−
89−type SimpleMatrixParam = [sitemap|
90−POST hello;p:Int Bool
91−|]
92−type SimpleMatrixParam' = "hello" :> MatrixParam "p" Int :> Post Bool
93−type SimpleMatrixParam'' = "hello" :> MatrixParam "r" Int :> Post Bool
94−type SimpleMatrixParam''' = "hello" :> MatrixParam "p" Bool :> Post Bool
95−
96−type ComplexMatrixParam = [sitemap|
97−POST hello;p:Int;q:String/world;r:Int Bool
98−|]
99−type ComplexMatrixParam' = "hello" :> MatrixParam "p" Int :> MatrixParam "q" String :> "world" :> MatrixParam "r" Int :> Post Bool
100−type ComplexMatrixParam'' = "hello" :> MatrixParam "p" Int :> MatrixParam "q" String :> "world" :> MatrixParam "s" Int :> Post Bool
101−type ComplexMatrixParam''' = "hello" :> MatrixParam "p" Int :> MatrixParam "q" String :> "world" :> MatrixParam "r" Bool :> Post Bool
102−
103−-- Combinations ----------------------------------------------------------
104−
105−type TwoPaths = [sitemap|
106−POST hello Bool
107−GET hello Bool
108−|]
109−type TwoPaths' = ("hello" :> Post Bool) :<|> ("hello" :> Get Bool)
110−
111−type WithInlineComments = [sitemap|
112−GET hello Bool -- This is a comment
113−|]
114−type WithInlineComments' = "hello" :> Get Bool
115−
116−type WithInlineComments2 = [sitemap|
117−GET hello Bool
118−-- This is a comment
119−|]
120−type WithInlineComments2' = "hello" :> Get Bool
121−
122−
123−type WithBlockComments = [sitemap|
124−GET hello Bool {-
125−POST hello Bool
126−-}
127−|]
128−type WithBlockComments' = "hello" :> Get Bool
129−
130−type WithBlockComments2 = [sitemap|
131−GET hello Bool {-
132−POST hello Bool
133−-}
134−POST hello Bool
135−|]
136−type WithBlockComments2' = ("hello" :> Get Bool) :<|> ("hello" :> Post Bool)
137−
138−--------------------------------------------------------------------------
139−-- Spec
140−--------------------------------------------------------------------------
141−
142−spec :: Spec
143−spec = do
144− describe "'sitemap' QuasiQuoter" $ do
145− it "Handles simple GET types" $ do
146− (u::SimpleGet) ~= (u::SimpleGet' ) ~> True
147− (u::SimpleGet) ~= (u::SimpleGet'' ) ~> False
148− (u::SimpleGet2) ~= (u::SimpleGet2' ) ~> True
149− (u::SimpleGet2) ~= (u::SimpleGet2'') ~> False
150− it "Handles simple POST types" $ do
151− (u::SimplePost) ~= (u::SimplePost' ) ~> True
152− (u::SimplePost) ~= (u::SimplePost'' ) ~> False
153− (u::SimplePost2) ~= (u::SimplePost2' ) ~> True
154− (u::SimplePost2) ~= (u::SimplePost2'') ~> False
155− it "Handles simple PUT types" $ do
156− (u::SimplePut) ~= (u::SimplePut' ) ~> True
157− (u::SimplePut) ~= (u::SimplePut'' ) ~> False
158− (u::SimplePut2) ~= (u::SimplePut2' ) ~> True
159− (u::SimplePut2) ~= (u::SimplePut2'') ~> False
160− it "Handles simple request body types" $ do
161− (u::SimpleReqBody) ~= (u::SimpleReqBody' ) ~> True
162− (u::SimpleReqBody) ~= (u::SimpleReqBody'') ~> False
163− it "Handles simple captures" $ do
164− (u::SimpleCapture) ~= (u::SimpleCapture' ) ~> True
165− (u::SimpleCapture) ~= (u::SimpleCapture'') ~> False
166− (u::SimpleCapture) ~= (u::SimpleCapture''') ~> False
167− it "Handles simple querystring parameters" $ do
168− (u::SimpleQueryParam) ~= (u::SimpleQueryParam' ) ~> True
169− (u::SimpleQueryParam) ~= (u::SimpleQueryParam'') ~> False
170− (u::SimpleQueryParam) ~= (u::SimpleQueryParam''') ~> False
171− it "Handles simple matrix parameters" $ do
172− (u::SimpleMatrixParam) ~= (u::SimpleMatrixParam' ) ~> True
173− (u::SimpleMatrixParam) ~= (u::SimpleMatrixParam'') ~> False
174− (u::SimpleMatrixParam) ~= (u::SimpleMatrixParam''') ~> False
175− it "Handles more complex matrix parameters" $ do
176− (u::ComplexMatrixParam) ~= (u::ComplexMatrixParam' ) ~> True
177− (u::ComplexMatrixParam) ~= (u::ComplexMatrixParam'') ~> False
178− (u::ComplexMatrixParam) ~= (u::ComplexMatrixParam''') ~> False
179− it "Handles multiples paths" $ do
180− (u::TwoPaths) ~= (u::TwoPaths') ~> True
181− it "Ignores inline comments" $ do
182− (u::WithInlineComments) ~= (u::WithInlineComments') ~> True
183− (u::WithInlineComments2) ~= (u::WithInlineComments2') ~> True
184− it "Ignores inline comments" $ do
185− (u::WithBlockComments) ~= (u::WithBlockComments') ~> True
186− (u::WithBlockComments2) ~= (u::WithBlockComments2') ~> True
187−
188−
189−--------------------------------------------------------------------------
190−-- Utilities
191−--------------------------------------------------------------------------
192−data HTrue
193−data HFalse
194−
195−-- Kiselyov's Type Equality predicate
196−class TypeEq x y b | x y -> b where { areEq :: x -> y -> Bool }
197−instance TypeEq x x HTrue where { areEq _ _ = True }
198−instance b ~ HFalse => TypeEq x y b where { areEq _ _ = False}
199−
200−infix 4 ~=
201−(~=) :: TypeEq x y b => x -> y -> Bool
202−(~=) = areEq
203−
204−u :: a
205−u = undefined
206−
207−infix 3 ~>
208−(~>) :: (Show a, Eq a) => a -> a -> Expectation
209−(~>) = shouldBe
test/Servant/Utils/LinksSpec.hs+21−11
@@ -15,22 +15,32 @@ type TestApi =
1515 "hello" :> Capture "name" String :> QueryParam "capital" Bool :> Delete
1616
1717 :<|> "parent" :> MatrixParams "name" String :> "child"
18− :> MatrixParam "gender" String :> Get String
18+ :> MatrixParam "gender" String :> Get '[JSON] String
1919
2020 -- Flags
2121 :<|> "ducks" :> MatrixFlag "yellow" :> MatrixFlag "loud" :> Delete
2222 :<|> "balls" :> QueryFlag "bouncy" :> QueryFlag "fast" :> Delete
2323
2424 -- All of the verbs
25− :<|> "get" :> Get ()
26− :<|> "put" :> Put ()
27− :<|> "post" :> ReqBody 'True :> Post ()
25+ :<|> "get" :> Get '[JSON] ()
26+ :<|> "put" :> Put '[JSON] ()
27+ :<|> "post" :> ReqBody '[JSON] 'True :> Post '[JSON] ()
2828 :<|> "delete" :> Header "ponies" :> Delete
2929 :<|> "raw" :> Raw
3030
31−type TestLink = "hello" :> "hi" :> Get Bool
32−type TestLink2 = "greet" :> Post Bool
33−type TestLink3 = "parent" :> "child" :> Get String
31+type TestLink = "hello" :> "hi" :> Get '[JSON] Bool
32+type TestLink2 = "greet" :> Post '[PlainText] Bool
33+type TestLink3 = "parent" :> "child" :> Get '[JSON] String
34+
35+type BadTestLink = "hallo" :> "hi" :> Get '[JSON] Bool
36+type BadTestLink2 = "greet" :> Get '[PlainText] Bool
37+type BadTestLink3 = "parent" :> "child" :> MatrixFlag "male" :> Get '[JSON] String
38+
39+type BadTestLink' = "hello" :> "hi" :> Get '[OctetStream] Bool
40+type BadTestLink'2 = "greet" :> Get '[OctetStream] Bool
41+
42+type NotALink = "hello" :> Capture "x" Bool :> Get '[JSON] Bool
43+type NotALink2 = "hello" :> ReqBody '[JSON] 'True :> Get '[JSON] Bool
3444
3545apiLink :: (IsElem endpoint TestApi, HasLink endpoint)
3646 => Proxy endpoint -> MkLink endpoint
@@ -56,7 +66,7 @@ spec = describe "Servant.Utils.Links" $ do
5666 let l3 = Proxy :: Proxy ("parent" :> MatrixParams "name" String
5767 :> "child"
5868 :> MatrixParam "gender" String
59− :> Get String)
69+ :> Get '[JSON] String)
6070 apiLink l3 ["Hubert?x=;&", "Cumberdale"] "Edward?"
6171 `shouldBeURI` "parent;name[]=Hubert%3Fx%3D%3B%26;\
6272 \name[]=Cumberdale/child;gender=Edward%3F"
@@ -73,8 +83,8 @@ spec = describe "Servant.Utils.Links" $ do
7383 apiLink l2 False True `shouldBeURI` "ducks;loud"
7484
7585 it "Generates correct links for all of the verbs" $ do
76− apiLink (Proxy :: Proxy ("get" :> Get ())) `shouldBeURI` "get"
77− apiLink (Proxy :: Proxy ("put" :> Put ())) `shouldBeURI` "put"
78− apiLink (Proxy :: Proxy ("post" :> Post ())) `shouldBeURI` "post"
86+ apiLink (Proxy :: Proxy ("get" :> Get '[JSON] ())) `shouldBeURI` "get"
87+ apiLink (Proxy :: Proxy ("put" :> Put '[JSON] ())) `shouldBeURI` "put"
88+ apiLink (Proxy :: Proxy ("post" :> Post '[JSON] ())) `shouldBeURI` "post"
7989 apiLink (Proxy :: Proxy ("delete" :> Delete)) `shouldBeURI` "delete"
8090 apiLink (Proxy :: Proxy ("raw" :> Raw)) `shouldBeURI` "raw"