forked from derekelkins/servant-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
308 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ pkgs ? import <nixpkgs> { config.allowUnfree = true; } | ||
, src ? builtins.filterSource (path: type: | ||
type != "unknown" && | ||
baseNameOf path != ".git" && | ||
baseNameOf path != "result" && | ||
baseNameOf path != "dist") ./. | ||
, servant ? import ../servant {} | ||
}: | ||
pkgs.haskellPackages.buildLocalCabalWithArgs { | ||
name = "servant-server"; | ||
inherit src; | ||
args = { | ||
inherit servant; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
{-# LANGUAGE DataKinds #-} | ||
{-# LANGUAGE FlexibleInstances #-} | ||
{-# LANGUAGE MultiParamTypeClasses #-} | ||
{-# LANGUAGE OverloadedStrings #-} | ||
{-# LANGUAGE PolyKinds #-} | ||
{-# LANGUAGE ScopedTypeVariables #-} | ||
{-# LANGUAGE TypeFamilies #-} | ||
{-# LANGUAGE TypeOperators #-} | ||
{-# LANGUAGE UndecidableInstances #-} | ||
module Servant.Server.ContentTypes where | ||
|
||
import Data.Aeson (ToJSON(..), encode) | ||
import Data.ByteString.Lazy (ByteString) | ||
import qualified Data.ByteString as BS | ||
import Data.Proxy (Proxy(..)) | ||
import Data.String.Conversions (cs) | ||
import qualified Network.HTTP.Media as M | ||
|
||
|
||
import Servant.API (XML, HTML, JSON, JavaScript, CSS, PlainText) | ||
|
||
-- | Instances of 'Accept' represent mimetypes. They are used for matching | ||
-- against the @Accept@ HTTP header of the request, and for setting the | ||
-- @Content-Type@ header of the response | ||
-- | ||
-- Example: | ||
-- | ||
-- instance Accept HTML where | ||
-- contentType _ = "text" // "html" | ||
-- | ||
class Accept ctype where | ||
contentType :: Proxy ctype -> M.MediaType | ||
|
||
instance Accept HTML where | ||
contentType _ = "text" M.// "html" | ||
|
||
instance Accept JSON where | ||
contentType _ = "application" M.// "json" | ||
|
||
instance Accept XML where | ||
contentType _ = "application" M.// "xml" | ||
|
||
instance Accept JavaScript where | ||
contentType _ = "application" M.// "javascript" | ||
|
||
instance Accept CSS where | ||
contentType _ = "text" M.// "css" | ||
|
||
instance Accept PlainText where | ||
contentType _ = "text" M.// "plain" | ||
|
||
newtype AcceptHeader = AcceptHeader BS.ByteString | ||
deriving (Eq, Show) | ||
|
||
-- | Instantiate this class to register a way of serializing a type based | ||
-- on the @Accept@ header. | ||
class Accept ctype => MimeRender ctype a where | ||
toByteString :: Proxy ctype -> a -> ByteString | ||
|
||
class AllCTRender list a where | ||
-- If the Accept header can be matched, returns (Just) a tuple of the | ||
-- Content-Type and response (serialization of @a@ into the appropriate | ||
-- mimetype). | ||
handleAcceptH :: Proxy list -> AcceptHeader -> a -> Maybe (ByteString, ByteString) | ||
|
||
instance ( AllMimeRender ctyps a, IsEmpty ctyps ~ 'False | ||
) => AllCTRender ctyps a where | ||
handleAcceptH _ (AcceptHeader accept) val = M.mapAcceptMedia lkup accept | ||
where pctyps = Proxy :: Proxy ctyps | ||
amrs = amr pctyps val | ||
lkup = zip (map fst amrs) $ map (\(a,b) -> (cs $ show a, b)) amrs | ||
|
||
|
||
-------------------------------------------------------------------------- | ||
-- Check that all elements of list are instances of MimeRender | ||
-------------------------------------------------------------------------- | ||
class AllMimeRender ls a where | ||
amr :: Proxy ls -> a -> [(M.MediaType, ByteString)] -- list of content-types/response pairs | ||
|
||
instance ( MimeRender ctyp a ) => AllMimeRender '[ctyp] a where | ||
amr _ a = [(contentType pctyp, toByteString pctyp a)] | ||
where pctyp = Proxy :: Proxy ctyp | ||
|
||
instance ( MimeRender ctyp a | ||
, MimeRender ctyp' a | ||
, AllMimeRender ctyps a | ||
) => AllMimeRender (ctyp ': ctyp' ': ctyps) a where | ||
amr _ a = (contentType pctyp, toByteString pctyp a) | ||
:(contentType pctyp', toByteString pctyp' a) | ||
:(amr pctyps a) | ||
where pctyp = Proxy :: Proxy ctyp | ||
pctyps = Proxy :: Proxy ctyps | ||
pctyp' = Proxy :: Proxy ctyp' | ||
|
||
|
||
instance AllMimeRender '[] a where | ||
amr _ _ = [] | ||
|
||
type family IsEmpty (ls::[*]) where | ||
IsEmpty '[] = 'True | ||
IsEmpty x = 'False | ||
|
||
-------------------------------------------------------------------------- | ||
-- MimeRender Instances | ||
-------------------------------------------------------------------------- | ||
|
||
instance ToJSON a => MimeRender JSON a where | ||
toByteString _ = encode | ||
|
||
instance Show a => MimeRender PlainText a where | ||
toByteString _ = encode . show | ||
|
||
instance MimeRender PlainText String where | ||
toByteString _ = encode |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
{-# LANGUAGE DataKinds #-} | ||
{-# LANGUAGE FlexibleInstances #-} | ||
{-# LANGUAGE MultiParamTypeClasses #-} | ||
{-# LANGUAGE OverloadedStrings #-} | ||
{-# LANGUAGE TypeOperators #-} | ||
{-# OPTIONS_GHC -fno-warn-orphans #-} | ||
module Servant.Server.ContentTypesSpec where | ||
|
||
import Control.Applicative | ||
import Data.Aeson (encode) | ||
import Data.ByteString.Char8 | ||
import Data.Function (on) | ||
import Data.Maybe (isJust, fromJust) | ||
import Data.List (maximumBy) | ||
import Data.Proxy (Proxy(..)) | ||
import Data.String (IsString(..)) | ||
import Data.String.Conversions (cs) | ||
import Network.HTTP.Types (hAccept) | ||
import Network.Wai (pathInfo, requestHeaders) | ||
import Network.Wai.Test ( runSession, request, defaultRequest | ||
, assertContentType, assertStatus ) | ||
import Test.Hspec | ||
import Test.QuickCheck | ||
|
||
import Servant.API | ||
import Servant.Server | ||
import Servant.Server.ContentTypes | ||
|
||
|
||
spec :: Spec | ||
spec = describe "Servant.Server.ContentTypes" $ do | ||
handleAcceptHSpec | ||
contentTypeSpec | ||
|
||
handleAcceptHSpec :: Spec | ||
handleAcceptHSpec = describe "handleAcceptH" $ do | ||
|
||
it "should return Just if the 'Accept' header matches" $ do | ||
handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (3 :: Int) | ||
`shouldSatisfy` isJust | ||
handleAcceptH (Proxy :: Proxy '[XML, JSON]) "application/json" (3 :: Int) | ||
`shouldSatisfy` isJust | ||
handleAcceptH (Proxy :: Proxy '[XML, JSON, HTML]) "text/html" (3 :: Int) | ||
`shouldSatisfy` isJust | ||
|
||
it "should return the Content-Type as the first element of the tuple" $ do | ||
handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (3 :: Int) | ||
`shouldSatisfy` ((== "application/json") . fst . fromJust) | ||
handleAcceptH (Proxy :: Proxy '[XML, JSON]) "application/json" (3 :: Int) | ||
`shouldSatisfy` ((== "application/json") . fst . fromJust) | ||
handleAcceptH (Proxy :: Proxy '[XML, JSON, HTML]) "text/html" (3 :: Int) | ||
`shouldSatisfy` ((== "text/html") . fst . fromJust) | ||
|
||
it "should return the appropriately serialized representation" $ do | ||
property $ \x -> handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (x :: Int) | ||
== Just ("application/json", encode x) | ||
|
||
it "respects the Accept spec ordering" $ | ||
property $ \a b c i -> fst (fromJust $ val a b c i) == (fst $ highest a b c) | ||
where | ||
highest a b c = maximumBy (compare `on` snd) [ ("text/html", a) | ||
, ("application/json", b) | ||
, ("application/xml", c) | ||
] | ||
acceptH a b c = addToAccept (Proxy :: Proxy HTML) a $ | ||
addToAccept (Proxy :: Proxy JSON) b $ | ||
addToAccept (Proxy :: Proxy XML ) c "" | ||
val a b c i = handleAcceptH (Proxy :: Proxy '[HTML, JSON, XML]) | ||
(acceptH a b c) (i :: Int) | ||
|
||
type ContentTypeApi = "foo" :> Get '[JSON] Int | ||
:<|> "bar" :> Get '[JSON, PlainText] Int | ||
|
||
contentTypeApi :: Proxy ContentTypeApi | ||
contentTypeApi = Proxy | ||
|
||
contentTypeServer :: Server ContentTypeApi | ||
contentTypeServer = return 5 :<|> return 3 | ||
|
||
contentTypeSpec :: Spec | ||
contentTypeSpec = do | ||
describe "Accept Headers" $ do | ||
|
||
it "uses the highest quality possible in the header" $ | ||
flip runSession (serve contentTypeApi contentTypeServer) $ do | ||
let acceptH = "text/plain; q=0.9, application/json; q=0.8" | ||
response <- Network.Wai.Test.request defaultRequest{ | ||
requestHeaders = [(hAccept, acceptH)] , | ||
pathInfo = ["bar"] | ||
} | ||
assertContentType "text/plain" response | ||
|
||
it "returns the first content-type if the Accept header is missing" $ | ||
flip runSession (serve contentTypeApi contentTypeServer) $ do | ||
response <- Network.Wai.Test.request defaultRequest{ | ||
pathInfo = ["bar"] | ||
} | ||
assertContentType "application/json" response | ||
|
||
it "returns 406 if it can't serve the requested content-type" $ | ||
flip runSession (serve contentTypeApi contentTypeServer) $ do | ||
let acceptH = "text/css" | ||
response <- Network.Wai.Test.request defaultRequest{ | ||
requestHeaders = [(hAccept, acceptH)] , | ||
pathInfo = ["bar"] | ||
} | ||
assertStatus 406 response | ||
|
||
|
||
instance Show a => MimeRender HTML a where | ||
toByteString _ = cs . show | ||
|
||
instance Show a => MimeRender XML a where | ||
toByteString _ = cs . show | ||
|
||
instance IsString AcceptHeader where | ||
fromString = AcceptHeader . fromString | ||
|
||
addToAccept :: Accept a => Proxy a -> ZeroToOne -> AcceptHeader -> AcceptHeader | ||
addToAccept p (ZeroToOne f) (AcceptHeader h) = AcceptHeader (cont h) | ||
where new = cs (show $ contentType p) `append` "; q=" `append` pack (show f) | ||
cont "" = new | ||
cont old = old `append` ", " `append` new | ||
|
||
newtype ZeroToOne = ZeroToOne Float | ||
deriving (Eq, Show, Ord) | ||
|
||
instance Arbitrary ZeroToOne where | ||
arbitrary = ZeroToOne <$> elements [ x / 10 | x <- [1..10]] |
Oops, something went wrong.