Merge 1ae85d1ee9
into 6392dce4bf
This commit is contained in:
commit
c095f8b251
|
@ -23,6 +23,7 @@ module Servant.Client.Core.HasClient (
|
|||
(/:),
|
||||
foldMapUnion,
|
||||
matchUnion,
|
||||
ToDeepQuery (..)
|
||||
) where
|
||||
|
||||
import Prelude ()
|
||||
|
@ -44,6 +45,7 @@ import Data.List
|
|||
import Data.Sequence
|
||||
(fromList)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Network.HTTP.Media
|
||||
(MediaType, matches, parseAccept)
|
||||
import qualified Network.HTTP.Media as Media
|
||||
|
@ -69,12 +71,12 @@ import Network.HTTP.Types
|
|||
import qualified Network.HTTP.Types as H
|
||||
import Servant.API
|
||||
((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth, BasicAuthData,
|
||||
BuildHeadersTo (..), Capture', CaptureAll, Description,
|
||||
BuildHeadersTo (..), Capture', CaptureAll, DeepQuery, Description,
|
||||
EmptyAPI, Fragment, FramingRender (..), FramingUnrender (..),
|
||||
FromSourceIO (..), Header', Headers (..), HttpVersion,
|
||||
IsSecure, MimeRender (mimeRender),
|
||||
MimeUnrender (mimeUnrender), NoContent (NoContent),
|
||||
NoContentVerb, QueryFlag, QueryParam', QueryParams, Raw,
|
||||
NoContentVerb, QueryFlag, QueryParam', QueryParams, QueryString, Raw,
|
||||
ReflectMethod (..), RemoteHost, ReqBody', SBoolI, Stream,
|
||||
StreamBody', Summary, ToHttpApiData, ToSourceIO (..), Vault,
|
||||
Verb, WithNamedContext, WithStatus (..), contentType, getHeadersHList,
|
||||
|
@ -662,6 +664,44 @@ instance (KnownSymbol sym, HasClient m api)
|
|||
hoistClientMonad pm _ f cl = \b ->
|
||||
hoistClientMonad pm (Proxy :: Proxy api) f (cl b)
|
||||
|
||||
instance (HasClient m api)
|
||||
=> HasClient m (QueryString :> api) where
|
||||
type Client m (QueryString :> api) =
|
||||
H.Query -> Client m api
|
||||
|
||||
clientWithRoute pm Proxy req query =
|
||||
clientWithRoute pm (Proxy :: Proxy api)
|
||||
(setQueryString query req)
|
||||
|
||||
hoistClientMonad pm _ f cl = \b ->
|
||||
hoistClientMonad pm (Proxy :: Proxy api) f (cl b)
|
||||
|
||||
class ToDeepQuery a where
|
||||
toDeepQuery :: a -> [([T.Text], Maybe T.Text)]
|
||||
|
||||
generateDeepParam :: T.Text -> ([T.Text], Maybe T.Text) -> (T.Text, Maybe T.Text)
|
||||
generateDeepParam name (keys, value) =
|
||||
let makeKeySegment key = "[" <> key <> "]"
|
||||
in (name <> foldMap makeKeySegment keys, value)
|
||||
|
||||
instance (KnownSymbol sym, ToDeepQuery a, HasClient m api)
|
||||
=> HasClient m (DeepQuery sym a :> api) where
|
||||
type Client m (DeepQuery sym a :> api) =
|
||||
a -> Client m api
|
||||
|
||||
clientWithRoute pm Proxy req deepObject =
|
||||
let params = toDeepQuery deepObject
|
||||
withParams = foldl' addDeepParam req params
|
||||
addDeepParam r' kv =
|
||||
let (k, textV) = generateDeepParam paramname kv
|
||||
in appendToQueryString k (encodeUtf8 <$> textV) r'
|
||||
paramname = pack $ symbolVal (Proxy :: Proxy sym)
|
||||
in clientWithRoute pm (Proxy :: Proxy api)
|
||||
withParams
|
||||
|
||||
hoistClientMonad pm _ f cl = \b ->
|
||||
hoistClientMonad pm (Proxy :: Proxy api) f (cl b)
|
||||
|
||||
-- | Pick a 'Method' and specify where the server you want to query is. You get
|
||||
-- back the full `Response`.
|
||||
instance RunClient m => HasClient m Raw where
|
||||
|
|
|
@ -20,6 +20,9 @@ module Servant.Client.Core.Reexport
|
|||
, ClientError(..)
|
||||
, EmptyClient(..)
|
||||
|
||||
-- * DeepQuery
|
||||
, ToDeepQuery(..)
|
||||
|
||||
-- * BaseUrl
|
||||
, BaseUrl(..)
|
||||
, Scheme(..)
|
||||
|
|
|
@ -18,6 +18,7 @@ module Servant.Client.Core.Request (
|
|||
appendToPath,
|
||||
appendToQueryString,
|
||||
encodeQueryParamValue,
|
||||
setQueryString,
|
||||
setRequestBody,
|
||||
setRequestBodyLBS,
|
||||
) where
|
||||
|
@ -50,7 +51,7 @@ import GHC.Generics
|
|||
import Network.HTTP.Media
|
||||
(MediaType)
|
||||
import Network.HTTP.Types
|
||||
(Header, HeaderName, HttpVersion (..), Method, QueryItem,
|
||||
(Header, HeaderName, HttpVersion (..), Method, Query, QueryItem,
|
||||
http11, methodGet)
|
||||
import Servant.API
|
||||
(ToHttpApiData, toEncodedUrlPiece, toHeader, SourceIO)
|
||||
|
@ -162,6 +163,12 @@ appendToQueryString pname pvalue req
|
|||
= req { requestQueryString = requestQueryString req
|
||||
Seq.|> (encodeUtf8 pname, pvalue)}
|
||||
|
||||
setQueryString :: Query
|
||||
-> Request
|
||||
-> Request
|
||||
setQueryString query req
|
||||
= req { requestQueryString = Seq.fromList query }
|
||||
|
||||
-- | Encode a query parameter value.
|
||||
--
|
||||
encodeQueryParamValue :: ToHttpApiData a => a -> BS.ByteString
|
||||
|
|
|
@ -31,11 +31,13 @@ import Control.Monad.Error.Class
|
|||
import Data.Aeson
|
||||
import Data.ByteString
|
||||
(ByteString)
|
||||
import qualified Data.ByteString.Char8 as C8
|
||||
import Data.ByteString.Builder
|
||||
(byteString)
|
||||
import qualified Data.ByteString.Lazy as LazyByteString
|
||||
import Data.Char
|
||||
(chr, isPrint)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Monoid ()
|
||||
import Data.Proxy
|
||||
import Data.SOP
|
||||
|
@ -54,17 +56,18 @@ import Network.Wai.Handler.Warp
|
|||
import System.IO.Unsafe
|
||||
(unsafePerformIO)
|
||||
import Test.QuickCheck
|
||||
import Text.Read (readMaybe)
|
||||
import Web.FormUrlEncoded
|
||||
(FromForm, ToForm)
|
||||
|
||||
import Servant.API
|
||||
((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth,
|
||||
BasicAuthData (..), Capture, CaptureAll, DeleteNoContent,
|
||||
BasicAuthData (..), Capture, CaptureAll, DeepQuery, DeleteNoContent,
|
||||
EmptyAPI, FormUrlEncoded, Fragment, FromHttpApiData (..), Get, Header, Headers,
|
||||
JSON, MimeRender (mimeRender), MimeUnrender (mimeUnrender),
|
||||
NoContent (NoContent), PlainText, Post, QueryFlag, QueryParam,
|
||||
QueryParams, Raw, ReqBody, StdMethod (GET), ToHttpApiData (..), UVerb, Union,
|
||||
Verb, WithStatus (WithStatus), NamedRoutes, addHeader)
|
||||
QueryParams, QueryString, Raw, ReqBody, StdMethod (GET), ToHttpApiData (..),
|
||||
UVerb, Union, Verb, WithStatus (WithStatus), NamedRoutes, addHeader)
|
||||
import Servant.API.Generic ((:-))
|
||||
import Servant.Client
|
||||
import qualified Servant.Client.Core.Auth as Auth
|
||||
|
@ -121,6 +124,25 @@ data OtherRoutes mode = OtherRoutes
|
|||
-- Get for HTTP 307 Temporary Redirect
|
||||
type Get307 = Verb 'GET 307
|
||||
|
||||
data Filter = Filter
|
||||
{ ageFilter :: Integer
|
||||
, nameFilter :: String
|
||||
}
|
||||
deriving Show
|
||||
|
||||
instance FromDeepQuery Filter where
|
||||
fromDeepQuery params = do
|
||||
let maybeToRight l = maybe (Left l) Right
|
||||
age' <- maybeToRight "missing age" $ readMaybe . Text.unpack =<< join (lookup ["age"] params)
|
||||
name' <- maybeToRight "missing name" $ join $ lookup ["name"] params
|
||||
return $ Filter age' (Text.unpack name')
|
||||
|
||||
instance ToDeepQuery Filter where
|
||||
toDeepQuery (Filter age' name') =
|
||||
[ (["age"], Just (Text.pack $ show age'))
|
||||
, (["name"], Just (Text.pack name'))
|
||||
]
|
||||
|
||||
type Api =
|
||||
Get '[JSON] Person
|
||||
:<|> "get" :> Get '[JSON] Person
|
||||
|
@ -139,6 +161,8 @@ type Api =
|
|||
:<|> "param-binary" :> QueryParam "payload" UrlEncodedByteString :> Raw
|
||||
:<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]
|
||||
:<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool
|
||||
:<|> "query-string" :> QueryString :> Get '[JSON] Person
|
||||
:<|> "deep-query" :> DeepQuery "filter" Filter :> Get '[JSON] Person
|
||||
:<|> "fragment" :> Fragment String :> Get '[JSON] Person
|
||||
:<|> "rawSuccess" :> Raw
|
||||
:<|> "rawSuccessPassHeaders" :> Raw
|
||||
|
@ -176,6 +200,8 @@ getQueryParam :: Maybe String -> ClientM Person
|
|||
getQueryParamBinary :: Maybe UrlEncodedByteString -> HTTP.Method -> ClientM Response
|
||||
getQueryParams :: [String] -> ClientM [Person]
|
||||
getQueryFlag :: Bool -> ClientM Bool
|
||||
getQueryString :: [(ByteString, Maybe ByteString)] -> ClientM Person
|
||||
getDeepQuery :: Filter -> ClientM Person
|
||||
getFragment :: ClientM Person
|
||||
getRawSuccess :: HTTP.Method -> ClientM Response
|
||||
getRawSuccessPassHeaders :: HTTP.Method -> ClientM Response
|
||||
|
@ -203,6 +229,8 @@ getRoot
|
|||
:<|> getQueryParamBinary
|
||||
:<|> getQueryParams
|
||||
:<|> getQueryFlag
|
||||
:<|> getQueryString
|
||||
:<|> getDeepQuery
|
||||
:<|> getFragment
|
||||
:<|> getRawSuccess
|
||||
:<|> getRawSuccessPassHeaders
|
||||
|
@ -240,6 +268,14 @@ server = serve api (
|
|||
)
|
||||
:<|> (\ names -> return (zipWith Person names [0..]))
|
||||
:<|> return
|
||||
:<|> (\ q -> return alice { _name = maybe mempty C8.unpack $ join (lookup "name" q)
|
||||
, _age = fromMaybe 0 (readMaybe . C8.unpack =<< join (lookup "age" q))
|
||||
}
|
||||
)
|
||||
:<|> (\ filter' -> return alice { _name = nameFilter filter'
|
||||
, _age = ageFilter filter'
|
||||
}
|
||||
)
|
||||
:<|> return alice
|
||||
:<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "rawSuccess")
|
||||
:<|> (Tagged $ \ request respond -> (respond $ Wai.responseLBS HTTP.ok200 (Wai.requestHeaders $ request) "rawSuccess"))
|
||||
|
|
|
@ -115,6 +115,13 @@ successSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
|
|||
forM_ [False, True] $ \ flag -> it (show flag) $ \(_, baseUrl) -> do
|
||||
left show <$> runClient (getQueryFlag flag) baseUrl `shouldReturn` Right flag
|
||||
|
||||
it "Servant.API.QueryParam.QueryString" $ \(_, baseUrl) -> do
|
||||
let qs = [("name", Just "bob"), ("age", Just "1")]
|
||||
left show <$> runClient (getQueryString qs) baseUrl `shouldReturn` (Right (Person "bob" 1))
|
||||
|
||||
it "Servant.API.QueryParam.DeepQuery" $ \(_, baseUrl) -> do
|
||||
left show <$> runClient (getDeepQuery $ Filter 1 "bob") baseUrl `shouldReturn` (Right (Person "bob" 1))
|
||||
|
||||
it "Servant.API.Fragment" $ \(_, baseUrl) -> do
|
||||
left id <$> runClient getFragment baseUrl `shouldReturn` Right alice
|
||||
|
||||
|
|
|
@ -110,6 +110,9 @@ module Servant.Server
|
|||
|
||||
, getAcceptHeader
|
||||
|
||||
-- * DeepQuery parsing
|
||||
, FromDeepQuery (..)
|
||||
|
||||
-- * Re-exports
|
||||
, Application
|
||||
, Tagged (..)
|
||||
|
|
|
@ -38,6 +38,7 @@ import Control.Monad.Trans
|
|||
(liftIO)
|
||||
import Control.Monad.Trans.Resource
|
||||
(runResourceT)
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Builder as BB
|
||||
import qualified Data.ByteString.Char8 as BC8
|
||||
|
@ -45,6 +46,7 @@ import qualified Data.ByteString.Lazy as BL
|
|||
import Data.Constraint (Constraint, Dict(..))
|
||||
import Data.Either
|
||||
(partitionEithers)
|
||||
import qualified Data.Map.Strict as Map
|
||||
import Data.Maybe
|
||||
(fromMaybe, isNothing, mapMaybe, maybeToList)
|
||||
import Data.String
|
||||
|
@ -71,10 +73,10 @@ import Prelude ()
|
|||
import Prelude.Compat
|
||||
import Servant.API
|
||||
((:<|>) (..), (:>), Accept (..), BasicAuth, Capture',
|
||||
CaptureAll, Description, EmptyAPI, Fragment,
|
||||
CaptureAll, DeepQuery, Description, EmptyAPI, Fragment,
|
||||
FramingRender (..), FramingUnrender (..), FromSourceIO (..),
|
||||
Header', If, IsSecure (..), NoContentVerb, QueryFlag,
|
||||
QueryParam', QueryParams, Raw, ReflectMethod (reflectMethod),
|
||||
QueryParam', QueryParams, QueryString, Raw, ReflectMethod (reflectMethod),
|
||||
RemoteHost, ReqBody', SBool (..), SBoolI (..), SourceIO,
|
||||
Stream, StreamBody', Summary, ToSourceIO (..), Vault, Verb,
|
||||
WithNamedContext, NamedRoutes)
|
||||
|
@ -585,6 +587,121 @@ instance (KnownSymbol sym, HasServer api context)
|
|||
examine v | v == "true" || v == "1" || v == "" = True
|
||||
| otherwise = False
|
||||
|
||||
-- | If you use @'QueryString'@ in one of the endpoints for your API,
|
||||
-- this automatically requires your server-side handler to be a function
|
||||
-- that takes an argument of type @Query@ (@[('ByteString', 'Maybe' 'ByteString')]@).
|
||||
--
|
||||
-- This lets you extract the whole query string. This is useful when the query string
|
||||
-- can contain parameters with dynamic names, that you can't access with @'QueryParam'@.
|
||||
--
|
||||
-- Example:
|
||||
--
|
||||
-- > type MyApi = "books" :> QueryString :> Get '[JSON] [Book]
|
||||
-- >
|
||||
-- > server :: Server MyApi
|
||||
-- > server = getBooksBy
|
||||
-- > where getBooksBy :: Query -> Handler [Book]
|
||||
-- > getBooksBy filters = ...filter books based on the dynamic filters provided...
|
||||
instance
|
||||
( HasServer api context
|
||||
)
|
||||
=> HasServer (QueryString :> api) context where
|
||||
------
|
||||
type ServerT (QueryString :> api) m =
|
||||
Query -> ServerT api m
|
||||
|
||||
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
|
||||
|
||||
route Proxy context subserver =
|
||||
route (Proxy :: Proxy api) context (passToServer subserver queryString)
|
||||
|
||||
-- | If you use @'DeepQuery' "symbol" a@ in one of the endpoints for your API,
|
||||
-- this automatically requires your server-side handler to be a function
|
||||
-- that takes an argument of type @a@.
|
||||
--
|
||||
-- This lets you extract an object from multiple parameters in the query string,
|
||||
-- with its fields enclosed in brackets: `/books?filter[author][name]=value`. When
|
||||
-- all the fields are known in advance, it can be done with @'QueryParam'@ (it can
|
||||
-- still be tedious if you the object has many fields). When some fields are dynamic,
|
||||
-- it cannot be done with @'QueryParam'.
|
||||
--
|
||||
-- The way the object is constructed from the extracted fields can be controlled by
|
||||
-- providing an instance on @'FromDeepQuery'@
|
||||
--
|
||||
-- Example:
|
||||
--
|
||||
-- > type MyApi = "books" :> DeepQuery "filter" BookQuery :> Get '[JSON] [Book]
|
||||
-- >
|
||||
-- > server :: Server MyApi
|
||||
-- > server = getBooksBy
|
||||
-- > where getBooksBy :: BookQuery -> Handler [Book]
|
||||
-- > getBooksBy query = ...filter books based on the dynamic filters provided...
|
||||
instance
|
||||
( KnownSymbol sym, FromDeepQuery a, HasServer api context
|
||||
, HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters
|
||||
)
|
||||
=> HasServer (DeepQuery sym a :> api) context where
|
||||
------
|
||||
type ServerT (DeepQuery sym a :> api) m =
|
||||
a -> ServerT api m
|
||||
|
||||
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
|
||||
|
||||
route Proxy context subserver = route (Proxy :: Proxy api) context $
|
||||
subserver `addParameterCheck` withRequest paramsCheck
|
||||
where
|
||||
rep = typeRep (Proxy :: Proxy DeepQuery)
|
||||
formatError = urlParseErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context)
|
||||
|
||||
paramname = cs $ symbolVal (Proxy :: Proxy sym)
|
||||
paramsCheck req =
|
||||
let relevantParams :: [(T.Text, Maybe T.Text)]
|
||||
relevantParams = mapMaybe isRelevantParam
|
||||
. queryToQueryText
|
||||
. queryString
|
||||
$ req
|
||||
isRelevantParam (name, value) = (, value) <$>
|
||||
case T.stripPrefix paramname name of
|
||||
Just "" -> Just ""
|
||||
Just x | "[" `T.isPrefixOf` x -> Just x
|
||||
_ -> Nothing
|
||||
in case fromDeepQuery =<< traverse parseDeepParam relevantParams of
|
||||
Left e -> delayedFailFatal $ formatError rep req
|
||||
$ cs $ "Error parsing deep query parameter(s) "
|
||||
<> paramname <> T.pack " failed: "
|
||||
<> T.pack e
|
||||
Right parsed -> return parsed
|
||||
|
||||
parseDeepParam :: (T.Text, Maybe T.Text) -> Either String ([T.Text], Maybe T.Text)
|
||||
parseDeepParam (paramname, value) =
|
||||
let parseParam "" = return []
|
||||
parseParam n = reverse <$> go [] n
|
||||
go parsed remaining = case T.take 1 remaining of
|
||||
"[" -> case T.breakOn "]" remaining of
|
||||
(_ , "") -> Left $ "Error parsing deep param, missing closing ']': " <> T.unpack remaining
|
||||
(name, "]") -> return $ T.drop 1 name : parsed
|
||||
(name, remaining') -> case T.take 2 remaining' of
|
||||
"][" -> go (T.drop 1 name : parsed) (T.drop 1 remaining')
|
||||
_ -> Left $ "Error parsing deep param, incorrect brackets: " <> T.unpack remaining
|
||||
_ -> Left $ "Error parsing deep param, missing opening '[': " <> T.unpack remaining
|
||||
in (, value) <$> parseParam paramname
|
||||
|
||||
-- | Extract a deep object from (possibly nested) query parameters.
|
||||
-- a param like @filter[a][b][c]=d@ will be represented as
|
||||
-- @'(["a", "b", "c"], Just "d")'@. Note that a parameter with no
|
||||
-- nested field is possible: @filter=a@ will be represented as
|
||||
-- @'([], Just "a")'@
|
||||
class FromDeepQuery a where
|
||||
fromDeepQuery :: [([T.Text], Maybe T.Text)] -> Either String a
|
||||
|
||||
instance FromHttpApiData a => FromDeepQuery (Map.Map T.Text a) where
|
||||
fromDeepQuery params =
|
||||
let parseParam ([k], Just rawV) = (k,) <$> first T.unpack (parseQueryParam rawV)
|
||||
parseParam (_, Nothing) = Left "Empty map value"
|
||||
parseParam ([], _) = Left "Empty map parameter"
|
||||
parseParam (_ , Just _) = Left "Nested map values"
|
||||
in Map.fromList <$> traverse parseParam params
|
||||
|
||||
-- | Just pass the request to the underlying application and serve its response.
|
||||
--
|
||||
-- Example:
|
||||
|
|
|
@ -16,12 +16,13 @@ import Prelude ()
|
|||
import Prelude.Compat
|
||||
|
||||
import Control.Monad
|
||||
(forM_, unless, when)
|
||||
(forM_, join, unless, when)
|
||||
import Control.Monad.Error.Class
|
||||
(MonadError (..))
|
||||
import Data.Aeson
|
||||
(FromJSON, ToJSON, decode', encode)
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Char8 as C8
|
||||
import qualified Data.ByteString.Base64 as Base64
|
||||
import Data.Char
|
||||
(toUpper)
|
||||
|
@ -49,14 +50,15 @@ import Network.Wai.Test
|
|||
import Servant.API
|
||||
((:<|>) (..), (:>), AuthProtect, BasicAuth,
|
||||
BasicAuthData (BasicAuthData), Capture, Capture', CaptureAll,
|
||||
Delete, EmptyAPI, Fragment, Get, HasStatus (StatusOf), Header,
|
||||
Headers, HttpVersion, IsSecure (..), JSON, Lenient,
|
||||
NoContent (..), NoContentVerb, NoFraming, OctetStream, Patch,
|
||||
PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw,
|
||||
DeepQuery, Delete, EmptyAPI, Fragment, Get,
|
||||
HasStatus (StatusOf), Header, Headers, HttpVersion,
|
||||
IsSecure (..), JSON, Lenient, NoContent (..), NoContentVerb,
|
||||
NoFraming, OctetStream, Patch, PlainText, Post, Put,
|
||||
QueryFlag, QueryParam, QueryParams, QueryString, Raw,
|
||||
RemoteHost, ReqBody, SourceIO, StdMethod (..), Stream, Strict,
|
||||
UVerb, Union, Verb, WithStatus (..), addHeader)
|
||||
import Servant.Server
|
||||
(Context ((:.), EmptyContext), Handler, Server, Tagged (..),
|
||||
(Context ((:.), EmptyContext), FromDeepQuery (..), Handler, Server, Tagged (..),
|
||||
emptyServer, err401, err403, err404, respond, serve,
|
||||
serveWithContext)
|
||||
import Servant.Test.ComprehensiveAPI
|
||||
|
@ -67,6 +69,7 @@ import Test.Hspec.Wai
|
|||
(get, liftIO, matchHeaders, matchStatus, shouldRespondWith,
|
||||
with, (<:>))
|
||||
import qualified Test.Hspec.Wai as THW
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
import Servant.Server.Experimental.Auth
|
||||
(AuthHandler, AuthServerData, mkAuthHandler)
|
||||
|
@ -320,17 +323,33 @@ captureAllSpec = do
|
|||
-- * queryParamSpec {{{
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
data Filter = Filter
|
||||
{ ageFilter :: Integer
|
||||
, nameFilter :: String
|
||||
}
|
||||
deriving Show
|
||||
|
||||
instance FromDeepQuery Filter where
|
||||
fromDeepQuery params = do
|
||||
let maybeToRight l = maybe (Left l) Right
|
||||
age' <- maybeToRight "missing age" $ readMaybe . T.unpack =<< join (lookup ["age"] params)
|
||||
name' <- maybeToRight "missing name" $ join $ lookup ["name"] params
|
||||
return $ Filter age' (T.unpack name')
|
||||
|
||||
|
||||
type QueryParamApi = QueryParam "name" String :> Get '[JSON] Person
|
||||
:<|> "a" :> QueryParams "names" String :> Get '[JSON] Person
|
||||
:<|> "b" :> QueryFlag "capitalize" :> Get '[JSON] Person
|
||||
:<|> "param" :> QueryParam "age" Integer :> Get '[JSON] Person
|
||||
:<|> "multiparam" :> QueryParams "ages" Integer :> Get '[JSON] Person
|
||||
:<|> "raw-query-string" :> QueryString :> Get '[JSON] Person
|
||||
:<|> "deep-query" :> DeepQuery "filter" Filter :> Get '[JSON] Person
|
||||
|
||||
queryParamApi :: Proxy QueryParamApi
|
||||
queryParamApi = Proxy
|
||||
|
||||
qpServer :: Server QueryParamApi
|
||||
qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize :<|> qpAge :<|> qpAges
|
||||
qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize :<|> qpAge :<|> qpAges :<|> qpRaw :<|> qpDeep
|
||||
|
||||
where qpNames (_:name2:_) = return alice { name = name2 }
|
||||
qpNames _ = return alice
|
||||
|
@ -343,6 +362,15 @@ qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize :<|> qpAge :<|> qpAge
|
|||
|
||||
qpAges ages = return alice{ age = sum ages}
|
||||
|
||||
qpRaw q = return alice { name = maybe mempty C8.unpack $ join (lookup "name" q)
|
||||
, age = fromMaybe 0 (readMaybe . C8.unpack =<< join (lookup "age" q))
|
||||
}
|
||||
|
||||
qpDeep filter' =
|
||||
return alice { name = nameFilter filter'
|
||||
, age = ageFilter filter'
|
||||
}
|
||||
|
||||
queryParamServer (Just name_) = return alice{name = name_}
|
||||
queryParamServer Nothing = return alice
|
||||
|
||||
|
@ -414,6 +442,22 @@ queryParamSpec = do
|
|||
{ name = "Alice"
|
||||
}
|
||||
|
||||
it "allows retrieving a full query string" $
|
||||
flip runSession (serve queryParamApi qpServer) $ do
|
||||
response <- mkRequest "?age=32&name=john" ["raw-query-string"]
|
||||
liftIO $ decode' (simpleBody response) `shouldBe` Just alice
|
||||
{ name = "john"
|
||||
, age = 32
|
||||
}
|
||||
|
||||
it "allows retrieving a query string deep object" $
|
||||
flip runSession (serve queryParamApi qpServer) $ do
|
||||
response <- mkRequest "?filter[age]=32&filter[name]=john" ["deep-query"]
|
||||
liftIO $ decode' (simpleBody response) `shouldBe` Just alice
|
||||
{ name = "john"
|
||||
, age = 32
|
||||
}
|
||||
|
||||
describe "Uses queryString instead of rawQueryString" $ do
|
||||
-- test query parameters rewriter
|
||||
let queryRewriter :: Middleware
|
||||
|
|
|
@ -48,6 +48,7 @@ library
|
|||
Servant.API.Modifiers
|
||||
Servant.API.NamedRoutes
|
||||
Servant.API.QueryParam
|
||||
Servant.API.QueryString
|
||||
Servant.API.Raw
|
||||
Servant.API.RemoteHost
|
||||
Servant.API.ReqBody
|
||||
|
|
|
@ -19,6 +19,8 @@ module Servant.API (
|
|||
-- | Retrieving the HTTP version of the request
|
||||
module Servant.API.QueryParam,
|
||||
-- | Retrieving parameters from the query string of the 'URI': @'QueryParam'@
|
||||
module Servant.API.QueryString,
|
||||
-- | Retrieving the complete query string of the 'URI': @'QueryString'@
|
||||
module Servant.API.Fragment,
|
||||
-- | Documenting the fragment of the 'URI': @'Fragment'@
|
||||
module Servant.API.ReqBody,
|
||||
|
@ -114,6 +116,8 @@ import Servant.API.Modifiers
|
|||
(Lenient, Optional, Required, Strict)
|
||||
import Servant.API.QueryParam
|
||||
(QueryFlag, QueryParam, QueryParam', QueryParams)
|
||||
import Servant.API.QueryString
|
||||
(QueryString, DeepQuery)
|
||||
import Servant.API.Raw
|
||||
(Raw)
|
||||
import Servant.API.RemoteHost
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
{-# LANGUAGE PolyKinds #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# OPTIONS_HADDOCK not-home #-}
|
||||
module Servant.API.QueryString (QueryString, DeepQuery) where
|
||||
|
||||
import Data.Typeable
|
||||
(Typeable)
|
||||
import GHC.TypeLits
|
||||
(Symbol)
|
||||
|
||||
-- | Extract the whole query string from a request. This is useful for query strings
|
||||
-- containing dynamic parameter names. For query strings with static parameter names,
|
||||
-- 'QueryParam' is more suited.
|
||||
--
|
||||
-- Example:
|
||||
--
|
||||
-- >>> -- /books?author=<author name>&year=<book year>
|
||||
-- >>> type MyApi = "books" :> QueryString :> Get '[JSON] [Book]
|
||||
data QueryString
|
||||
deriving Typeable
|
||||
|
||||
-- | Extract an deep object from a query string.
|
||||
--
|
||||
-- Example:
|
||||
--
|
||||
-- >>> -- /books?filter[author][name]=<author name>&filter[year]=<book year>
|
||||
-- >>> type MyApi = "books" :> DeepQuery "filter" BookQuery :> Get '[JSON] [Book]
|
||||
data DeepQuery (sym :: Symbol) (a :: *)
|
||||
deriving Typeable
|
||||
|
||||
-- $setup
|
||||
-- >>> import Servant.API
|
||||
-- >>> import Data.Aeson
|
||||
-- >>> import Data.Text
|
||||
-- >>> data Book
|
||||
-- >>> data BookQuery
|
||||
-- >>> instance ToJSON Book where { toJSON = undefined }
|
Loading…
Reference in New Issue