Skip to content

Commit ec0cd8a

Browse files
authored
Merge pull request #1262 from haskell-servant/rawQueryString
Refactor of #1249
2 parents b4e5aa0 + 40582c4 commit ec0cd8a

File tree

3 files changed

+131
-109
lines changed

3 files changed

+131
-109
lines changed

changelog.d/pr1249

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
synopsis: use queryString to parse QueryParam, QueryParams and QueryFlag
2+
packages: servant-server
3+
prs: #1249
4+
description: {
5+
6+
Some APIs need query parameters rewriting, e.g. in order to support
7+
for multiple casing (camel, snake, etc) or something to that effect.
8+
9+
This could be easily achieved by using WAI Middleware and modyfing
10+
request's `Query`. But QueryParam, QueryParams and QueryFlag use
11+
`rawQueryString`. By using `queryString` rather then `rawQueryString`
12+
we can enable such rewritings.
13+
14+
}

servant-server/src/Servant/Server/Internal.hs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import Network.Socket
6464
(SockAddr)
6565
import Network.Wai
6666
(Application, Request, httpVersion, isSecure, lazyRequestBody,
67-
rawQueryString, remoteHost, requestBody, requestHeaders,
67+
queryString, remoteHost, requestBody, requestHeaders,
6868
requestMethod, responseLBS, responseStream, vault)
6969
import Prelude ()
7070
import Prelude.Compat
@@ -452,7 +452,7 @@ instance
452452
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
453453

454454
route Proxy context subserver =
455-
let querytext req = parseQueryText $ rawQueryString req
455+
let querytext = queryToQueryText . queryString
456456
paramname = cs $ symbolVal (Proxy :: Proxy sym)
457457

458458
parseParam :: Request -> DelayedIO (RequestArgument mods a)
@@ -519,8 +519,8 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer api context)
519519
params :: [T.Text]
520520
params = mapMaybe snd
521521
. filter (looksLikeParam . fst)
522-
. parseQueryText
523-
. rawQueryString
522+
. queryToQueryText
523+
. queryString
524524
$ req
525525

526526
looksLikeParam name = name == paramname || name == (paramname <> "[]")
@@ -546,7 +546,7 @@ instance (KnownSymbol sym, HasServer api context)
546546
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
547547

548548
route Proxy context subserver =
549-
let querytext r = parseQueryText $ rawQueryString r
549+
let querytext = queryToQueryText . queryString
550550
param r = case lookup paramname (querytext r) of
551551
Just Nothing -> True -- param is there, with no value
552552
Just (Just v) -> examine v -- param with a value

servant-server/test/Servant/ServerSpec.hs

