git.iko.soy/typeable/servant
on master help sign in
Merge branch 'master' into existential-error
commitad16c4f7685bce1e1f710e40b7f1013507ddaad3 authorChristian Marie <christian@ponies.io> dateFri 12 Jun 2015, 19:56 (11 years ago) parented7d235ab19ea8
35 files changed+757−516
.gitignore+5−5 scripts/bump-versions.sh+61 scripts/lib/common.sh+31 scripts/start-sandbox.sh+1−5 scripts/test-all.sh+2−6 scripts/update-defaults-nix.sh+1−5 scripts/upload.sh+52 servant-blaze/servant-blaze.cabal+1−1 servant-blaze/src/Servant/HTML/Blaze.hs+1−1 servant-client/CHANGELOG.md+4 servant-client/servant-client.cabal+4−1 servant-client/src/Servant/Client.hs+10−7 servant-client/src/Servant/Common/BaseUrl.hs+23−9 servant-client/src/Servant/Common/Req.hs+5−1 servant-client/test/Servant/ClientSpec.hs+17−9 servant-docs/servant-docs.cabal+1−1 servant-examples/auth-combinator/auth-combinator.hs+9−8 servant-examples/servant-examples.cabal+12−12 servant-jquery/servant-jquery.cabal+1−1 servant-lucid/servant-lucid.cabal+1−1 servant-lucid/src/Servant/HTML/Lucid.hs+1−1 servant-server/CHANGELOG.md+6 servant-server/example/greet.hs+1−1 servant-server/servant-server.cabal+10−6 servant-server/src/Servant/Server.hs+1−2 servant-server/src/Servant/Server/Internal.hs+169−426 servant-server/src/Servant/Server/Internal/PathInfo.hs+38 servant-server/src/Servant/Server/Internal/Router.hs+72 servant-server/src/Servant/Server/Internal/RoutingApplication.hs+144 servant-server/test/Servant/ServerSpec.hs+51−1 servant/CHANGELOG.md+9 servant/servant.cabal+2−2 servant/src/Servant/API/Capture.hs+1 servant/src/Servant/API/ContentTypes.hs+1−1 servant/src/Servant/Utils/Links.hs+9−3
.gitignore+5−5
@@ -1,8 +1,8 @@
1−dist
2−bin
3−lib
4−share
5−packages
1+/dist
2+/bin
3+/lib
4+/share
5+/packages
66*-packages.conf.d
77cabal-dev
88add-source-timestamps
scripts/bump-versions.shnew file+61
@@ -0,0 +1,61 @@
1+#!/bin/bash -
2+#===============================================================================
3+#
4+# FILE: bump-versions.sh
5+#
6+# USAGE: ./bump-versions.sh
7+#
8+# DESCRIPTION: Bump the versions for all servant packages
9+#
10+# OPTIONS: See usage
11+# REQUIREMENTS: bumper, bash >= 4
12+# CREATED: 11.05.2015 21:36
13+# REVISION: ---
14+#===============================================================================
15+
16+set -o nounset
17+set -o errexit
18+
19+. lib/common.sh
20+
21+usage () {
22+ echo " bump-versions.sh <POSITION> [-d|--dry-run]"
23+ echo " | [-h|--help]"
24+ echo " Bumps the specified positional version of all servant packages."
25+ echo " POSITION is a number between 0 and 3, inclusive."
26+ exit 0
27+}
28+
29+if [ $# -eq 0 ] ; then
30+ echo "expecting one or more arguments. Got 0"
31+ usage
32+elif [ $# -gt 2 ] ; then
33+ echo "expecting one or more arguments"
34+ usage
35+fi
36+
37+versions_equal
38+
39+while [ "${1:-unset}" != "unset" ] ; do
40+ case "$1" in
41+ -h | --help) usage ;;
42+ -d | --dry-run) DRY_RUN=true
43+ shift ;;
44+ 0) if POSITION="none" ; then POSITION=0 ; else usage ; fi
45+ shift ;;
46+ 1) if POSITION="none" ; then POSITION=1 ; else usage ; fi
47+ shift ;;
48+ 2) if POSITION="none" ; then POSITION=2 ; else usage ; fi
49+ shift ;;
50+ 3) if POSITION="none" ; then POSITION=3 ; else usage ; fi
51+ shift ;;
52+ *) usage ;;
53+ esac
54+done
55+
56+if $DRY_RUN ; then
57+ bumper --dry-run -"$POSITION" $(join , "${SOURCES[@]}")
58+else
59+ bumper -"$POSITION" $(join , "${SOURCES[@]}")
60+fi
61+
scripts/lib/common.shnew file+31
@@ -0,0 +1,31 @@
1+#!/bin/bash -
2+#===============================================================================
3+#
4+# FILE: lib/common.sh
5+#
6+# DESCRIPTION: Common functions for servant's shell scripts
7+# Meant to be sourced rather than run.
8+#
9+# REQUIREMENTS: bash >= 4
10+#===============================================================================
11+
12+
13+DIR=$( dirname $( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ))
14+DRY_RUN=false
15+POSITION="none"
16+SOURCES_TXT="$( dirname $DIR)/sources.txt"
17+CABAL=${CABAL:-cabal}
18+
19+declare -a SOURCES
20+readarray -t SOURCES < "$SOURCES_TXT"
21+
22+join () { local IFS="$1"; shift; echo "$*"; }
23+
24+versions_equal () {
25+ local NUM=$(find . -name 'servant*.cabal' | xargs grep "^version:" | awk '{ print $2 }' | uniq -c | wc -l)
26+ if [ 1 -eq $NUM ] ; then
27+ return 0
28+ else
29+ echo "versions of packages are not all the same!" && exit 1
30+ fi
31+}
scripts/start-sandbox.sh+1−5
@@ -14,11 +14,7 @@ set -o nounset
1414set -o errexit
1515
1616DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
17−SOURCES_TXT="$( dirname $DIR)/sources.txt"
18−CABAL=${CABAL:-cabal}
19−
20−declare -a SOURCES
21−readarray -t SOURCES < "$SOURCES_TXT"
17+. "$DIR"/lib/common.sh
2218
2319prepare_sandbox () {
2420 $CABAL sandbox init
scripts/test-all.sh+2−6
@@ -15,13 +15,9 @@ set -o nounset
1515set -o errexit
1616
1717DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
18−GHC_FLAGS="-Werror"
19−SOURCES_TXT="$( dirname $DIR)/sources.txt"
20−CABAL=${CABAL:-cabal}
21−
22−declare -a SOURCES
23−readarray -t SOURCES < "$SOURCES_TXT"
18+. "$DIR"/lib/common.sh
2419
20+GHC_FLAGS="-Werror"
2521
2622prepare_sandbox () {
2723 $CABAL sandbox init
scripts/update-defaults-nix.sh+1−5
@@ -12,11 +12,7 @@ set -o nounset
1212set -o errexit
1313
1414DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
15−BASE_DIR="$( dirname $DIR)"
16−SOURCES_TXT="$BASE_DIR/sources.txt"
17−
18−declare -a SOURCES
19−readarray -t SOURCES < "$SOURCES_TXT"
15+. "$DIR"/lib/common.sh
2016
2117for s in ${SOURCES[@]} ; do
2218 echo $s
scripts/upload.shnew file+52
@@ -0,0 +1,52 @@
1+#!/bin/bash -
2+#===============================================================================
3+#
4+# FILE: upload.sh
5+#
6+# USAGE: ./upload.sh <USER> <PASSWORD>
7+#
8+# DESCRIPTION: Uploads all servant packages to Hackage
9+#
10+# REQUIREMENTS: cabal, bash >= 4
11+# AUTHOR: Julian K. Arni
12+# CREATED: 05.06.2015 13:05
13+#===============================================================================
14+
15+set -o nounset
16+set -o errexit
17+
18+DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
19+. "$DIR"/lib/common.sh
20+
21+usage () {
22+ echo " upload.sh <USER> <PASSWORD>"
23+ echo " Uploads all servant packages to Hackage"
24+ exit 0
25+}
26+
27+
28+upload_package () {
29+ local package="$1"
30+ local user="$2"
31+ local pass="$3"
32+ local cabalFile="$package.cabal"
33+ pushd "$package"
34+ local version=$(grep -i '^version:' $cabalFile | awk '{ print $2 }')
35+ local sdist="dist/${package}-${version}.tar.gz"
36+ cabal sdist
37+ echo "User is: $user"
38+ cabal upload --user="$user" --password="$pass" "$sdist"
39+ popd
40+}
41+
42+
43+if [ $# -ne 2 ] ; then
44+ echo "expecting two arguments."
45+ usage
46+fi
47+
48+versions_equal
49+
50+for s in ${SOURCES[@]} ; do
51+ upload_package "$s" "$1" "$2"
52+done
servant-blaze/servant-blaze.cabal+1−1
@@ -2,7 +2,7 @@
22-- documentation, see http://haskell.org/cabal/users-guide/
33
44name: servant-blaze
5−version: 0.4.0.0
5+version: 0.4.1
66synopsis: Blaze-html support for servant
77-- description:
88homepage: http://haskell-servant.github.io/
servant-blaze/src/Servant/HTML/Blaze.hs+1−1
@@ -25,7 +25,7 @@ import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
2525
2626data HTML deriving Typeable
2727
28−-- | @text/plain;charset=utf-8@
28+-- | @text/html;charset=utf-8@
2929instance Accept HTML where
3030 contentType _ = "text" M.// "html" M./: ("charset", "utf-8")
3131
servant-client/CHANGELOG.md+4
@@ -1,3 +1,7 @@
1+0.4.1
2+-----
3+* The `HasClient` instance for `Delete cts ()` now does not care at all about content types provided.
4+
150.4
26---
37* `Delete` now is like `Get`, `Post`, `Put`, and `Patch` and returns a response body
servant-client/servant-client.cabal+4−1
@@ -1,5 +1,5 @@
11name: servant-client
2−version: 0.4.0
2+version: 0.4.1
33synopsis: automatical derivation of querying functions for servant webservices
44description:
55 This library lets you derive automatically Haskell functions that
@@ -56,6 +56,9 @@ test-suite spec
5656 default-language: Haskell2010
5757 hs-source-dirs: test
5858 main-is: Spec.hs
59+ other-modules:
60+ Servant.ClientSpec
61+ , Servant.Common.BaseUrlSpec
5962 build-depends:
6063 base == 4.*
6164 , aeson
servant-client/src/Servant/Client.hs+10−7
@@ -7,6 +7,7 @@
77{-# LANGUAGE ScopedTypeVariables #-}
88{-# LANGUAGE TypeFamilies #-}
99{-# LANGUAGE TypeOperators #-}
10+{-# LANGUAGE UndecidableInstances #-}
1011#if !MIN_VERSION_base(4,8,0)
1112{-# LANGUAGE OverlappingInstances #-}
1213#endif
@@ -126,8 +127,9 @@ instance
126127#if MIN_VERSION_base(4,8,0)
127128 {-# OVERLAPPABLE #-}
128129#endif
129− (MimeUnrender ct a) => HasClient (Delete (ct ': cts) a) where
130− type Client (Delete (ct ': cts) a) = EitherT ServantError IO a
130+ -- See https://downloads.haskell.org/~ghc/7.8.2/docs/html/users_guide/type-class-extensions.html#undecidable-instances
131+ (MimeUnrender ct a, cts' ~ (ct ': cts)) => HasClient (Delete cts' a) where
132+ type Client (Delete cts' a) = EitherT ServantError IO a
131133 clientWithRoute Proxy req baseurl =
132134 snd <$> performRequestCT (Proxy :: Proxy ct) H.methodDelete req [200, 202] baseurl
133135
@@ -137,8 +139,8 @@ instance
137139#if MIN_VERSION_base(4,8,0)
138140 {-# OVERLAPPING #-}
139141#endif
140− HasClient (Delete (ct ': cts) ()) where
141− type Client (Delete (ct ': cts) ()) = EitherT ServantError IO ()
142+ HasClient (Delete cts ()) where
143+ type Client (Delete cts ()) = EitherT ServantError IO ()
142144 clientWithRoute Proxy req baseurl =
143145 void $ performRequestNoBody H.methodDelete req [204] baseurl
144146
@@ -148,9 +150,10 @@ instance
148150#if MIN_VERSION_base(4,8,0)
149151 {-# OVERLAPPING #-}
150152#endif
151− ( MimeUnrender ct a, BuildHeadersTo ls
152− ) => HasClient (Delete (ct ': cts) (Headers ls a)) where
153− type Client (Delete (ct ': cts) (Headers ls a)) = EitherT ServantError IO (Headers ls a)
153+ -- See https://downloads.haskell.org/~ghc/7.8.2/docs/html/users_guide/type-class-extensions.html#undecidable-instances
154+ ( MimeUnrender ct a, BuildHeadersTo ls, cts' ~ (ct ': cts)
155+ ) => HasClient (Delete cts' (Headers ls a)) where
156+ type Client (Delete cts' (Headers ls a)) = EitherT ServantError IO (Headers ls a)
154157 clientWithRoute Proxy req baseurl = do
155158 (hdrs, resp) <- performRequestCT (Proxy :: Proxy ct) H.methodDelete req [200, 202] baseurl
156159 return $ Headers { getResponse = resp
servant-client/src/Servant/Common/BaseUrl.hs+23−9
@@ -1,8 +1,19 @@
1−{-# LANGUAGE DeriveGeneric #-}
2−{-# LANGUAGE ViewPatterns #-}
3−module Servant.Common.BaseUrl where
1+{-# LANGUAGE DeriveDataTypeable #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE ViewPatterns #-}
4+module Servant.Common.BaseUrl (
5+ -- * types
6+ BaseUrl (..)
7+ , InvalidBaseUrlException
8+ , Scheme (..)
9+ -- * functions
10+ , parseBaseUrl
11+ , showBaseUrl
12+) where
413
14+import Control.Monad.Catch (MonadThrow, throwM, Exception)
515import Data.List
16+import Data.Typeable
617import GHC.Generics
718import Network.URI
819import Safe
@@ -34,20 +45,23 @@ showBaseUrl (BaseUrl urlscheme host port) =
3445 (Https, 443) -> ""
3546 _ -> ":" ++ show port
3647
37−parseBaseUrl :: String -> Either String BaseUrl
48+data InvalidBaseUrlException = InvalidBaseUrlException String deriving (Show, Typeable)
49+instance Exception InvalidBaseUrlException
50+
51+parseBaseUrl :: MonadThrow m => String -> m BaseUrl
3852parseBaseUrl s = case parseURI (removeTrailingSlash s) of
3953 -- This is a rather hacky implementation and should be replaced with something
4054 -- implemented in attoparsec (which is already a dependency anyhow (via aeson)).
4155 Just (URI "http:" (Just (URIAuth "" host (':' : (readMaybe -> Just port)))) "" "" "") ->
42− Right (BaseUrl Http host port)
56+ return (BaseUrl Http host port)
4357 Just (URI "http:" (Just (URIAuth "" host "")) "" "" "") ->
44− Right (BaseUrl Http host 80)
58+ return (BaseUrl Http host 80)
4559 Just (URI "https:" (Just (URIAuth "" host (':' : (readMaybe -> Just port)))) "" "" "") ->
46− Right (BaseUrl Https host port)
60+ return (BaseUrl Https host port)
4761 Just (URI "https:" (Just (URIAuth "" host "")) "" "" "") ->
48− Right (BaseUrl Https host 443)
62+ return (BaseUrl Https host 443)
4963 _ -> if "://" `isInfixOf` s
50− then Left ("invalid base url: " ++ s)
64+ then throwM (InvalidBaseUrlException $ "Invalid base URL: " ++ s)
5165 else parseBaseUrl ("http://" ++ s)
5266 where
5367 removeTrailingSlash str = case lastMay str of
servant-client/src/Servant/Common/Req.hs+5−1
@@ -1,3 +1,4 @@
1+{-# LANGUAGE DeriveDataTypeable #-}
12{-# LANGUAGE CPP #-}
23{-# LANGUAGE OverloadedStrings #-}
34{-# LANGUAGE ScopedTypeVariables #-}
@@ -18,6 +19,7 @@ import Data.String.Conversions
1819import Data.Proxy
1920import Data.Text (Text)
2021import Data.Text.Encoding
22+import Data.Typeable
2123import Network.HTTP.Client hiding (Proxy)
2224import Network.HTTP.Client.TLS
2325import Network.HTTP.Media
@@ -53,7 +55,9 @@ data ServantError
5355 | ConnectionError
5456 { connectionError :: String
5557 }
56− deriving (Show)
58+ deriving (Show, Typeable)
59+
60+instance Exception ServantError
5761
5862data Req = Req
5963 { reqPath :: String
servant-client/test/Servant/ClientSpec.hs+17−9
@@ -80,7 +80,7 @@ type TestHeaders = '[Header "X-Example1" Int, Header "X-Example2" String]
8080
8181type Api =
8282 "get" :> Get '[JSON] Person
83− :<|> "delete" :> Delete '[JSON] ()
83+ :<|> "deleteEmpty" :> Delete '[] ()
8484 :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person
8585 :<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person
8686 :<|> "param" :> QueryParam "name" String :> Get '[FormUrlEncoded,JSON] Person
@@ -98,6 +98,7 @@ type Api =
9898 ReqBody '[JSON] [(String, [Rational])] :>
9999 Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])
100100 :<|> "headers" :> Get '[JSON] (Headers TestHeaders Bool)
101+ :<|> "deleteContentType" :> Delete '[JSON] ()
101102api :: Proxy Api
102103api = Proxy
103104
@@ -123,6 +124,7 @@ server = serve api (
123124 :<|> (\ _request respond -> respond $ responseLBS badRequest400 [] "rawFailure")
124125 :<|> (\ a b c d -> return (a, b, c, d))
125126 :<|> (return $ addHeader 1729 $ addHeader "eg2" True)
127+ :<|> return ()
126128 )
127129
128130withServer :: (BaseUrl -> IO a) -> IO a
@@ -148,7 +150,7 @@ withFailServer action = withWaiDaemon (return failServer) action
148150spec :: IO ()
149151spec = withServer $ \ baseUrl -> do
150152 let getGet :: EitherT ServantError IO Person
151− getDelete :: EitherT ServantError IO ()
153+ getDeleteEmpty :: EitherT ServantError IO ()
152154 getCapture :: String -> EitherT ServantError IO Person
153155 getBody :: Person -> EitherT ServantError IO Person
154156 getQueryParam :: Maybe String -> EitherT ServantError IO Person
@@ -161,8 +163,9 @@ spec = withServer $ \ baseUrl -> do
161163 getRawFailure :: Method -> EitherT ServantError IO (Int, ByteString, MediaType, [HTTP.Header], C.Response ByteString)
162164 getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])] -> EitherT ServantError IO (String, Maybe Int, Bool, [(String, [Rational])])
163165 getRespHeaders :: EitherT ServantError IO (Headers TestHeaders Bool)
166+ getDeleteContentType :: EitherT ServantError IO ()
164167 ( getGet
165− :<|> getDelete
168+ :<|> getDeleteEmpty
166169 :<|> getCapture
167170 :<|> getBody
168171 :<|> getQueryParam
@@ -174,15 +177,20 @@ spec = withServer $ \ baseUrl -> do
174177 :<|> getRawSuccess
175178 :<|> getRawFailure
176179 :<|> getMultiple
177− :<|> getRespHeaders)
180+ :<|> getRespHeaders
181+ :<|> getDeleteContentType)
178182 = client api baseUrl
179183
180184 hspec $ do
181185 it "Servant.API.Get" $ do
182186 (Arrow.left show <$> runEitherT getGet) `shouldReturn` Right alice
183187
184− it "Servant.API.Delete" $ do
185− (Arrow.left show <$> runEitherT getDelete) `shouldReturn` Right ()
188+ describe "Servant.API.Delete" $ do
189+ it "allows empty content type" $ do
190+ (Arrow.left show <$> runEitherT getDeleteEmpty) `shouldReturn` Right ()
191+
192+ it "allows content type" $ do
193+ (Arrow.left show <$> runEitherT getDeleteContentType) `shouldReturn` Right ()
186194
187195 it "Servant.API.Capture" $ do
188196 (Arrow.left show <$> runEitherT (getCapture "Paula")) `shouldReturn` Right (Person "Paula" 0)
@@ -274,11 +282,11 @@ spec = withServer $ \ baseUrl -> do
274282failSpec :: IO ()
275283failSpec = withFailServer $ \ baseUrl -> do
276284 let getGet :: EitherT ServantError IO Person
277− getDelete :: EitherT ServantError IO ()
285+ getDeleteEmpty :: EitherT ServantError IO ()
278286 getCapture :: String -> EitherT ServantError IO Person
279287 getBody :: Person -> EitherT ServantError IO Person
280288 ( getGet
281− :<|> getDelete
289+ :<|> getDeleteEmpty
282290 :<|> getCapture
283291 :<|> getBody
284292 :<|> _ )
@@ -289,7 +297,7 @@ failSpec = withFailServer $ \ baseUrl -> do
289297 hspec $ do
290298 context "client returns errors appropriately" $ do
291299 it "reports FailureResponse" $ do
292− Left res <- runEitherT getDelete
300+ Left res <- runEitherT getDeleteEmpty
293301 case res of
294302 FailureResponse (Status 404 "Not Found") _ _ -> return ()
295303 _ -> fail $ "expected 404 response, but got " <> show res
servant-docs/servant-docs.cabal+1−1
@@ -1,5 +1,5 @@
11name: servant-docs
2−version: 0.4.0
2+version: 0.4.1
33synopsis: generate API docs for your servant webservice
44description:
55 Library for generating API docs from a servant API definition.
servant-examples/auth-combinator/auth-combinator.hs+9−8
@@ -28,14 +28,15 @@ data AuthProtected
2828instance HasServer rest => HasServer (AuthProtected :> rest) where
2929 type ServerT (AuthProtected :> rest) m = ServerT rest m
3030
31− route Proxy a request respond =
32− case lookup "Cookie" (requestHeaders request) of
33− Nothing -> respond . succeedWith $ responseLBS status401 [] "Missing auth header."
34− Just v -> do
35− authGranted <- isGoodCookie v
36− if authGranted
37− then route (Proxy :: Proxy rest) a request respond
38− else respond . succeedWith $ responseLBS status403 [] "Invalid cookie."
31+ route Proxy a = WithRequest $ \ request ->
32+ route (Proxy :: Proxy rest) $ do
33+ case lookup "Cookie" (requestHeaders request) of
34+ Nothing -> return $ failWith $ HttpError status401 (Just "Missing auth header.")
35+ Just v -> do
36+ authGranted <- isGoodCookie v
37+ if authGranted
38+ then a
39+ else return $ failWith $ HttpError status403 (Just "Invalid cookie.")
3940
4041type PrivateAPI = Get '[JSON] [PrivateData]
4142
servant-examples/servant-examples.cabal+12−12
@@ -1,5 +1,5 @@
11name: servant-examples
2−version: 0.4
2+version: 0.4.1
33synopsis: Example programs for servant
44description: Example programs for servant,
55 showcasing solutions to common needs.
@@ -26,11 +26,11 @@ executable tutorial
2626 , js-jquery
2727 , lucid
2828 , random
29− , servant
30− , servant-docs
31− , servant-jquery
32− , servant-lucid
33− , servant-server
29+ , servant == 0.4.*
30+ , servant-docs == 0.4.*
31+ , servant-jquery == 0.4.*
32+ , servant-lucid == 0.4.*
33+ , servant-server == 0.4.*
3434 , text
3535 , time
3636 , transformers
@@ -47,9 +47,9 @@ executable t8-main
4747 aeson
4848 , base >= 4.7 && < 5
4949 , either
50− , servant
51− , servant-client
52− , servant-server
50+ , servant == 0.4.*
51+ , servant-client == 0.4.*
52+ , servant-server == 0.4.*
5353 , wai
5454
5555executable hackage
@@ -70,8 +70,8 @@ executable wai-middleware
7070 build-depends:
7171 aeson >= 0.8
7272 , base >= 4.7 && < 5
73− , servant
74− , servant-server
73+ , servant == 0.4.*
74+ , servant-server == 0.4.*
7575 , text
7676 , wai
7777 , wai-extra
@@ -86,7 +86,7 @@ executable auth-combinator
8686 , base >= 4.7 && < 5
8787 , bytestring
8888 , http-types
89− , servant
89+ , servant == 0.4.*
9090 , servant-server == 0.4.*
9191 , text
9292 , wai
servant-jquery/servant-jquery.cabal+1−1
@@ -1,5 +1,5 @@
11name: servant-jquery
2−version: 0.4.0
2+version: 0.4.1
33synopsis: Automatically derive (jquery) javascript functions to query servant webservices
44description:
55 Automatically derive jquery-based javascript functions to query servant webservices.
servant-lucid/servant-lucid.cabal+1−1
@@ -2,7 +2,7 @@
22-- documentation, see http://haskell.org/cabal/users-guide/
33
44name: servant-lucid
5−version: 0.4.0.0
5+version: 0.4.1
66synopsis: Servant support for lucid
77-- description:
88homepage: http://haskell-servant.github.io/
servant-lucid/src/Servant/HTML/Lucid.hs+1−1
@@ -24,7 +24,7 @@ import Servant.API (Accept (..), MimeRender (..))
2424
2525data HTML deriving Typeable
2626
27−-- | @text/plain;charset=utf-8@
27+-- | @text/html;charset=utf-8@
2828instance Accept HTML where
2929 contentType _ = "text" M.// "html" M./: ("charset", "utf-8")
3030
servant-server/CHANGELOG.md+6
@@ -1,3 +1,9 @@
1+0.4.1
2+-----
3+* Bump attoparsec upper bound to < 0.14
4+* Bump wai-app-static upper bound to < 3.2
5+* Bump either upper bound to < 4.5
6+
170.4
28---
39* `Delete` now is like `Get`, `Post`, `Put`, and `Patch` and returns a response body
servant-server/example/greet.hs+1−1
@@ -44,7 +44,7 @@ testApi = Proxy
4444-- There's one handler per endpoint, which, just like in the type
4545-- that represents the API, are glued together using :<|>.
4646--
47−-- Each handler runs in the 'EitherT (Int, String) IO' monad.
47+-- Each handler runs in the 'EitherT ServantErr IO' monad.
4848server :: Server TestApi
4949server = helloH :<|> postGreetH :<|> deleteGreetH
5050
servant-server/servant-server.cabal+10−6
@@ -1,5 +1,5 @@
11name: servant-server
2−version: 0.4.0
2+version: 0.4.1
33synopsis: A family of combinators for defining webservices APIs and serving them
44description:
55 A family of combinators for defining webservices APIs and serving them
@@ -35,15 +35,19 @@ library
3535 Servant
3636 Servant.Server
3737 Servant.Server.Internal
38− Servant.Server.Internal.ServantErr
3938 Servant.Server.Internal.Enter
39+ Servant.Server.Internal.PathInfo
40+ Servant.Server.Internal.Router
41+ Servant.Server.Internal.RoutingApplication
42+ Servant.Server.Internal.ServantErr
4043 Servant.Utils.StaticFiles
4144 build-depends:
4245 base >= 4.7 && < 5
43− , aeson >= 0.7 && < 0.9
44− , attoparsec >= 0.12 && < 0.13
46+ , aeson >= 0.7 && < 0.10
47+ , attoparsec >= 0.12 && < 0.14
4548 , bytestring >= 0.10 && < 0.11
46− , either >= 4.3 && < 4.4
49+ , containers >= 0.5 && < 0.6
50+ , either >= 4.3 && < 4.5
4751 , http-types >= 0.8 && < 0.9
4852 , network-uri >= 2.6 && < 2.7
4953 , mtl >= 2 && < 3
@@ -51,7 +55,7 @@ library
5155 , safe >= 0.3 && < 0.4
5256 , servant == 0.4.*
5357 , split >= 0.2 && < 0.3
54− , string-conversions >= 0.3 && < 0.4
58+ , string-conversions >= 0.3 && < 0.5
5559 , system-filepath >= 0.4 && < 0.5
5660 , filepath >= 1
5761 , text >= 1.2 && < 1.3
servant-server/src/Servant/Server.hs+1−2
@@ -81,7 +81,6 @@ import Data.Proxy (Proxy)
8181import Network.Wai (Application)
8282import Servant.Server.Internal
8383import Servant.Server.Internal.Enter
84−import Servant.Server.Internal.ServantErr
8584
8685
8786-- * Implementing Servers
@@ -108,7 +107,7 @@ import Servant.Server.Internal.ServantErr
108107-- > main = Network.Wai.Handler.Warp.run 8080 app
109108--
110109serve :: HasServer layout => Proxy layout -> Server layout -> Application
111−serve p server = toApplication (route p server)
110+serve p server = toApplication (runRouter (route p (return (RR (Right server)))))
112111
113112
114113-- Documentation
servant-server/src/Servant/Server/Internal.hs+169−426
@@ -2,6 +2,7 @@
22{-# LANGUAGE DataKinds #-}
33{-# LANGUAGE FlexibleContexts #-}
44{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
56{-# LANGUAGE OverloadedStrings #-}
67{-# LANGUAGE PolyKinds #-}
78{-# LANGUAGE ScopedTypeVariables #-}
@@ -11,17 +12,20 @@
1112{-# LANGUAGE OverlappingInstances #-}
1213#endif
1314
14−module Servant.Server.Internal where
15+module Servant.Server.Internal
16+ ( module Servant.Server.Internal
17+ , module Servant.Server.Internal.PathInfo
18+ , module Servant.Server.Internal.Router
19+ , module Servant.Server.Internal.RoutingApplication
20+ , module Servant.Server.Internal.ServantErr
21+ ) where
1522
1623#if !MIN_VERSION_base(4,8,0)
1724import Control.Applicative ((<$>))
18−import Data.Monoid (Monoid, mappend, mempty)
1925#endif
20−import Control.Monad.Trans.Either (EitherT, runEitherT)
26+import Control.Monad.Trans.Either (EitherT)
2127import qualified Data.ByteString as B
22−import qualified Data.ByteString.Lazy as BL
23−import Data.IORef (newIORef, readIORef, writeIORef)
24−import Data.List (unfoldr)
28+import qualified Data.Map as M
2529import Data.Maybe (catMaybes, fromMaybe)
2630import Data.String (fromString)
2731import Data.String.Conversions (cs, (<>))
@@ -31,12 +35,9 @@ import Data.Text.Encoding (decodeUtf8, encodeUtf8)
3135import Data.Typeable
3236import GHC.TypeLits (KnownSymbol, symbolVal)
3337import Network.HTTP.Types hiding (Header, ResponseHeaders)
34−import Network.Wai (Application, Request, Response,
35− ResponseReceived, lazyRequestBody,
36− pathInfo, rawQueryString,
37− requestBody, requestHeaders,
38− requestMethod, responseLBS,
39− strictRequestBody)
38+import Network.Wai (Application, lazyRequestBody,
39+ rawQueryString, requestHeaders,
40+ requestMethod, responseLBS)
4041import Servant.API ((:<|>) (..), (:>), Capture,
4142 Delete, Get, Header,
4243 MatrixFlag, MatrixParam, MatrixParams,
@@ -50,136 +51,15 @@ import Servant.API.ResponseHeaders (Headers, getResponse, GetHeaders,
5051 getHeaders)
5152import Servant.Common.Text (FromText, fromText)
5253
54+import Servant.Server.Internal.PathInfo
55+import Servant.Server.Internal.Router
56+import Servant.Server.Internal.RoutingApplication
5357import Servant.Server.Internal.ServantErr
5458
55−data ReqBodyState = Uncalled
56− | Called !B.ByteString
57− | Done !B.ByteString
58−
59−
60−toApplication :: RoutingApplication -> Application
61−toApplication ra request respond = do
62− reqBodyRef <- newIORef Uncalled
63− -- We may need to consume the requestBody more than once. In order to
64− -- maintain the illusion that 'requestBody' works as expected,
65− -- 'ReqBodyState' is introduced, and the complete body is memoized and
66− -- returned as many times as requested with empty "Done" marker chunks in
67− -- between.
68− -- See https://github.com/haskell-servant/servant/issues/3
69− let memoReqBody = do
70− ior <- readIORef reqBodyRef
71− case ior of
72− Uncalled -> do
73− r <- BL.toStrict <$> strictRequestBody request
74− writeIORef reqBodyRef $ Done r
75− return r
76− Called bs -> do
77− writeIORef reqBodyRef $ Done bs
78− return bs
79− Done bs -> do
80− writeIORef reqBodyRef $ Called bs
81− return B.empty
82−
83− ra request{ requestBody = memoReqBody } (routingRespond . routeResult)
84− where
85− routingRespond :: Either RouteMismatch Response -> IO ResponseReceived
86− routingRespond (Left NotFound) =
87− respond $ responseLBS notFound404 [] "not found"
88− routingRespond (Left WrongMethod) =
89− respond $ responseLBS methodNotAllowed405 [] "method not allowed"
90− routingRespond (Left (InvalidBody err)) =
91− respond $ responseLBS badRequest400 [] $ fromString $ "invalid request body: " ++ err
92− routingRespond (Left UnsupportedMediaType) =
93− respond $ responseLBS unsupportedMediaType415 [] "unsupported media type"
94− routingRespond (Left (HttpError status body)) =
95− respond $ responseLBS status [] $ fromMaybe (BL.fromStrict $ statusMessage status) body
96− routingRespond (Right response) =
97− respond response
98−
99−-- Note that the ordering of the constructors has great significance! It
100−-- determines the Ord instance and, consequently, the monoid instance.
101−-- * Route mismatch
102−data RouteMismatch =
103− NotFound -- ^ the usual "not found" error
104− | WrongMethod -- ^ a more informative "you just got the HTTP method wrong" error
105− | UnsupportedMediaType -- ^ request body has unsupported media type
106− | InvalidBody String -- ^ an even more informative "your json request body wasn't valid" error
107− | HttpError Status (Maybe BL.ByteString) -- ^ an even even more informative arbitrary HTTP response code error.
108− deriving (Eq, Ord, Show)
109−
110−instance Monoid RouteMismatch where
111− mempty = NotFound
112− -- The following isn't great, since it picks @InvalidBody@ based on
113− -- alphabetical ordering, but any choice would be arbitrary.
114− --
115− -- "As one judge said to the other, 'Be just and if you can't be just, be
116− -- arbitrary'" -- William Burroughs
117− mappend = max
118−
119−
120−-- | A wrapper around @'Either' 'RouteMismatch' a@.
121−newtype RouteResult a =
122− RR { routeResult :: Either RouteMismatch a }
123− deriving (Eq, Show)
124−
125−failWith :: RouteMismatch -> RouteResult a
126−failWith = RR . Left
127−
128−succeedWith :: a -> RouteResult a
129−succeedWith = RR . Right
130−
131−isMismatch :: RouteResult a -> Bool
132−isMismatch (RR (Left _)) = True
133−isMismatch _ = False
134−
135−-- | Like `null . pathInfo`, but works with redundant trailing slashes.
136−pathIsEmpty :: Request -> Bool
137−pathIsEmpty = f . processedPathInfo
138− where
139− f [] = True
140− f [""] = True
141− f _ = False
142−
143−-- | If we get a `Right`, it has precedence over everything else.
144−--
145−-- This in particular means that if we could get several 'Right's,
146−-- only the first we encounter would be taken into account.
147−instance Monoid (RouteResult a) where
148− mempty = RR $ Left mempty
149−
150− RR (Left x) `mappend` RR (Left y) = RR $ Left (x <> y)
151− RR (Left _) `mappend` RR (Right y) = RR $ Right y
152− r `mappend` _ = r
153−
154−type RoutingApplication =
155− Request -- ^ the request, the field 'pathInfo' may be modified by url routing
156− -> (RouteResult Response -> IO ResponseReceived) -> IO ResponseReceived
157−
158−splitMatrixParameters :: Text -> (Text, Text)
159−splitMatrixParameters = T.break (== ';')
160−
161−parsePathInfo :: Request -> [Text]
162−parsePathInfo = filter (/= "") . mergePairs . map splitMatrixParameters . pathInfo
163− where mergePairs = concat . unfoldr pairToList
164− pairToList [] = Nothing
165− pairToList ((a, b):xs) = Just ([a, b], xs)
166−
167−-- | Returns a processed pathInfo from the request.
168−--
169−-- In order to handle matrix parameters in the request correctly, the raw pathInfo needs to be
170−-- processed, so routing works as intended. Therefor this function should be used to access
171−-- the pathInfo for routing purposes.
172−processedPathInfo :: Request -> [Text]
173−processedPathInfo r =
174− case pinfo of
175− (x:xs) | T.head x == ';' -> xs
176− _ -> pinfo
177− where pinfo = parsePathInfo r
178−
17959class HasServer layout where
18060 type ServerT layout (m :: * -> *) :: *
18161
182− route :: Proxy layout -> Server layout -> RoutingApplication
62+ route :: Proxy layout -> IO (RouteResult (Server layout)) -> Router
18363
18464type Server layout = ServerT layout (EitherT ServantErr IO)
18565
@@ -200,12 +80,8 @@ instance (HasServer a, HasServer b) => HasServer (a :<|> b) where
20080
20181 type ServerT (a :<|> b) m = ServerT a m :<|> ServerT b m
20282
203− route Proxy (a :<|> b) request respond =
204− route pa a request $ \mResponse ->
205− if isMismatch mResponse
206− then route pb b request $ \mResponse' -> respond (mResponse <> mResponse')
207− else respond mResponse
208−
83+ route Proxy server = choice (route pa (extractL <$> server))
84+ (route pb (extractR <$> server))
20985 where pa = Proxy :: Proxy a
21086 pb = Proxy :: Proxy b
21187
@@ -227,7 +103,7 @@ captured _ = fromText
227103-- >
228104-- > server :: Server MyApi
229105-- > server = getBook
230−-- > where getBook :: Text -> EitherT (Int, String) IO Book
106+-- > where getBook :: Text -> EitherT ServantErr IO Book
231107-- > getBook isbn = ...
232108instance (KnownSymbol capture, FromText a, HasServer sublayout)
233109 => HasServer (Capture capture a :> sublayout) where
@@ -235,25 +111,72 @@ instance (KnownSymbol capture, FromText a, HasServer sublayout)
235111 type ServerT (Capture capture a :> sublayout) m =
236112 a -> ServerT sublayout m
237113
238− route Proxy subserver request respond = case processedPathInfo request of
239− (first : rest)
240− -> case captured captureProxy first of
241− Nothing -> respond $ failWith NotFound
242− Just v -> route (Proxy :: Proxy sublayout) (subserver v) request{
243− pathInfo = rest
244− } respond
245− _ -> respond $ failWith NotFound
246−
114+ route Proxy subserver =
115+ DynamicRouter $ \ first ->
116+ route (Proxy :: Proxy sublayout)
117+ (case captured captureProxy first of
118+ Nothing -> return $ failWith NotFound
119+ Just v -> feedTo subserver v)
247120 where captureProxy = Proxy :: Proxy (Capture capture a)
248121
249122
123+methodRouter :: (AllCTRender ctypes a)
124+ => Method -> Proxy ctypes -> Status
125+ -> IO (RouteResult (EitherT ServantErr IO a))
126+ -> Router
127+methodRouter method proxy status action = LeafRouter route'
128+ where
129+ route' request respond
130+ | pathIsEmpty request && requestMethod request == method = do
131+ runAction action respond $ \ output -> do
132+ let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
133+ case handleAcceptH proxy (AcceptHeader accH) output of
134+ Nothing -> failWith UnsupportedMediaType
135+ Just (contentT, body) -> succeedWith $
136+ responseLBS status [ ("Content-Type" , cs contentT)] body
137+ | pathIsEmpty request && requestMethod request /= method =
138+ respond $ failWith WrongMethod
139+ | otherwise = respond $ failWith NotFound
140+
141+methodRouterHeaders :: (GetHeaders (Headers h v), AllCTRender ctypes v)
142+ => Method -> Proxy ctypes -> Status
143+ -> IO (RouteResult (EitherT ServantErr IO (Headers h v)))
144+ -> Router
145+methodRouterHeaders method proxy status action = LeafRouter route'
146+ where
147+ route' request respond
148+ | pathIsEmpty request && requestMethod request == method = do
149+ runAction action respond $ \ output -> do
150+ let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
151+ headers = getHeaders output
152+ case handleAcceptH proxy (AcceptHeader accH) (getResponse output) of
153+ Nothing -> failWith UnsupportedMediaType
154+ Just (contentT, body) -> succeedWith $
155+ responseLBS status ( ("Content-Type" , cs contentT) : headers) body
156+ | pathIsEmpty request && requestMethod request /= method =
157+ respond $ failWith WrongMethod
158+ | otherwise = respond $ failWith NotFound
159+
160+methodRouterEmpty :: Method
161+ -> IO (RouteResult (EitherT ServantErr IO ()))
162+ -> Router
163+methodRouterEmpty method action = LeafRouter route'
164+ where
165+ route' request respond
166+ | pathIsEmpty request && requestMethod request == method = do
167+ runAction action respond $ \ () ->
168+ succeedWith $ responseLBS noContent204 [] ""
169+ | pathIsEmpty request && requestMethod request /= method =
170+ respond $ failWith WrongMethod
171+ | otherwise = respond $ failWith NotFound
172+
250173-- | If you have a 'Delete' endpoint in your API,
251174-- the handler for this endpoint is meant to delete
252175-- a resource.
253176--
254177-- The code of the handler will, just like
255178-- for 'Servant.API.Get.Get', 'Servant.API.Post.Post' and
256−-- 'Servant.API.Put.Put', run in @EitherT (Int, String) IO ()@.
179+-- 'Servant.API.Put.Put', run in @EitherT ServantErr IO ()@.
257180-- The 'Int' represents the status code and the 'String' a message
258181-- to be returned. You can use 'Control.Monad.Trans.Either.left' to
259182-- painlessly error out if the conditions for a successful deletion
@@ -267,20 +190,7 @@ instance
267190
268191 type ServerT (Delete ctypes a) m = m a
269192
270− route Proxy action request respond
271− | pathIsEmpty request && requestMethod request == methodDelete = do
272− e <- runEitherT action
273− respond $ case e of
274− Right output -> do
275− let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
276− case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
277− Nothing -> failWith UnsupportedMediaType
278− Just (contentT, body) -> succeedWith $
279− responseLBS status200 [ ("Content-Type" , cs contentT)] body
280− Left err -> succeedWith $ responseServantErr err
281− | pathIsEmpty request && requestMethod request /= methodDelete =
282− respond $ failWith WrongMethod
283− | otherwise = respond $ failWith NotFound
193+ route Proxy = methodRouter methodDelete (Proxy :: Proxy ctypes) ok200
284194
285195instance
286196#if MIN_VERSION_base(4,8,0)
@@ -290,15 +200,7 @@ instance
290200
291201 type ServerT (Delete ctypes ()) m = m ()
292202
293− route Proxy action request respond
294− | pathIsEmpty request && requestMethod request == methodDelete = do
295− e <- runEitherT action
296− respond . succeedWith $ case e of
297− Right () -> responseLBS noContent204 [] ""
298− Left err -> responseServantErr err
299− | pathIsEmpty request && requestMethod request /= methodDelete =
300− respond $ failWith WrongMethod
301− | otherwise = respond $ failWith NotFound
203+ route Proxy = methodRouterEmpty methodDelete
302204
303205-- Add response headers
304206instance
@@ -310,26 +212,12 @@ instance
310212
311213 type ServerT (Delete ctypes (Headers h v)) m = m (Headers h v)
312214
313− route Proxy action request respond
314− | pathIsEmpty request && requestMethod request == methodDelete = do
315− e <- runEitherT action
316− respond $ case e of
317− Right output -> do
318− let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
319− headers = getHeaders output
320− case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) (getResponse output) of
321− Nothing -> failWith UnsupportedMediaType
322− Just (contentT, body) -> succeedWith $
323− responseLBS status200 ( ("Content-Type" , cs contentT) : headers) body
324− Left err -> succeedWith $ responseServantErr err
325− | pathIsEmpty request && requestMethod request /= methodDelete =
326− respond $ failWith WrongMethod
327− | otherwise = respond $ failWith NotFound
215+ route Proxy = methodRouterHeaders methodDelete (Proxy :: Proxy ctypes) ok200
328216
329217-- | When implementing the handler for a 'Get' endpoint,
330218-- just like for 'Servant.API.Delete.Delete', 'Servant.API.Post.Post'
331219-- and 'Servant.API.Put.Put', the handler code runs in the
332−-- @EitherT (Int, String) IO@ monad, where the 'Int' represents
220+-- @EitherT ServantErr IO@ monad, where the 'Int' represents
333221-- the status code and the 'String' a message, returned in case of
334222-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
335223-- to quickly fail if some conditions are not met.
@@ -347,20 +235,7 @@ instance
347235
348236 type ServerT (Get ctypes a) m = m a
349237
350− route Proxy action request respond
351− | pathIsEmpty request && requestMethod request == methodGet = do
352− e <- runEitherT action
353− respond $ case e of
354− Right output -> do
355− let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
356− case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
357− Nothing -> failWith UnsupportedMediaType
358− Just (contentT, body) -> succeedWith $
359− responseLBS ok200 [ ("Content-Type" , cs contentT)] body
360− Left err -> succeedWith $ responseServantErr err
361− | pathIsEmpty request && requestMethod request /= methodGet =
362− respond $ failWith WrongMethod
363− | otherwise = respond $ failWith NotFound
238+ route Proxy = methodRouter methodGet (Proxy :: Proxy ctypes) ok200
364239
365240-- '()' ==> 204 No Content
366241instance
@@ -371,15 +246,7 @@ instance
371246
372247 type ServerT (Get ctypes ()) m = m ()
373248
374− route Proxy action request respond
375− | pathIsEmpty request && requestMethod request == methodGet = do
376− e <- runEitherT action
377− respond . succeedWith $ case e of
378− Right () -> responseLBS noContent204 [] ""
379− Left err -> responseServantErr err
380− | pathIsEmpty request && requestMethod request /= methodGet =
381− respond $ failWith WrongMethod
382− | otherwise = respond $ failWith NotFound
249+ route Proxy = methodRouterEmpty methodGet
383250
384251-- Add response headers
385252instance
@@ -391,21 +258,7 @@ instance
391258
392259 type ServerT (Get ctypes (Headers h v)) m = m (Headers h v)
393260
394− route Proxy action request respond
395− | pathIsEmpty request && requestMethod request == methodGet = do
396− e <- runEitherT action
397− respond $ case e of
398− Right output -> do
399− let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
400− headers = getHeaders output
401− case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) (getResponse output) of
402− Nothing -> failWith UnsupportedMediaType
403− Just (contentT, body) -> succeedWith $
404− responseLBS ok200 ( ("Content-Type" , cs contentT) : headers) body
405− Left err -> succeedWith $ responseServantErr err
406− | pathIsEmpty request && requestMethod request /= methodGet =
407− respond $ failWith WrongMethod
408− | otherwise = respond $ failWith NotFound
261+ route Proxy = methodRouterHeaders methodGet (Proxy :: Proxy ctypes) ok200
409262
410263-- | If you use 'Header' in one of the endpoints for your API,
411264-- this automatically requires your server-side handler to be a function
@@ -425,7 +278,7 @@ instance
425278-- >
426279-- > server :: Server MyApi
427280-- > server = viewReferer
428−-- > where viewReferer :: Referer -> EitherT (Int, String) IO referer
281+-- > where viewReferer :: Referer -> EitherT ServantErr IO referer
429282-- > viewReferer referer = return referer
430283instance (KnownSymbol sym, FromText a, HasServer sublayout)
431284 => HasServer (Header sym a :> sublayout) where
@@ -433,16 +286,15 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
433286 type ServerT (Header sym a :> sublayout) m =
434287 Maybe a -> ServerT sublayout m
435288
436− route Proxy subserver request respond = do
289+ route Proxy subserver = WithRequest $ \ request ->
437290 let mheader = fromText . decodeUtf8 =<< lookup str (requestHeaders request)
438− route (Proxy :: Proxy sublayout) (subserver mheader) request respond
439−
440− where str = fromString $ symbolVal (Proxy :: Proxy sym)
291+ in route (Proxy :: Proxy sublayout) (feedTo subserver mheader)
292+ where str = fromString $ symbolVal (Proxy :: Proxy sym)
441293
442294-- | When implementing the handler for a 'Post' endpoint,
443295-- just like for 'Servant.API.Delete.Delete', 'Servant.API.Get.Get'
444296-- and 'Servant.API.Put.Put', the handler code runs in the
445−-- @EitherT (Int, String) IO@ monad, where the 'Int' represents
297+-- @EitherT ServantErr IO@ monad, where the 'Int' represents
446298-- the status code and the 'String' a message, returned in case of
447299-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
448300-- to quickly fail if some conditions are not met.
@@ -461,20 +313,7 @@ instance
461313
462314 type ServerT (Post ctypes a) m = m a
463315
464− route Proxy action request respond
465− | pathIsEmpty request && requestMethod request == methodPost = do
466− e <- runEitherT action
467− respond $ case e of
468− Right output -> do
469− let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
470− case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
471− Nothing -> failWith UnsupportedMediaType
472− Just (contentT, body) -> succeedWith $
473− responseLBS status201 [ ("Content-Type" , cs contentT)] body
474− Left err -> succeedWith $ responseServantErr err
475− | pathIsEmpty request && requestMethod request /= methodPost =
476− respond $ failWith WrongMethod
477− | otherwise = respond $ failWith NotFound
316+ route Proxy = methodRouter methodPost (Proxy :: Proxy ctypes) created201
478317
479318instance
480319#if MIN_VERSION_base(4,8,0)
@@ -484,15 +323,7 @@ instance
484323
485324 type ServerT (Post ctypes ()) m = m ()
486325
487− route Proxy action request respond
488− | pathIsEmpty request && requestMethod request == methodPost = do
489− e <- runEitherT action
490− respond . succeedWith $ case e of
491− Right () -> responseLBS noContent204 [] ""
492− Left err -> responseServantErr err
493− | pathIsEmpty request && requestMethod request /= methodPost =
494− respond $ failWith WrongMethod
495− | otherwise = respond $ failWith NotFound
326+ route Proxy = methodRouterEmpty methodPost
496327
497328-- Add response headers
498329instance
@@ -504,26 +335,12 @@ instance
504335
505336 type ServerT (Post ctypes (Headers h v)) m = m (Headers h v)
506337
507− route Proxy action request respond
508− | pathIsEmpty request && requestMethod request == methodPost = do
509− e <- runEitherT action
510− respond $ case e of
511− Right output -> do
512− let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
513− headers = getHeaders output
514− case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) (getResponse output) of
515− Nothing -> failWith UnsupportedMediaType
516− Just (contentT, body) -> succeedWith $
517− responseLBS status201 ( ("Content-Type" , cs contentT) : headers) body
518− Left err -> succeedWith $ responseServantErr err
519− | pathIsEmpty request && requestMethod request /= methodPost =
520− respond $ failWith WrongMethod
521− | otherwise = respond $ failWith NotFound
338+ route Proxy = methodRouterHeaders methodPost (Proxy :: Proxy ctypes) created201
522339
523340-- | When implementing the handler for a 'Put' endpoint,
524341-- just like for 'Servant.API.Delete.Delete', 'Servant.API.Get.Get'
525342-- and 'Servant.API.Post.Post', the handler code runs in the
526−-- @EitherT (Int, String) IO@ monad, where the 'Int' represents
343+-- @EitherT ServantErr IO@ monad, where the 'Int' represents
527344-- the status code and the 'String' a message, returned in case of
528345-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
529346-- to quickly fail if some conditions are not met.
@@ -541,20 +358,7 @@ instance
541358
542359 type ServerT (Put ctypes a) m = m a
543360
544− route Proxy action request respond
545− | pathIsEmpty request && requestMethod request == methodPut = do
546− e <- runEitherT action
547− respond $ case e of
548− Right output -> do
549− let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
550− case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
551− Nothing -> failWith UnsupportedMediaType
552− Just (contentT, body) -> succeedWith $
553− responseLBS status200 [ ("Content-Type" , cs contentT)] body
554− Left err -> succeedWith $ responseServantErr err
555− | pathIsEmpty request && requestMethod request /= methodPut =
556− respond $ failWith WrongMethod
557− | otherwise = respond $ failWith NotFound
361+ route Proxy = methodRouter methodPut (Proxy :: Proxy ctypes) ok200
558362
559363instance
560364#if MIN_VERSION_base(4,8,0)
@@ -564,15 +368,7 @@ instance
564368
565369 type ServerT (Put ctypes ()) m = m ()
566370
567− route Proxy action request respond
568− | pathIsEmpty request && requestMethod request == methodPut = do
569− e <- runEitherT action
570− respond . succeedWith $ case e of
571− Right () -> responseLBS noContent204 [] ""
572− Left err -> responseServantErr err
573− | pathIsEmpty request && requestMethod request /= methodPut =
574− respond $ failWith WrongMethod
575− | otherwise = respond $ failWith NotFound
371+ route Proxy = methodRouterEmpty methodPut
576372
577373-- Add response headers
578374instance
@@ -584,26 +380,12 @@ instance
584380
585381 type ServerT (Put ctypes (Headers h v)) m = m (Headers h v)
586382
587− route Proxy action request respond
588− | pathIsEmpty request && requestMethod request == methodPut = do
589− e <- runEitherT action
590− respond $ case e of
591− Right output -> do
592− let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
593− headers = getHeaders output
594− case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) (getResponse output) of
595− Nothing -> failWith UnsupportedMediaType
596− Just (contentT, body) -> succeedWith $
597− responseLBS status200 ( ("Content-Type" , cs contentT) : headers) body
598− Left err -> succeedWith $ responseServantErr err
599− | pathIsEmpty request && requestMethod request /= methodPut =
600− respond $ failWith WrongMethod
601− | otherwise = respond $ failWith NotFound
383+ route Proxy = methodRouterHeaders methodPut (Proxy :: Proxy ctypes) ok200
602384
603385-- | When implementing the handler for a 'Patch' endpoint,
604386-- just like for 'Servant.API.Delete.Delete', 'Servant.API.Get.Get'
605387-- and 'Servant.API.Put.Put', the handler code runs in the
606−-- @EitherT (Int, String) IO@ monad, where the 'Int' represents
388+-- @EitherT ServantErr IO@ monad, where the 'Int' represents
607389-- the status code and the 'String' a message, returned in case of
608390-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
609391-- to quickly fail if some conditions are not met.
@@ -619,20 +401,7 @@ instance
619401
620402 type ServerT (Patch ctypes a) m = m a
621403
622− route Proxy action request respond
623− | pathIsEmpty request && requestMethod request == methodPatch = do
624− e <- runEitherT action
625− respond $ case e of
626− Right output -> do
627− let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
628− case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
629− Nothing -> failWith UnsupportedMediaType
630− Just (contentT, body) -> succeedWith $
631− responseLBS status200 [ ("Content-Type" , cs contentT)] body
632− Left err -> succeedWith $ responseServantErr err
633− | pathIsEmpty request && requestMethod request /= methodPatch =
634− respond $ failWith WrongMethod
635− | otherwise = respond $ failWith NotFound
404+ route Proxy = methodRouter methodPatch (Proxy :: Proxy ctypes) ok200
636405
637406instance
638407#if MIN_VERSION_base(4,8,0)
@@ -642,15 +411,7 @@ instance
642411
643412 type ServerT (Patch ctypes ()) m = m ()
644413
645− route Proxy action request respond
646− | pathIsEmpty request && requestMethod request == methodPatch = do
647− e <- runEitherT action
648− respond . succeedWith $ case e of
649− Right () -> responseLBS noContent204 [] ""
650− Left err -> responseServantErr err
651− | pathIsEmpty request && requestMethod request /= methodPatch =
652− respond $ failWith WrongMethod
653− | otherwise = respond $ failWith NotFound
414+ route Proxy = methodRouterEmpty methodPatch
654415
655416-- Add response headers
656417instance
@@ -662,21 +423,7 @@ instance
662423
663424 type ServerT (Patch ctypes (Headers h v)) m = m (Headers h v)
664425
665− route Proxy action request respond
666− | pathIsEmpty request && requestMethod request == methodPatch = do
667− e <- runEitherT action
668− respond $ case e of
669− Right outpatch -> do
670− let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
671− headers = getHeaders outpatch
672− case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) (getResponse outpatch) of
673− Nothing -> failWith UnsupportedMediaType
674− Just (contentT, body) -> succeedWith $
675− responseLBS status200 ( ("Content-Type" , cs contentT) : headers) body
676− Left err -> succeedWith $ responseServantErr err
677− | pathIsEmpty request && requestMethod request /= methodPatch =
678− respond $ failWith WrongMethod
679− | otherwise = respond $ failWith NotFound
426+ route Proxy = methodRouterHeaders methodPatch (Proxy :: Proxy ctypes) ok200
680427
681428-- | If you use @'QueryParam' "author" Text@ in one of the endpoints for your API,
682429-- this automatically requires your server-side handler to be a function
@@ -696,7 +443,7 @@ instance
696443-- >
697444-- > server :: Server MyApi
698445-- > server = getBooksBy
699−-- > where getBooksBy :: Maybe Text -> EitherT (Int, String) IO [Book]
446+-- > where getBooksBy :: Maybe Text -> EitherT ServantErr IO [Book]
700447-- > getBooksBy Nothing = ...return all books...
701448-- > getBooksBy (Just author) = ...return books by the given author...
702449instance (KnownSymbol sym, FromText a, HasServer sublayout)
@@ -705,7 +452,7 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
705452 type ServerT (QueryParam sym a :> sublayout) m =
706453 Maybe a -> ServerT sublayout m
707454
708− route Proxy subserver request respond = do
455+ route Proxy subserver = WithRequest $ \ request ->
709456 let querytext = parseQueryText $ rawQueryString request
710457 param =
711458 case lookup paramname querytext of
@@ -713,9 +460,7 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
713460 Just Nothing -> Nothing -- param present with no value -> Nothing
714461 Just (Just v) -> fromText v -- if present, we try to convert to
715462 -- the right type
716−
717− route (Proxy :: Proxy sublayout) (subserver param) request respond
718−
463+ in route (Proxy :: Proxy sublayout) (feedTo subserver param)
719464 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
720465
721466-- | If you use @'QueryParams' "authors" Text@ in one of the endpoints for your API,
@@ -735,7 +480,7 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
735480-- >
736481-- > server :: Server MyApi
737482-- > server = getBooksBy
738−-- > where getBooksBy :: [Text] -> EitherT (Int, String) IO [Book]
483+-- > where getBooksBy :: [Text] -> EitherT ServantErr IO [Book]
739484-- > getBooksBy authors = ...return all books by these authors...
740485instance (KnownSymbol sym, FromText a, HasServer sublayout)
741486 => HasServer (QueryParams sym a :> sublayout) where
@@ -743,16 +488,14 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
743488 type ServerT (QueryParams sym a :> sublayout) m =
744489 [a] -> ServerT sublayout m
745490
746− route Proxy subserver request respond = do
491+ route Proxy subserver = WithRequest $ \ request ->
747492 let querytext = parseQueryText $ rawQueryString request
748493 -- if sym is "foo", we look for query string parameters
749494 -- named "foo" or "foo[]" and call fromText on the
750495 -- corresponding values
751496 parameters = filter looksLikeParam querytext
752497 values = catMaybes $ map (convert . snd) parameters
753−
754− route (Proxy :: Proxy sublayout) (subserver values) request respond
755−
498+ in route (Proxy :: Proxy sublayout) (feedTo subserver values)
756499 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
757500 looksLikeParam (name, _) = name == paramname || name == (paramname <> "[]")
758501 convert Nothing = Nothing
@@ -768,7 +511,7 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
768511-- >
769512-- > server :: Server MyApi
770513-- > server = getBooks
771−-- > where getBooks :: Bool -> EitherT (Int, String) IO [Book]
514+-- > where getBooks :: Bool -> EitherT ServantErr IO [Book]
772515-- > getBooks onlyPublished = ...return all books, or only the ones that are already published, depending on the argument...
773516instance (KnownSymbol sym, HasServer sublayout)
774517 => HasServer (QueryFlag sym :> sublayout) where
@@ -776,15 +519,13 @@ instance (KnownSymbol sym, HasServer sublayout)
776519 type ServerT (QueryFlag sym :> sublayout) m =
777520 Bool -> ServerT sublayout m
778521
779− route Proxy subserver request respond = do
522+ route Proxy subserver = WithRequest $ \ request ->
780523 let querytext = parseQueryText $ rawQueryString request
781524 param = case lookup paramname querytext of
782525 Just Nothing -> True -- param is there, with no value
783526 Just (Just v) -> examine v -- param with a value
784527 Nothing -> False -- param not in the query string
785−
786− route (Proxy :: Proxy sublayout) (subserver param) request respond
787−
528+ in route (Proxy :: Proxy sublayout) (feedTo subserver param)
788529 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
789530 examine v | v == "true" || v == "1" || v == "" = True
790531 | otherwise = False
@@ -810,7 +551,7 @@ parseMatrixText = parseQueryText
810551-- >
811552-- > server :: Server MyApi
812553-- > server = getBooksBy
813−-- > where getBooksBy :: Maybe Text -> EitherT (Int, String) IO [Book]
554+-- > where getBooksBy :: Maybe Text -> EitherT ServantErr IO [Book]
814555-- > getBooksBy Nothing = ...return all books...
815556-- > getBooksBy (Just author) = ...return books by the given author...
816557instance (KnownSymbol sym, FromText a, HasServer sublayout)
@@ -819,16 +560,17 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
819560 type ServerT (MatrixParam sym a :> sublayout) m =
820561 Maybe a -> ServerT sublayout m
821562
822− route Proxy subserver request respond = case parsePathInfo request of
823− (first : _)
824− -> do let querytext = parseMatrixText . encodeUtf8 $ T.tail first
825− param = case lookup paramname querytext of
826− Nothing -> Nothing -- param absent from the query string
827− Just Nothing -> Nothing -- param present with no value -> Nothing
828− Just (Just v) -> fromText v -- if present, we try to convert to
829− -- the right type
830− route (Proxy :: Proxy sublayout) (subserver param) request respond
831− _ -> route (Proxy :: Proxy sublayout) (subserver Nothing) request respond
563+ route Proxy subserver = WithRequest $ \ request ->
564+ case parsePathInfo request of
565+ (first : _)
566+ -> do let querytext = parseMatrixText . encodeUtf8 $ T.tail first
567+ param = case lookup paramname querytext of
568+ Nothing -> Nothing -- param absent from the query string
569+ Just Nothing -> Nothing -- param present with no value -> Nothing
570+ Just (Just v) -> fromText v -- if present, we try to convert to
571+ -- the right type
572+ route (Proxy :: Proxy sublayout) (feedTo subserver param)
573+ _ -> route (Proxy :: Proxy sublayout) (feedTo subserver Nothing)
832574
833575 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
834576
@@ -849,7 +591,7 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
849591-- >
850592-- > server :: Server MyApi
851593-- > server = getBooksBy
852−-- > where getBooksBy :: [Text] -> EitherT (Int, String) IO [Book]
594+-- > where getBooksBy :: [Text] -> EitherT ServantErr IO [Book]
853595-- > getBooksBy authors = ...return all books by these authors...
854596instance (KnownSymbol sym, FromText a, HasServer sublayout)
855597 => HasServer (MatrixParams sym a :> sublayout) where
@@ -857,16 +599,17 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
857599 type ServerT (MatrixParams sym a :> sublayout) m =
858600 [a] -> ServerT sublayout m
859601
860− route Proxy subserver request respond = case parsePathInfo request of
861− (first : _)
862− -> do let matrixtext = parseMatrixText . encodeUtf8 $ T.tail first
863− -- if sym is "foo", we look for matrix parameters
864− -- named "foo" or "foo[]" and call fromText on the
865− -- corresponding values
866− parameters = filter looksLikeParam matrixtext
867− values = catMaybes $ map (convert . snd) parameters
868− route (Proxy :: Proxy sublayout) (subserver values) request respond
869− _ -> route (Proxy :: Proxy sublayout) (subserver []) request respond
602+ route Proxy subserver = WithRequest $ \ request ->
603+ case parsePathInfo request of
604+ (first : _)
605+ -> do let matrixtext = parseMatrixText . encodeUtf8 $ T.tail first
606+ -- if sym is "foo", we look for matrix parameters
607+ -- named "foo" or "foo[]" and call fromText on the
608+ -- corresponding values
609+ parameters = filter looksLikeParam matrixtext
610+ values = catMaybes $ map (convert . snd) parameters
611+ route (Proxy :: Proxy sublayout) (feedTo subserver values)
612+ _ -> route (Proxy :: Proxy sublayout) (feedTo subserver [])
870613
871614 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
872615 looksLikeParam (name, _) = name == paramname || name == (paramname <> "[]")
@@ -883,7 +626,7 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
883626-- >
884627-- > server :: Server MyApi
885628-- > server = getBooks
886−-- > where getBooks :: Bool -> EitherT (Int, String) IO [Book]
629+-- > where getBooks :: Bool -> EitherT ServantErr IO [Book]
887630-- > getBooks onlyPublished = ...return all books, or only the ones that are already published, depending on the argument...
888631instance (KnownSymbol sym, HasServer sublayout)
889632 => HasServer (MatrixFlag sym :> sublayout) where
@@ -891,17 +634,18 @@ instance (KnownSymbol sym, HasServer sublayout)
891634 type ServerT (MatrixFlag sym :> sublayout) m =
892635 Bool -> ServerT sublayout m
893636
894− route Proxy subserver request respond = case parsePathInfo request of
895− (first : _)
896− -> do let matrixtext = parseMatrixText . encodeUtf8 $ T.tail first
897− param = case lookup paramname matrixtext of
898− Just Nothing -> True -- param is there, with no value
899− Just (Just v) -> examine v -- param with a value
900− Nothing -> False -- param not in the query string
901−
902− route (Proxy :: Proxy sublayout) (subserver param) request respond
903−
904− _ -> route (Proxy :: Proxy sublayout) (subserver False) request respond
637+ route Proxy subserver = WithRequest $ \ request ->
638+ case parsePathInfo request of
639+ (first : _)
640+ -> do let matrixtext = parseMatrixText . encodeUtf8 $ T.tail first
641+ param = case lookup paramname matrixtext of
642+ Just Nothing -> True -- param is there, with no value
643+ Just (Just v) -> examine v -- param with a value
644+ Nothing -> False -- param not in the query string
645+
646+ route (Proxy :: Proxy sublayout) (feedTo subserver param)
647+
648+ _ -> route (Proxy :: Proxy sublayout) (feedTo subserver False)
905649
906650 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
907651 examine v | v == "true" || v == "1" || v == "" = True
@@ -919,8 +663,11 @@ instance HasServer Raw where
919663
920664 type ServerT Raw m = Application
921665
922− route Proxy rawApplication request respond =
923− rawApplication request (respond . succeedWith)
666+ route Proxy rawApplication = LeafRouter $ \ request respond -> do
667+ r <- rawApplication
668+ case r of
669+ RR (Left err) -> respond $ failWith err
670+ RR (Right app) -> app request (respond . succeedWith)
924671
925672-- | If you use 'ReqBody' in one of the endpoints for your API,
926673-- this automatically requires your server-side handler to be a function
@@ -940,7 +687,7 @@ instance HasServer Raw where
940687-- >
941688-- > server :: Server MyApi
942689-- > server = postBook
943−-- > where postBook :: Book -> EitherT (Int, String) IO Book
690+-- > where postBook :: Book -> EitherT ServantErr IO Book
944691-- > postBook book = ...insert into your db...
945692instance ( AllCTUnrender list a, HasServer sublayout
946693 ) => HasServer (ReqBody list a :> sublayout) where
@@ -948,19 +695,20 @@ instance ( AllCTUnrender list a, HasServer sublayout
948695 type ServerT (ReqBody list a :> sublayout) m =
949696 a -> ServerT sublayout m
950697
951− route Proxy subserver request respond = do
952− -- See HTTP RFC 2616, section 7.2.1
953− -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
954− -- See also "W3C Internet Media Type registration, consistency of use"
955− -- http://www.w3.org/2001/tag/2002/0129-mime
956− let contentTypeH = fromMaybe "application/octet-stream"
957− $ lookup hContentType $ requestHeaders request
958− mrqbody <- handleCTypeH (Proxy :: Proxy list) (cs contentTypeH)
959− <$> lazyRequestBody request
960− case mrqbody of
961− Nothing -> respond . failWith $ UnsupportedMediaType
962− Just (Left e) -> respond . failWith $ InvalidBody e
963− Just (Right v) -> route (Proxy :: Proxy sublayout) (subserver v) request respond
698+ route Proxy subserver = WithRequest $ \ request ->
699+ route (Proxy :: Proxy sublayout) $ do
700+ -- See HTTP RFC 2616, section 7.2.1
701+ -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
702+ -- See also "W3C Internet Media Type registration, consistency of use"
703+ -- http://www.w3.org/2001/tag/2002/0129-mime
704+ let contentTypeH = fromMaybe "application/octet-stream"
705+ $ lookup hContentType $ requestHeaders request
706+ mrqbody <- handleCTypeH (Proxy :: Proxy list) (cs contentTypeH)
707+ <$> lazyRequestBody request
708+ case mrqbody of
709+ Nothing -> return $ failWith $ UnsupportedMediaType
710+ Just (Left e) -> return $ failWith $ InvalidBody e
711+ Just (Right v) -> feedTo subserver v
964712
965713-- | Make sure the incoming request starts with @"/path"@, strip it and
966714-- pass the rest of the request path to @sublayout@.
@@ -968,14 +716,9 @@ instance (KnownSymbol path, HasServer sublayout) => HasServer (path :> sublayout
968716
969717 type ServerT (path :> sublayout) m = ServerT sublayout m
970718
971− route Proxy subserver request respond = case processedPathInfo request of
972− (first : rest)
973− | first == cs (symbolVal proxyPath)
974− -> route (Proxy :: Proxy sublayout) subserver request{
975− pathInfo = rest
976− } respond
977− _ -> respond $ failWith NotFound
978−
719+ route Proxy subserver = StaticRouter $
720+ M.singleton (cs (symbolVal proxyPath))
721+ (route (Proxy :: Proxy sublayout) subserver)
979722 where proxyPath = Proxy :: Proxy path
980723
981724ct_wildcard :: B.ByteString
servant-server/src/Servant/Server/Internal/PathInfo.hsnew file+38
@@ -0,0 +1,38 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Servant.Server.Internal.PathInfo where
3+
4+import Data.List (unfoldr)
5+import Data.Text (Text)
6+import qualified Data.Text as T
7+import Network.Wai (Request, pathInfo)
8+
9+-- | Like `null . pathInfo`, but works with redundant trailing slashes.
10+pathIsEmpty :: Request -> Bool
11+pathIsEmpty = f . processedPathInfo
12+ where
13+ f [] = True
14+ f [""] = True
15+ f _ = False
16+
17+
18+splitMatrixParameters :: Text -> (Text, Text)
19+splitMatrixParameters = T.break (== ';')
20+
21+parsePathInfo :: Request -> [Text]
22+parsePathInfo = filter (/= "") . mergePairs . map splitMatrixParameters . pathInfo
23+ where mergePairs = concat . unfoldr pairToList
24+ pairToList [] = Nothing
25+ pairToList ((a, b):xs) = Just ([a, b], xs)
26+
27+-- | Returns a processed pathInfo from the request.
28+--
29+-- In order to handle matrix parameters in the request correctly, the raw pathInfo needs to be
30+-- processed, so routing works as intended. Therefor this function should be used to access
31+-- the pathInfo for routing purposes.
32+processedPathInfo :: Request -> [Text]
33+processedPathInfo r =
34+ case pinfo of
35+ (x:xs) | T.head x == ';' -> xs
36+ _ -> pinfo
37+ where pinfo = parsePathInfo r
38+
servant-server/src/Servant/Server/Internal/Router.hsnew file+72
@@ -0,0 +1,72 @@
1+module Servant.Server.Internal.Router where
2+
3+import Data.Map (Map)
4+import qualified Data.Map as M
5+import Data.Monoid ((<>))
6+import Data.Text (Text)
7+import Network.Wai (Request, pathInfo)
8+import Servant.Server.Internal.PathInfo
9+import Servant.Server.Internal.RoutingApplication
10+
11+-- | Internal representation of a router.
12+data Router =
13+ WithRequest (Request -> Router)
14+ -- ^ current request is passed to the router
15+ | StaticRouter (Map Text Router)
16+ -- ^ first path component used for lookup and removed afterwards
17+ | DynamicRouter (Text -> Router)
18+ -- ^ first path component used for lookup and removed afterwards
19+ | LeafRouter RoutingApplication
20+ -- ^ to be used for routes that match an empty path
21+ | Choice Router Router
22+ -- ^ left-biased choice between two routers
23+
24+-- | Smart constructor for the choice between routers.
25+-- We currently optimize the following cases:
26+--
27+-- * Two static routers can be joined by joining their maps.
28+-- * Two dynamic routers can be joined by joining their codomains.
29+-- * Two 'WithRequest' routers can be joined by passing them
30+-- the same request and joining their codomains.
31+-- * A 'WithRequest' router can be joined with anything else by
32+-- passing the same request to both but ignoring it in the
33+-- component that does not need it.
34+--
35+choice :: Router -> Router -> Router
36+choice (StaticRouter table1) (StaticRouter table2) =
37+ StaticRouter (M.unionWith choice table1 table2)
38+choice (DynamicRouter fun1) (DynamicRouter fun2) =
39+ DynamicRouter (\ first -> choice (fun1 first) (fun2 first))
40+choice (WithRequest router1) (WithRequest router2) =
41+ WithRequest (\ request -> choice (router1 request) (router2 request))
42+choice (WithRequest router1) router2 =
43+ WithRequest (\ request -> choice (router1 request) router2)
44+choice router1 (WithRequest router2) =
45+ WithRequest (\ request -> choice router1 (router2 request))
46+choice router1 router2 = Choice router1 router2
47+
48+-- | Interpret a router as an application.
49+runRouter :: Router -> RoutingApplication
50+runRouter (WithRequest router) request respond =
51+ runRouter (router request) request respond
52+runRouter (StaticRouter table) request respond =
53+ case processedPathInfo request of
54+ first : rest
55+ | Just router <- M.lookup first table
56+ -> let request' = request { pathInfo = rest }
57+ in runRouter router request' respond
58+ _ -> respond $ failWith NotFound
59+runRouter (DynamicRouter fun) request respond =
60+ case processedPathInfo request of
61+ first : rest
62+ -> let request' = request { pathInfo = rest }
63+ in runRouter (fun first) request' respond
64+ _ -> respond $ failWith NotFound
65+runRouter (LeafRouter app) request respond = app request respond
66+runRouter (Choice r1 r2) request respond =
67+ runRouter r1 request $ \ mResponse1 ->
68+ if isMismatch mResponse1
69+ then runRouter r2 request $ \ mResponse2 ->
70+ respond (mResponse1 <> mResponse2)
71+ else respond mResponse1
72+
servant-server/src/Servant/Server/Internal/RoutingApplication.hsnew file+144
@@ -0,0 +1,144 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE TypeOperators #-}
5+module Servant.Server.Internal.RoutingApplication where
6+
7+#if !MIN_VERSION_base(4,8,0)
8+import Control.Applicative (Applicative, (<$>))
9+import Data.Monoid (Monoid, mappend, mempty)
10+#endif
11+import Control.Monad.Trans.Either (EitherT, runEitherT)
12+import qualified Data.ByteString as B
13+import qualified Data.ByteString.Lazy as BL
14+import Data.IORef (newIORef, readIORef, writeIORef)
15+import Data.Maybe (fromMaybe)
16+import Data.Monoid ((<>))
17+import Data.String (fromString)
18+import Network.HTTP.Types hiding (Header, ResponseHeaders)
19+import Network.Wai (Application, Request, Response,
20+ ResponseReceived,
21+ requestBody,
22+ responseLBS,
23+ strictRequestBody)
24+import Servant.API ((:<|>) (..))
25+import Servant.Server.Internal.ServantErr
26+
27+type RoutingApplication =
28+ Request -- ^ the request, the field 'pathInfo' may be modified by url routing
29+ -> (RouteResult Response -> IO ResponseReceived) -> IO ResponseReceived
30+
31+-- | A wrapper around @'Either' 'RouteMismatch' a@.
32+newtype RouteResult a =
33+ RR { routeResult :: Either RouteMismatch a }
34+ deriving (Eq, Show, Functor, Applicative)
35+
36+-- | If we get a `Right`, it has precedence over everything else.
37+--
38+-- This in particular means that if we could get several 'Right's,
39+-- only the first we encounter would be taken into account.
40+instance Monoid (RouteResult a) where
41+ mempty = RR $ Left mempty
42+
43+ RR (Left x) `mappend` RR (Left y) = RR $ Left (x <> y)
44+ RR (Left _) `mappend` RR (Right y) = RR $ Right y
45+ r `mappend` _ = r
46+
47+-- Note that the ordering of the constructors has great significance! It
48+-- determines the Ord instance and, consequently, the monoid instance.
49+data RouteMismatch =
50+ NotFound -- ^ the usual "not found" error
51+ | WrongMethod -- ^ a more informative "you just got the HTTP method wrong" error
52+ | UnsupportedMediaType -- ^ request body has unsupported media type
53+ | InvalidBody String -- ^ an even more informative "your json request body wasn't valid" error
54+ | HttpError Status (Maybe BL.ByteString) -- ^ an even even more informative arbitrary HTTP response code error.
55+ deriving (Eq, Ord, Show)
56+
57+instance Monoid RouteMismatch where
58+ mempty = NotFound
59+ -- The following isn't great, since it picks @InvalidBody@ based on
60+ -- alphabetical ordering, but any choice would be arbitrary.
61+ --
62+ -- "As one judge said to the other, 'Be just and if you can't be just, be
63+ -- arbitrary'" -- William Burroughs
64+ mappend = max
65+
66+data ReqBodyState = Uncalled
67+ | Called !B.ByteString
68+ | Done !B.ByteString
69+
70+toApplication :: RoutingApplication -> Application
71+toApplication ra request respond = do
72+ reqBodyRef <- newIORef Uncalled
73+ -- We may need to consume the requestBody more than once. In order to
74+ -- maintain the illusion that 'requestBody' works as expected,
75+ -- 'ReqBodyState' is introduced, and the complete body is memoized and
76+ -- returned as many times as requested with empty "Done" marker chunks in
77+ -- between.
78+ -- See https://github.com/haskell-servant/servant/issues/3
79+ let memoReqBody = do
80+ ior <- readIORef reqBodyRef
81+ case ior of
82+ Uncalled -> do
83+ r <- BL.toStrict <$> strictRequestBody request
84+ writeIORef reqBodyRef $ Done r
85+ return r
86+ Called bs -> do
87+ writeIORef reqBodyRef $ Done bs
88+ return bs
89+ Done bs -> do
90+ writeIORef reqBodyRef $ Called bs
91+ return B.empty
92+
93+ ra request{ requestBody = memoReqBody } (routingRespond . routeResult)
94+ where
95+ routingRespond :: Either RouteMismatch Response -> IO ResponseReceived
96+ routingRespond (Left NotFound) =
97+ respond $ responseLBS notFound404 [] "not found"
98+ routingRespond (Left WrongMethod) =
99+ respond $ responseLBS methodNotAllowed405 [] "method not allowed"
100+ routingRespond (Left (InvalidBody err)) =
101+ respond $ responseLBS badRequest400 [] $ fromString $ "invalid request body: " ++ err
102+ routingRespond (Left UnsupportedMediaType) =
103+ respond $ responseLBS unsupportedMediaType415 [] "unsupported media type"
104+ routingRespond (Left (HttpError status body)) =
105+ respond $ responseLBS status [] $ fromMaybe (BL.fromStrict $ statusMessage status) body
106+ routingRespond (Right response) =
107+ respond response
108+
109+runAction :: IO (RouteResult (EitherT ServantErr IO a))
110+ -> (RouteResult Response -> IO r)
111+ -> (a -> RouteResult Response)
112+ -> IO r
113+runAction action respond k = do
114+ r <- action
115+ go r
116+ where
117+ go (RR (Right a)) = do
118+ e <- runEitherT a
119+ respond $ case e of
120+ Right x -> k x
121+ Left err -> succeedWith $ responseServantErr err
122+ go (RR (Left err)) = respond $ failWith err
123+
124+feedTo :: IO (RouteResult (a -> b)) -> a -> IO (RouteResult b)
125+feedTo f x = (($ x) <$>) <$> f
126+
127+extractL :: RouteResult (a :<|> b) -> RouteResult a
128+extractL (RR (Right (a :<|> _))) = RR (Right a)
129+extractL (RR (Left err)) = RR (Left err)
130+
131+extractR :: RouteResult (a :<|> b) -> RouteResult b
132+extractR (RR (Right (_ :<|> b))) = RR (Right b)
133+extractR (RR (Left err)) = RR (Left err)
134+
135+failWith :: RouteMismatch -> RouteResult a
136+failWith = RR . Left
137+
138+succeedWith :: a -> RouteResult a
139+succeedWith = RR . Right
140+
141+isMismatch :: RouteResult a -> Bool
142+isMismatch (RR (Left _)) = True
143+isMismatch _ = False
144+
servant-server/test/Servant/ServerSpec.hs+51−1
@@ -42,7 +42,8 @@ import Servant.API ((:<|>) (..), (:>),
4242 Post, Put, QueryFlag, QueryParam,
4343 QueryParams, Raw, ReqBody)
4444import Servant.Server (Server, serve, ServantErr(..), err404)
45−import Servant.Server.Internal (RouteMismatch (..))
45+import Servant.Server.Internal.RoutingApplication
46+ (RouteMismatch (..))
4647
4748
4849-- * test data types
@@ -89,6 +90,7 @@ spec = do
8990 headerSpec
9091 rawSpec
9192 unionSpec
93+ prioErrorsSpec
9294 errorsSpec
9395 responseHeadersSpec
9496
@@ -572,6 +574,54 @@ responseHeadersSpec = describe "ResponseHeaders" $ do
572574 Test.Hspec.Wai.request method "" [(hAccept, "crazy/mime")] ""
573575 `shouldRespondWith` 415
574576
577+type PrioErrorsApi = ReqBody '[JSON] Person :> "foo" :> Get '[JSON] Integer
578+
579+prioErrorsApi :: Proxy PrioErrorsApi
580+prioErrorsApi = Proxy
581+
582+-- | Test the relative priority of error responses from the server.
583+--
584+-- In particular, we check whether matching continues even if a 'ReqBody'
585+-- or similar construct is encountered early in a path. We don't want to
586+-- see a complaint about the request body unless the path actually matches.
587+--
588+prioErrorsSpec :: Spec
589+prioErrorsSpec = describe "PrioErrors" $ do
590+ let server = return . age
591+ with (return $ serve prioErrorsApi server) $ do
592+ let check (mdescr, method) path (cdescr, ctype, body) resp =
593+ it fulldescr $
594+ Test.Hspec.Wai.request method path [(hContentType, ctype)] body
595+ `shouldRespondWith` resp
596+ where
597+ fulldescr = "returns " ++ show (matchStatus resp) ++ " on " ++ mdescr
598+ ++ " " ++ cs path ++ " (" ++ cdescr ++ ")"
599+
600+ get' = ("GET", methodGet)
601+ put' = ("PUT", methodPut)
602+
603+ txt = ("text" , "text/plain;charset=utf8" , "42" )
604+ ijson = ("invalid json", "application/json;charset=utf8", "invalid" )
605+ vjson = ("valid json" , "application/json;charset=utf8", encode alice)
606+
607+ check get' "/" txt 404
608+ check get' "/bar" txt 404
609+ check get' "/foo" txt 415
610+ check put' "/" txt 404
611+ check put' "/bar" txt 404
612+ check put' "/foo" txt 405
613+ check get' "/" ijson 404
614+ check get' "/bar" ijson 404
615+ check get' "/foo" ijson 400
616+ check put' "/" ijson 404
617+ check put' "/bar" ijson 404
618+ check put' "/foo" ijson 405
619+ check get' "/" vjson 404
620+ check get' "/bar" vjson 404
621+ check get' "/foo" vjson 200
622+ check put' "/" vjson 404
623+ check put' "/bar" vjson 404
624+ check put' "/foo" vjson 405
575625
576626-- | Test server error functionality.
577627errorsSpec :: Spec
servant/CHANGELOG.md+9
@@ -1,3 +1,12 @@
1+0.4.2
2+-----
3+* Fix missing cases for `Patch` in `safeLink`
4+
5+0.4.1
6+-----
7+* Allow whitespace after parsing JSON
8+* Stricter matching for `safeLink` for `Capture`
9+
1100.4
211---
312* `Delete` now is like `Get`, `Post`, `Put`, and `Patch` and returns a response body
servant/servant.cabal+2−2
@@ -1,5 +1,5 @@
11name: servant
2−version: 0.4.0
2+version: 0.4.1
33synopsis: A family of combinators for defining webservices APIs
44description:
55 A family of combinators for defining webservices APIs and serving them
@@ -55,7 +55,7 @@ library
5555 , http-media >= 0.4 && < 0.7
5656 , http-types == 0.8.*
5757 , text >= 1 && < 2
58− , string-conversions >= 0.3 && < 0.4
58+ , string-conversions >= 0.3 && < 0.5
5959 , network-uri >= 2.6
6060 hs-source-dirs: src
6161 default-language: Haskell2010
servant/src/Servant/API/Capture.hs+1
@@ -9,6 +9,7 @@ import GHC.TypeLits (Symbol)
99-- | Capture a value from the request path under a certain type @a@.
1010--
1111-- Example:
12+--
1213-- >>> -- GET /books/:isbn
1314-- >>> type MyApi = "books" :> Capture "isbn" Text :> Get '[JSON] Book
1415data Capture (sym :: Symbol) a
servant/src/Servant/API/ContentTypes.hs+1−1
@@ -166,7 +166,7 @@ instance ( AllMimeRender ctyps a, IsNonEmpty ctyps
166166 handleAcceptH _ (AcceptHeader accept) val = M.mapAcceptMedia lkup accept
167167 where pctyps = Proxy :: Proxy ctyps
168168 amrs = allMimeRender pctyps val
169− lkup = fmap (\(a,b) -> (a, (cs $ show a, b))) amrs
169+ lkup = fmap (\(a,b) -> (a, (fromStrict $ M.renderHeader a, b))) amrs
170170
171171
172172--------------------------------------------------------------------------
servant/src/Servant/Utils/Links.hs+9−3
@@ -72,8 +72,7 @@
7272--
7373-- >>> let bad_link = Proxy :: Proxy ("hello" :> Delete '[JSON] ())
7474-- >>> safeLink api bad_link
75−-- <BLANKLINE>
76−-- <interactive>:64:1:
75+-- ...
7776-- Could not deduce (Or
7877-- (IsElem' (Delete '[JSON] ()) (Get '[JSON] Int))
7978-- (IsElem'
@@ -123,6 +122,7 @@ import Servant.API.Header ( Header )
123122import Servant.API.Get ( Get )
124123import Servant.API.Post ( Post )
125124import Servant.API.Put ( Put )
125+import Servant.API.Patch ( Patch )
126126import Servant.API.Delete ( Delete )
127127import Servant.API.Sub ( type (:>) )
128128import Servant.API.Raw ( Raw )
@@ -168,7 +168,8 @@ type family IsElem endpoint api :: Constraint where
168168 IsElem (e :> sa) (e :> sb) = IsElem sa sb
169169 IsElem sa (Header x :> sb) = IsElem sa sb
170170 IsElem sa (ReqBody y x :> sb) = IsElem sa sb
171− IsElem (e :> sa) (Capture x y :> sb) = IsElem sa sb
171+ IsElem (Capture z y :> sa) (Capture x y :> sb)
172+ = IsElem sa sb
172173 IsElem sa (QueryParam x y :> sb) = IsElem sa sb
173174 IsElem sa (QueryParams x y :> sb) = IsElem sa sb
174175 IsElem sa (QueryFlag x :> sb) = IsElem sa sb
@@ -178,6 +179,7 @@ type family IsElem endpoint api :: Constraint where
178179 IsElem (Get ct typ) (Get ct' typ) = IsSubList ct ct'
179180 IsElem (Post ct typ) (Post ct' typ) = IsSubList ct ct'
180181 IsElem (Put ct typ) (Put ct' typ) = IsSubList ct ct'
182+ IsElem (Patch ct typ) (Patch ct' typ) = IsSubList ct ct'
181183 IsElem (Delete ct typ) (Delete ct' typ) = IsSubList ct ct'
182184 IsElem e e = ()
183185 IsElem e a = IsElem' e a
@@ -352,6 +354,10 @@ instance HasLink (Put y r) where
352354 type MkLink (Put y r) = URI
353355 toLink _ = linkURI
354356
357+instance HasLink (Patch y r) where
358+ type MkLink (Patch y r) = URI
359+ toLink _ = linkURI
360+
355361instance HasLink (Delete y r) where
356362 type MkLink (Delete y r) = URI
357363 toLink _ = linkURI