Skip to content

Commit f548824

Browse files
committed
Add QueryParamform to Server
1 parent 9a2cddc commit f548824

File tree

3 files changed

+74
-3
lines changed

3 files changed

+74
-3
lines changed

Diff for: servant-server/servant-server.cabal

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ test-suite spec
127127
, exceptions
128128
, hspec == 2.*
129129
, hspec-wai >= 0.8 && <0.9
130+
, http-api-data
130131
, http-types
131132
, mtl
132133
, network >= 2.6

Diff for: servant-server/src/Servant/Server/Internal.hs

+44-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import Network.Wai (Application, Request, Response,
4646
responseLBS, vault)
4747
import Prelude ()
4848
import Prelude.Compat
49+
import Web.FormUrlEncoded (FromForm, urlDecodeAsForm)
4950
import Web.HttpApiData (FromHttpApiData, parseHeaderMaybe,
5051
parseQueryParam,
5152
parseUrlPieceMaybe,
@@ -54,7 +55,7 @@ import Servant.API ((:<|>) (..), (:>), BasicAuth, Capt
5455
CaptureAll, Verb,
5556
ReflectMethod(reflectMethod),
5657
IsSecure(..), Header, QueryFlag,
57-
QueryParam, QueryParams, Raw,
58+
QueryParam, QueryParams, QueryParamForm, Raw,
5859
RemoteHost, ReqBody, Vault,
5960
WithNamedContext)
6061
import Servant.API.ContentTypes (AcceptHeader (..),
@@ -405,6 +406,48 @@ instance (KnownSymbol sym, HasServer api context)
405406
examine v | v == "true" || v == "1" || v == "" = True
406407
| otherwise = False
407408

409+
-- | If you use @'QueryParamForm' BookSearchParams@ in one of the endpoints for your API,
410+
-- this automatically requires your server-side handler to be a function
411+
-- that takes an argument of type @['BookSearchParams']@.
412+
--
413+
-- This lets servant worry about all key-values in the query string
414+
-- and turning each of them into a value of the type you specify.
415+
--
416+
-- You can control how the individual values are converted from 'BookSearchParams'
417+
-- to your type by simply providing an instance of 'FromForm' for your type.
418+
--
419+
-- Example:
420+
--
421+
-- > data BookSearchParams = BookSearchParams
422+
-- > { title :: Text
423+
-- > { authors :: [Text]
424+
-- > , page :: Maybe Int
425+
-- > } deriving (Generic)
426+
-- > instance FromForm BookSearchParams
427+
-- >
428+
-- > type MyApi = "books" :> QueryParamForm BookSearchParams :> Get '[JSON] [Book]
429+
-- >
430+
-- > server :: Server MyApi
431+
-- > server = getBooksBy
432+
-- > where getBooksBy :: BookSearchParams -> Handler [Book]
433+
-- > getBooksBy searchParams = ...return all books by these conditions...
434+
435+
instance (FromForm a, HasServer api context)
436+
=> HasServer (QueryParamForm a :> api) context where
437+
438+
type ServerT (QueryParamForm a :> api) m =
439+
a -> ServerT api m
440+
441+
route Proxy context subserver = route (Proxy :: Proxy api) context $
442+
subserver `addParameterCheck` withRequest paramsCheck
443+
where
444+
paramsCheck req =
445+
case urlDecodeAsForm (BL.drop 1 . BL.fromStrict $ rawQueryString req) of
446+
Right form -> return form
447+
Left err -> delayedFailFatal err400
448+
{ errBody = cs $ "Error parsing query parameter(s) to form failed: " <> err
449+
}
450+
408451
-- | Just pass the request to the underlying application and serve its response.
409452
--
410453
-- Example:

Diff for: servant-server/test/Servant/ServerSpec.hs

+29-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import Servant.API ((:<|>) (..), (:>), AuthProtect,
4444
NoContent (..), Patch, PlainText,
4545
Post, Put,
4646
QueryFlag, QueryParam, QueryParams,
47-
Raw, RemoteHost, ReqBody,
47+
QueryParamForm, Raw, RemoteHost, ReqBody,
4848
StdMethod (..), Verb, addHeader)
4949
import Servant.API.Internal.Test.ComprehensiveAPI
5050
import Servant.Server (Server, Handler, err401, err403,
@@ -64,6 +64,7 @@ import Servant.Server.Experimental.Auth
6464
mkAuthHandler)
6565
import Servant.Server.Internal.Context
6666
(NamedContext(..))
67+
import Web.FormUrlEncoded (FromForm)
6768

6869
-- * comprehensive api test
6970

@@ -277,12 +278,13 @@ type QueryParamApi = QueryParam "name" String :> Get '[JSON] Person
277278
:<|> "b" :> QueryFlag "capitalize" :> Get '[JSON] Person
278279
:<|> "param" :> QueryParam "age" Integer :> Get '[JSON] Person
279280
:<|> "multiparam" :> QueryParams "ages" Integer :> Get '[JSON] Person
281+
:<|> "paramform" :> QueryParamForm Person :> Get '[JSON] Person
280282

281283
queryParamApi :: Proxy QueryParamApi
282284
queryParamApi = Proxy
283285

284286
qpServer :: Server QueryParamApi
285-
qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize :<|> qpAge :<|> qpAges
287+
qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize :<|> qpAge :<|> qpAges :<|> qpPerson
286288

287289
where qpNames (_:name2:_) = return alice { name = name2 }
288290
qpNames _ = return alice
@@ -295,6 +297,8 @@ qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize :<|> qpAge :<|> qpAge
295297

296298
qpAges ages = return alice{ age = sum ages}
297299

300+
qpPerson person = return person
301+
298302
queryParamServer (Just name_) = return alice{name = name_}
299303
queryParamServer Nothing = return alice
300304

@@ -410,6 +414,28 @@ queryParamSpec = do
410414
name = "Alice"
411415
}
412416

417+
it "parses query form" $
418+
(flip runSession) (serve queryParamApi qpServer) $ do
419+
let params = "?name=Alice&age=42"
420+
response <- Network.Wai.Test.request defaultRequest{
421+
rawQueryString = params,
422+
queryString = parseQuery params,
423+
pathInfo = ["paramform"]
424+
}
425+
liftIO $
426+
decode' (simpleBody response) `shouldBe` Just alice
427+
428+
it "generates an error on parse failures of query form" $
429+
(flip runSession) (serve queryParamApi qpServer) $ do
430+
let params = "?name=Alice"
431+
response <- Network.Wai.Test.request defaultRequest{
432+
rawQueryString = params,
433+
queryString = parseQuery params,
434+
pathInfo = ["paramform"]
435+
}
436+
liftIO $ statusCode (simpleStatus response) `shouldBe` 400
437+
return ()
438+
413439
-- }}}
414440
------------------------------------------------------------------------------
415441
-- * reqBodySpec {{{
@@ -732,6 +758,7 @@ data Person = Person {
732758

733759
instance ToJSON Person
734760
instance FromJSON Person
761+
instance FromForm Person
735762

736763
alice :: Person
737764
alice = Person "Alice" 42

0 commit comments

Comments
 (0)