Lines changed: 112 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import qualified Data.ByteString as BS
2525
import qualified Data.ByteString.Base64 as Base64
2626
import Data.Char
2727
(toUpper)
28+
import Data.Maybe
29+
(fromMaybe)
2830
import Data.Proxy
2931
(Proxy (Proxy))
3032
import Data.String
@@ -35,26 +37,26 @@ import qualified Data.Text as T
3537
import GHC.Generics
3638
(Generic)
3739
import Network.HTTP.Types
38-
(Status (..), hAccept, hContentType, imATeapot418,
40+
(QueryItem, Status (..), hAccept, hContentType, imATeapot418,
3941
methodDelete, methodGet, methodHead, methodPatch, methodPost,
4042
methodPut, ok200, parseQuery)
4143
import Network.Wai
42-
(Application, Request, pathInfo, queryString, rawQueryString,
43-
requestHeaders, responseLBS)
44+
(Application, Middleware, Request, pathInfo, queryString,
45+
rawQueryString, requestHeaders, responseLBS)
4446
import Network.Wai.Test
4547
(defaultRequest, request, runSession, simpleBody,
4648
simpleHeaders, simpleStatus)
4749
import Servant.API
4850
((:<|>) (..), (:>), AuthProtect, BasicAuth,
49-
BasicAuthData (BasicAuthData), Capture, Capture', CaptureAll, Lenient, Strict, Delete,
50-
EmptyAPI, Get, Header, Headers, HttpVersion, IsSecure (..),
51-
JSON, NoContent (..), NoFraming, OctetStream, Patch,
52-
PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw,
53-
RemoteHost, ReqBody, SourceIO, StdMethod (..), Stream, Verb,
54-
NoContentVerb, addHeader)
51+
BasicAuthData (BasicAuthData), Capture, Capture', CaptureAll,
52+
Delete, EmptyAPI, Get, Header, Headers, HttpVersion,
53+
IsSecure (..), JSON, Lenient, NoContent (..), NoContentVerb,
54+
NoFraming, OctetStream, Patch, PlainText, Post, Put,
55+
QueryFlag, QueryParam, QueryParams, Raw, RemoteHost, ReqBody,
56+
SourceIO, StdMethod (..), Stream, Strict, Verb, addHeader)
5557
import Servant.Server
5658
(Context ((:.), EmptyContext), Handler, Server, Tagged (..),
57-
emptyServer, err400, err401, err403, err404, serve, serveWithContext)
59+
emptyServer, err401, err403, err404, serve, serveWithContext)
5860
import Servant.Test.ComprehensiveAPI
5961
import qualified Servant.Types.SourceT as S
6062
import Test.Hspec
@@ -218,7 +220,7 @@ captureServer = getLegs :<|> getEars :<|> getEyes
218220
_ -> throwError err404
219221

220222
getEars :: Either String Integer -> Handler Animal
221-
getEars (Left e) = return chimera -- ignore integer parse error, return weird animal
223+
getEars (Left _) = return chimera -- ignore integer parse error, return weird animal
222224
getEars (Right 2) = return jerry
223225
getEars (Right _) = throwError err404
224226

@@ -339,117 +341,123 @@ qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize :<|> qpAge :<|> qpAge
339341
queryParamServer (Just name_) = return alice{name = name_}
340342
queryParamServer Nothing = return alice
341343

344+
345+
342346
queryParamSpec :: Spec
343347
queryParamSpec = do
348+
let mkRequest params pinfo = Network.Wai.Test.request defaultRequest
349+
{ rawQueryString = params
350+
, queryString = parseQuery params
351+
, pathInfo = pinfo
352+
}
353+
344354
describe "Servant.API.QueryParam" $ do
345355
it "allows retrieving simple GET parameters" $
346-
(flip runSession) (serve queryParamApi qpServer) $ do
347-
let params1 = "?name=bob"
348-
response1 <- Network.Wai.Test.request defaultRequest{
349-
rawQueryString = params1,
350-
queryString = parseQuery params1
351-
}
352-
liftIO $ do
353-
decode' (simpleBody response1) `shouldBe` Just alice{
354-
name = "bob"
355-
}
356+
flip runSession (serve queryParamApi qpServer) $ do
357+
response1 <- mkRequest "?name=bob" []
358+
liftIO $ decode' (simpleBody response1) `shouldBe` Just alice
359+
{ name = "bob"
360+
}
356361

357362
it "allows retrieving lists in GET parameters" $
358-
(flip runSession) (serve queryParamApi qpServer) $ do
359-
let params2 = "?names[]=bob&names[]=john"
360-
response2 <- Network.Wai.Test.request defaultRequest{
361-
rawQueryString = params2,
362-
queryString = parseQuery params2,
363-
pathInfo = ["a"]
364-
}
365-
liftIO $
366-
decode' (simpleBody response2) `shouldBe` Just alice{
367-
name = "john"
368-
}
363+
flip runSession (serve queryParamApi qpServer) $ do
364+
response2 <- mkRequest "?names[]=bob&names[]=john" ["a"]
365+
liftIO $ decode' (simpleBody response2) `shouldBe` Just alice
366+
{ name = "john"
367+
}
369368

370369
it "parses a query parameter" $
371-
(flip runSession) (serve queryParamApi qpServer) $ do
372-
let params = "?age=55"
373-
response <- Network.Wai.Test.request defaultRequest{
374-
rawQueryString = params,
375-
queryString = parseQuery params,
376-
pathInfo = ["param"]
377-
}
378-
liftIO $
379-
decode' (simpleBody response) `shouldBe` Just alice{
380-
age = 55
381-
}
370+
flip runSession (serve queryParamApi qpServer) $ do
371+
response <- mkRequest "?age=55" ["param"]
372+
liftIO $ decode' (simpleBody response) `shouldBe` Just alice
373+
{ age = 55
374+
}
382375

383376
it "generates an error on query parameter parse failure" $
384-
(flip runSession) (serve queryParamApi qpServer) $ do
385-
let params = "?age=foo"
386-
response <- Network.Wai.Test.request defaultRequest{
387-
rawQueryString = params,
388-
queryString = parseQuery params,
389-
pathInfo = ["param"]
390-
}
377+
flip runSession (serve queryParamApi qpServer) $ do
378+
response <- mkRequest "?age=foo" ["param"]
391379
liftIO $ statusCode (simpleStatus response) `shouldBe` 400
392380
return ()
393381

394382
it "parses multiple query parameters" $
395-
(flip runSession) (serve queryParamApi qpServer) $ do
396-
let params = "?ages=10&ages=22"
397-
response <- Network.Wai.Test.request defaultRequest{
398-
rawQueryString = params,
399-
queryString = parseQuery params,
400-
pathInfo = ["multiparam"]
401-
}
402-
liftIO $
403-
decode' (simpleBody response) `shouldBe` Just alice{
404-
age = 32
405-
}
383+
flip runSession (serve queryParamApi qpServer) $ do
384+
response <- mkRequest "?ages=10&ages=22" ["multiparam"]
385+
liftIO $ decode' (simpleBody response) `shouldBe` Just alice
386+
{ age = 32
387+
}
406388

407389
it "generates an error on parse failures of multiple parameters" $
408-
(flip runSession) (serve queryParamApi qpServer) $ do
409-
let params = "?ages=2&ages=foo"
410-
response <- Network.Wai.Test.request defaultRequest{
411-
rawQueryString = params,
412-
queryString = parseQuery params,
413-
pathInfo = ["multiparam"]
414-
}
390+
flip runSession (serve queryParamApi qpServer) $ do
391+
response <- mkRequest "?ages=2&ages=foo" ["multiparam"]
415392
liftIO $ statusCode (simpleStatus response) `shouldBe` 400
416393
return ()
417394

418-
419395
it "allows retrieving value-less GET parameters" $
420-
(flip runSession) (serve queryParamApi qpServer) $ do
421-
let params3 = "?capitalize"
422-
response3 <- Network.Wai.Test.request defaultRequest{
423-
rawQueryString = params3,
424-
queryString = parseQuery params3,
425-
pathInfo = ["b"]
426-
}
427-
liftIO $
428-
decode' (simpleBody response3) `shouldBe` Just alice{
429-
name = "ALICE"
430-
}
431-
432-
let params3' = "?capitalize="
433-
response3' <- Network.Wai.Test.request defaultRequest{
434-
rawQueryString = params3',
435-
queryString = parseQuery params3',
436-
pathInfo = ["b"]
437-
}
438-
liftIO $
439-
decode' (simpleBody response3') `shouldBe` Just alice{
440-
name = "ALICE"
441-
}
442-
443-
let params3'' = "?unknown="
444-
response3'' <- Network.Wai.Test.request defaultRequest{
445-
rawQueryString = params3'',
446-
queryString = parseQuery params3'',
447-
pathInfo = ["b"]
448-
}
449-
liftIO $
450-
decode' (simpleBody response3'') `shouldBe` Just alice{
451-
name = "Alice"
452-
}
396+
flip runSession (serve queryParamApi qpServer) $ do
397+
response3 <- mkRequest "?capitalize" ["b"]
398+
liftIO $ decode' (simpleBody response3) `shouldBe` Just alice
399+
{ name = "ALICE"
400+
}
401+
402+
response3' <- mkRequest "?capitalize=" ["b"]
403+
liftIO $ decode' (simpleBody response3') `shouldBe` Just alice
404+
{ name = "ALICE"
405+
}
406+
407+
response3'' <- mkRequest "?unknown=" ["b"]
408+
liftIO $ decode' (simpleBody response3'') `shouldBe` Just alice
409+
{ name = "Alice"
410+
}
411+
412+
describe "Uses queryString instead of rawQueryString" $ do
413+
-- test query parameters rewriter
414+
let queryRewriter :: Middleware
415+
queryRewriter app req = app req
416+
{ queryString = fmap rewrite $ queryString req
417+
}
418+
where
419+
rewrite :: QueryItem -> QueryItem
420+
rewrite (k, v) = (fromMaybe k (BS.stripPrefix "person_" k), v)
421+
422+
let app = queryRewriter $ serve queryParamApi qpServer
423+
424+
it "allows rewriting for simple GET/query parameters" $
425+
flip runSession app $ do
426+
response1 <- mkRequest "?person_name=bob" []
427+
liftIO $ decode' (simpleBody response1) `shouldBe` Just alice
428+
{ name = "bob"
429+
}
430+
431+
it "allows rewriting for lists in GET parameters" $
432+
flip runSession app $ do
433+
response2 <- mkRequest "?person_names[]=bob&person_names[]=john" ["a"]
434+
liftIO $ decode' (simpleBody response2) `shouldBe` Just alice
435+
{ name = "john"
436+
}
437+
438+
it "allows rewriting when parsing multiple query parameters" $
439+
flip runSession app $ do
440+
response <- mkRequest "?person_ages=10&person_ages=22" ["multiparam"]
441+
liftIO $ decode' (simpleBody response) `shouldBe` Just alice
442+
{ age = 32
443+
}
444+
445+
it "allows retrieving value-less GET parameters" $
446+
flip runSession app $ do
447+
response3 <- mkRequest "?person_capitalize" ["b"]
448+
liftIO $ decode' (simpleBody response3) `shouldBe` Just alice
449+
{ name = "ALICE"
450+
}
451+
452+
response3' <- mkRequest "?person_capitalize=" ["b"]
453+
liftIO $ decode' (simpleBody response3') `shouldBe` Just alice
454+
{ name = "ALICE"
455+
}
456+
457+
response3'' <- mkRequest "?person_unknown=" ["b"]
458+
liftIO $ decode' (simpleBody response3'') `shouldBe` Just alice
459+
{ name = "Alice"
460+
}
453461

454462
-- }}}
455463
------------------------------------------------------------------------------
@@ -544,15 +552,15 @@ rawSpec :: Spec
544552
rawSpec = do
545553
describe "Servant.API.Raw" $ do
546554
it "runs applications" $ do
547-
(flip runSession) (serve rawApi (rawApplication (const (42 :: Integer)))) $ do
555+
flip runSession (serve rawApi (rawApplication (const (42 :: Integer)))) $ do
548556
response <- Network.Wai.Test.request defaultRequest{
549557
pathInfo = ["foo"]
550558
}
551559
liftIO $ do
552560
simpleBody response `shouldBe` "42"
553561

554562
it "gets the pathInfo modified" $ do
555-
(flip runSession) (serve rawApi (rawApplication pathInfo)) $ do
563+
flip runSession (serve rawApi (rawApplication pathInfo)) $ do
556564
response <- Network.Wai.Test.request defaultRequest{
557565
pathInfo = ["foo", "bar"]
558566
}

0 commit comments

Comments
 (0)