Add QueryParamForm for Client, Server, Internal, Foreign, and SafeLink
This commit is contained in:
parent
38f3da2499
commit
cffa511df9
19 changed files with 469 additions and 22 deletions
|
@ -79,6 +79,7 @@ library
|
||||||
, base64-bytestring >= 1.0.0.1 && < 1.1
|
, base64-bytestring >= 1.0.0.1 && < 1.1
|
||||||
, exceptions >= 0.10.0 && < 0.11
|
, exceptions >= 0.10.0 && < 0.11
|
||||||
, free >= 5.1 && < 5.2
|
, free >= 5.1 && < 5.2
|
||||||
|
, http-api-data >= 0.4 && < 0.4.2
|
||||||
, http-media >= 0.7.1.3 && < 0.9
|
, http-media >= 0.7.1.3 && < 0.9
|
||||||
, http-types >= 0.12.2 && < 0.13
|
, http-types >= 0.12.2 && < 0.13
|
||||||
, network-uri >= 2.6.1.0 && < 2.7
|
, network-uri >= 2.6.1.0 && < 2.7
|
||||||
|
|
|
@ -55,6 +55,7 @@ module Servant.Client.Core
|
||||||
, addHeader
|
, addHeader
|
||||||
, appendToQueryString
|
, appendToQueryString
|
||||||
, appendToPath
|
, appendToPath
|
||||||
|
, concatQueryString
|
||||||
, setRequestBodyLBS
|
, setRequestBodyLBS
|
||||||
, setRequestBody
|
, setRequestBody
|
||||||
) where
|
) where
|
||||||
|
|
|
@ -48,7 +48,7 @@ import Servant.API
|
||||||
FromSourceIO (..), Header', Headers (..), HttpVersion,
|
FromSourceIO (..), Header', Headers (..), HttpVersion,
|
||||||
IsSecure, MimeRender (mimeRender),
|
IsSecure, MimeRender (mimeRender),
|
||||||
MimeUnrender (mimeUnrender), NoContent (NoContent), QueryFlag,
|
MimeUnrender (mimeUnrender), NoContent (NoContent), QueryFlag,
|
||||||
QueryParam', QueryParams, Raw, ReflectMethod (..), RemoteHost,
|
QueryParam', QueryParams, QueryParamForm', Raw, ReflectMethod (..), RemoteHost,
|
||||||
ReqBody', SBoolI, Stream, StreamBody', Summary, ToHttpApiData,
|
ReqBody', SBoolI, Stream, StreamBody', Summary, ToHttpApiData,
|
||||||
ToSourceIO (..), Vault, Verb, NoContentVerb, WithNamedContext,
|
ToSourceIO (..), Vault, Verb, NoContentVerb, WithNamedContext,
|
||||||
contentType, getHeadersHList, getResponse, toQueryParam,
|
contentType, getHeadersHList, getResponse, toQueryParam,
|
||||||
|
@ -57,6 +57,8 @@ import Servant.API.ContentTypes
|
||||||
(contentTypes)
|
(contentTypes)
|
||||||
import Servant.API.Modifiers
|
import Servant.API.Modifiers
|
||||||
(FoldRequired, RequiredArgument, foldRequiredArgument)
|
(FoldRequired, RequiredArgument, foldRequiredArgument)
|
||||||
|
import Web.FormUrlEncoded
|
||||||
|
(ToForm (..))
|
||||||
|
|
||||||
import Servant.Client.Core.Auth
|
import Servant.Client.Core.Auth
|
||||||
import Servant.Client.Core.BasicAuth
|
import Servant.Client.Core.BasicAuth
|
||||||
|
@ -534,6 +536,55 @@ instance (KnownSymbol sym, HasClient m api)
|
||||||
hoistClientMonad pm _ f cl = \b ->
|
hoistClientMonad pm _ f cl = \b ->
|
||||||
hoistClientMonad pm (Proxy :: Proxy api) f (cl b)
|
hoistClientMonad pm (Proxy :: Proxy api) f (cl b)
|
||||||
|
|
||||||
|
-- | If you use a 'QueryParamForm' in one of your endpoints in your API,
|
||||||
|
-- the corresponding querying function will automatically take
|
||||||
|
-- an additional argument of the type specified by your 'QueryParamForm',
|
||||||
|
-- enclosed in Maybe.
|
||||||
|
--
|
||||||
|
-- If you give Nothing, nothing will be added to the query string.
|
||||||
|
--
|
||||||
|
-- If you give a non-'Nothing' value, this function will take care
|
||||||
|
-- of inserting a textual representation of your form in the query string.
|
||||||
|
--
|
||||||
|
-- You can control how values for your type are turned into
|
||||||
|
-- text by specifying a 'ToForm' instance for your type.
|
||||||
|
-- Example:
|
||||||
|
--
|
||||||
|
-- > data BookSearchParams = BookSearchParams
|
||||||
|
-- > { title :: Text
|
||||||
|
-- > , authors :: [Text]
|
||||||
|
-- > , page :: Maybe Int
|
||||||
|
-- > } deriving (Eq, Show, Generic)
|
||||||
|
-- > instance ToForm BookSearchParams
|
||||||
|
--
|
||||||
|
-- > type MyApi = "books" :> QueryParamForm "searchQ" BookSearchParams :> Get '[JSON] [Book]
|
||||||
|
-- >
|
||||||
|
-- > myApi :: Proxy MyApi
|
||||||
|
-- > myApi = Proxy
|
||||||
|
-- >
|
||||||
|
-- > getBooks :: Bool -> ClientM [Book]
|
||||||
|
-- > getBooks = client myApi
|
||||||
|
-- > -- then you can just use "getBooks" to query that endpoint.
|
||||||
|
-- > -- 'getBooksBy Nothing' for all books
|
||||||
|
-- > -- 'getBooksBy (Just $ BookSearchParams "white noise" ["DeLillo"] Nothing)'
|
||||||
|
instance (KnownSymbol sym, ToForm a, HasClient m api, SBoolI (FoldRequired mods))
|
||||||
|
=> HasClient m (QueryParamForm' mods sym a :> api) where
|
||||||
|
|
||||||
|
type Client m (QueryParamForm' mods sym a :> api) =
|
||||||
|
RequiredArgument mods a -> Client m api
|
||||||
|
|
||||||
|
-- if mparam = Nothing, we don't add it to the query string
|
||||||
|
clientWithRoute pm Proxy req mparam =
|
||||||
|
clientWithRoute pm (Proxy :: Proxy api) $ foldRequiredArgument
|
||||||
|
(Proxy :: Proxy mods) add (maybe req add) mparam
|
||||||
|
where
|
||||||
|
add :: ToForm a => a -> Request
|
||||||
|
add qForm = concatQueryString qForm req
|
||||||
|
|
||||||
|
hoistClientMonad pm _ f cl = \arg ->
|
||||||
|
hoistClientMonad pm (Proxy :: Proxy api) f (cl arg)
|
||||||
|
|
||||||
|
|
||||||
-- | Pick a 'Method' and specify where the server you want to query is. You get
|
-- | Pick a 'Method' and specify where the server you want to query is. You get
|
||||||
-- back the full `Response`.
|
-- back the full `Response`.
|
||||||
instance RunClient m => HasClient m Raw where
|
instance RunClient m => HasClient m Raw where
|
||||||
|
@ -710,4 +761,4 @@ decodedAs response ct = do
|
||||||
Left err -> throwClientError $ DecodeFailure (T.pack err) response
|
Left err -> throwClientError $ DecodeFailure (T.pack err) response
|
||||||
Right val -> return val
|
Right val -> return val
|
||||||
where
|
where
|
||||||
accept = toList $ contentTypes ct
|
accept = toList $ contentTypes ct
|
||||||
|
|
|
@ -17,6 +17,7 @@ module Servant.Client.Core.Request (
|
||||||
addHeader,
|
addHeader,
|
||||||
appendToPath,
|
appendToPath,
|
||||||
appendToQueryString,
|
appendToQueryString,
|
||||||
|
concatQueryString,
|
||||||
setRequestBody,
|
setRequestBody,
|
||||||
setRequestBodyLBS,
|
setRequestBodyLBS,
|
||||||
) where
|
) where
|
||||||
|
@ -50,9 +51,11 @@ import Network.HTTP.Media
|
||||||
(MediaType)
|
(MediaType)
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
(Header, HeaderName, HttpVersion (..), Method, QueryItem,
|
(Header, HeaderName, HttpVersion (..), Method, QueryItem,
|
||||||
http11, methodGet)
|
http11, methodGet, parseQuery)
|
||||||
import Servant.API
|
import Servant.API
|
||||||
(ToHttpApiData, toEncodedUrlPiece, toHeader, SourceIO)
|
(ToHttpApiData, toEncodedUrlPiece, toHeader, SourceIO)
|
||||||
|
import Web.FormUrlEncoded
|
||||||
|
(ToForm (..), urlEncodeAsForm)
|
||||||
|
|
||||||
import Servant.Client.Core.Internal (mediaTypeRnf)
|
import Servant.Client.Core.Internal (mediaTypeRnf)
|
||||||
|
|
||||||
|
@ -135,6 +138,14 @@ addHeader :: ToHttpApiData a => HeaderName -> a -> Request -> Request
|
||||||
addHeader name val req
|
addHeader name val req
|
||||||
= req { requestHeaders = requestHeaders req Seq.|> (name, toHeader val)}
|
= req { requestHeaders = requestHeaders req Seq.|> (name, toHeader val)}
|
||||||
|
|
||||||
|
concatQueryString :: ToForm a
|
||||||
|
=> a
|
||||||
|
-> Request
|
||||||
|
-> Request
|
||||||
|
concatQueryString form req
|
||||||
|
= let querySeq = Seq.fromList . parseQuery . LBS.toStrict . urlEncodeAsForm $ form
|
||||||
|
in req { requestQueryString = requestQueryString req Seq.>< querySeq }
|
||||||
|
|
||||||
-- | Set body and media type of the request being constructed.
|
-- | Set body and media type of the request being constructed.
|
||||||
--
|
--
|
||||||
-- The body is set to the given bytestring using the 'RequestBodyLBS'
|
-- The body is set to the given bytestring using the 'RequestBodyLBS'
|
||||||
|
|
|
@ -62,6 +62,7 @@ library
|
||||||
, base-compat >= 0.10.5 && < 0.12
|
, base-compat >= 0.10.5 && < 0.12
|
||||||
, case-insensitive >= 1.2.0.11 && < 1.3
|
, case-insensitive >= 1.2.0.11 && < 1.3
|
||||||
, hashable >= 1.2.7.0 && < 1.4
|
, hashable >= 1.2.7.0 && < 1.4
|
||||||
|
, http-api-data >= 0.4 && < 0.4.2
|
||||||
, http-media >= 0.7.1.3 && < 0.9
|
, http-media >= 0.7.1.3 && < 0.9
|
||||||
, http-types >= 0.12.2 && < 0.13
|
, http-types >= 0.12.2 && < 0.13
|
||||||
, lens >= 4.17 && < 4.19
|
, lens >= 4.17 && < 4.19
|
||||||
|
@ -100,6 +101,7 @@ test-suite spec
|
||||||
base
|
base
|
||||||
, base-compat
|
, base-compat
|
||||||
, aeson
|
, aeson
|
||||||
|
, http-api-data
|
||||||
, lens
|
, lens
|
||||||
, servant
|
, servant
|
||||||
, servant-docs
|
, servant-docs
|
||||||
|
|
|
@ -28,9 +28,12 @@ import Control.Lens
|
||||||
(makeLenses, mapped, over, traversed, view, (%~), (&), (.~),
|
(makeLenses, mapped, over, traversed, view, (%~), (&), (.~),
|
||||||
(<>~), (^.), (|>))
|
(<>~), (^.), (|>))
|
||||||
import qualified Data.ByteString.Char8 as BSC
|
import qualified Data.ByteString.Char8 as BSC
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as LBSC
|
||||||
import Data.ByteString.Lazy.Char8
|
import Data.ByteString.Lazy.Char8
|
||||||
(ByteString)
|
(ByteString)
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
import Data.Data
|
||||||
|
(Data, toConstr, constrFields)
|
||||||
import Data.Foldable
|
import Data.Foldable
|
||||||
(toList)
|
(toList)
|
||||||
import Data.Foldable
|
import Data.Foldable
|
||||||
|
@ -63,6 +66,8 @@ import GHC.TypeLits
|
||||||
import Servant.API
|
import Servant.API
|
||||||
import Servant.API.ContentTypes
|
import Servant.API.ContentTypes
|
||||||
import Servant.API.TypeLevel
|
import Servant.API.TypeLevel
|
||||||
|
import Web.FormUrlEncoded
|
||||||
|
(ToForm(..), urlEncodeAsForm)
|
||||||
|
|
||||||
import qualified Data.Universe.Helpers as U
|
import qualified Data.Universe.Helpers as U
|
||||||
|
|
||||||
|
@ -950,6 +955,27 @@ instance (KnownSymbol sym, ToParam (QueryFlag sym), HasDocs api)
|
||||||
paramP = Proxy :: Proxy (QueryFlag sym)
|
paramP = Proxy :: Proxy (QueryFlag sym)
|
||||||
action' = over params (|> toParam paramP) action
|
action' = over params (|> toParam paramP) action
|
||||||
|
|
||||||
|
-- | The docs for a @'QueryParamForm' sym a'@
|
||||||
|
-- require the following instances for the `a`:
|
||||||
|
-- 'Data', 'ToSample'
|
||||||
|
instance (KnownSymbol sym, Data a, ToForm a, ToSample a, HasDocs api)
|
||||||
|
=> HasDocs (QueryParamForm' mods sym a :> api) where
|
||||||
|
|
||||||
|
docsFor Proxy (endpoint, action) =
|
||||||
|
docsFor subApiP (endpoint, action')
|
||||||
|
|
||||||
|
where subApiP = Proxy :: Proxy api
|
||||||
|
action' =
|
||||||
|
let (Just sampleForm) = toSample (Proxy :: Proxy a)
|
||||||
|
paramNames = constrFields (toConstr sampleForm)
|
||||||
|
sampleEncoding = LBSC.unpack . urlEncodeAsForm . toForm $ sampleForm
|
||||||
|
in action & params <>~ (fmap (qParamMaker sampleEncoding) paramNames)
|
||||||
|
qParamMaker formEncodedSample pName = DocQueryParam {
|
||||||
|
_paramName = pName
|
||||||
|
, _paramValues = [formEncodedSample]
|
||||||
|
, _paramDesc = "Query parameter"
|
||||||
|
, _paramKind = Normal
|
||||||
|
}
|
||||||
|
|
||||||
instance HasDocs Raw where
|
instance HasDocs Raw where
|
||||||
docsFor _proxy (endpoint, action) _ =
|
docsFor _proxy (endpoint, action) _ =
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{-# LANGUAGE DataKinds #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
{-# LANGUAGE DeriveFunctor #-}
|
{-# LANGUAGE DeriveFunctor #-}
|
||||||
{-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
@ -21,6 +22,8 @@ import Control.Monad
|
||||||
import Control.Monad.Trans.Writer
|
import Control.Monad.Trans.Writer
|
||||||
(Writer, runWriter, tell)
|
(Writer, runWriter, tell)
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
|
import Data.Data
|
||||||
|
(Data)
|
||||||
import Data.List
|
import Data.List
|
||||||
(isInfixOf)
|
(isInfixOf)
|
||||||
import Data.Proxy
|
import Data.Proxy
|
||||||
|
@ -35,6 +38,8 @@ import Test.Tasty.Golden
|
||||||
(goldenVsString)
|
(goldenVsString)
|
||||||
import Test.Tasty.HUnit
|
import Test.Tasty.HUnit
|
||||||
(Assertion, HasCallStack, assertFailure, testCase, (@?=))
|
(Assertion, HasCallStack, assertFailure, testCase, (@?=))
|
||||||
|
import Web.FormUrlEncoded
|
||||||
|
(ToForm)
|
||||||
|
|
||||||
import Servant.API
|
import Servant.API
|
||||||
import Servant.Docs.Internal
|
import Servant.Docs.Internal
|
||||||
|
@ -52,6 +57,8 @@ instance ToParam (QueryParam' mods "bar" Int) where
|
||||||
toParam _ = DocQueryParam "bar" ["1","2","3"] "QueryParams Int" Normal
|
toParam _ = DocQueryParam "bar" ["1","2","3"] "QueryParams Int" Normal
|
||||||
instance ToParam (QueryParams "foo" Int) where
|
instance ToParam (QueryParams "foo" Int) where
|
||||||
toParam _ = DocQueryParam "foo" ["1","2","3"] "QueryParams Int" List
|
toParam _ = DocQueryParam "foo" ["1","2","3"] "QueryParams Int" List
|
||||||
|
instance ToParam (QueryParam "query" String) where
|
||||||
|
toParam _ = DocQueryParam "query" ["a","b","c"] "QueryParams String" Normal
|
||||||
instance ToParam (QueryFlag "foo") where
|
instance ToParam (QueryFlag "foo") where
|
||||||
toParam _ = DocQueryParam "foo" [] "QueryFlag" Flag
|
toParam _ = DocQueryParam "foo" [] "QueryFlag" Flag
|
||||||
instance ToCapture (Capture "foo" Int) where
|
instance ToCapture (Capture "foo" Int) where
|
||||||
|
@ -76,7 +83,7 @@ spec = describe "Servant.Docs" $ do
|
||||||
(defAction & notes <>~ [DocNote "Get an Integer" ["get an integer in Json or plain text"]])
|
(defAction & notes <>~ [DocNote "Get an Integer" ["get an integer in Json or plain text"]])
|
||||||
<>
|
<>
|
||||||
extraInfo
|
extraInfo
|
||||||
(Proxy :: Proxy (ReqBody '[JSON] String :> Post '[JSON] Datatype1))
|
(Proxy :: Proxy ("postJson" :> ReqBody '[JSON] String :> Post '[JSON] Datatype1))
|
||||||
(defAction & notes <>~ [DocNote "Post data" ["Posts some Json data"]])
|
(defAction & notes <>~ [DocNote "Post data" ["Posts some Json data"]])
|
||||||
md = markdown (docsWith defaultDocOptions [] extra (Proxy :: Proxy TestApi1))
|
md = markdown (docsWith defaultDocOptions [] extra (Proxy :: Proxy TestApi1))
|
||||||
tests md
|
tests md
|
||||||
|
@ -119,6 +126,12 @@ spec = describe "Servant.Docs" $ do
|
||||||
md `shouldContain` "## POST"
|
md `shouldContain` "## POST"
|
||||||
md `shouldContain` "## GET"
|
md `shouldContain` "## GET"
|
||||||
|
|
||||||
|
it "should mention the endpoints" $ do
|
||||||
|
md `shouldContain` "## POST /postJson"
|
||||||
|
md `shouldContain` "## GET /qparam"
|
||||||
|
md `shouldContain` "## GET /qparamform"
|
||||||
|
md `shouldContain` "## PUT /header"
|
||||||
|
|
||||||
it "mentions headers" $ do
|
it "mentions headers" $ do
|
||||||
md `shouldContain` "- This endpoint is sensitive to the value of the **X-Test** HTTP header."
|
md `shouldContain` "- This endpoint is sensitive to the value of the **X-Test** HTTP header."
|
||||||
|
|
||||||
|
@ -127,6 +140,15 @@ spec = describe "Servant.Docs" $ do
|
||||||
it "contains request body samples" $
|
it "contains request body samples" $
|
||||||
md `shouldContain` "17"
|
md `shouldContain` "17"
|
||||||
|
|
||||||
|
it "mentions optional query-param" $ do
|
||||||
|
md `shouldContain` "### GET Parameters:"
|
||||||
|
md `shouldContain` "- query"
|
||||||
|
it "mentions optional query-param-form params from QueryParamForm" $ do
|
||||||
|
md `shouldContain` "- dt1field1"
|
||||||
|
md `shouldContain` "- dt1field2"
|
||||||
|
-- contains sample url-encoded form
|
||||||
|
md `shouldContain` "- **Values**: *dt1field1=field%201&dt1field2=13*"
|
||||||
|
|
||||||
it "does not generate any docs mentioning the 'empty-api' path" $
|
it "does not generate any docs mentioning the 'empty-api' path" $
|
||||||
md `shouldNotContain` "empty-api"
|
md `shouldNotContain` "empty-api"
|
||||||
|
|
||||||
|
@ -135,9 +157,10 @@ spec = describe "Servant.Docs" $ do
|
||||||
|
|
||||||
data Datatype1 = Datatype1 { dt1field1 :: String
|
data Datatype1 = Datatype1 { dt1field1 :: String
|
||||||
, dt1field2 :: Int
|
, dt1field2 :: Int
|
||||||
} deriving (Eq, Show, Generic)
|
} deriving (Eq, Show, Data, Generic)
|
||||||
|
|
||||||
instance ToJSON Datatype1
|
instance ToJSON Datatype1
|
||||||
|
instance ToForm Datatype1
|
||||||
|
|
||||||
instance ToSample Datatype1 where
|
instance ToSample Datatype1 where
|
||||||
toSamples _ = singleSample $ Datatype1 "field 1" 13
|
toSamples _ = singleSample $ Datatype1 "field 1" 13
|
||||||
|
@ -152,9 +175,11 @@ instance MimeRender PlainText Int where
|
||||||
mimeRender _ = cs . show
|
mimeRender _ = cs . show
|
||||||
|
|
||||||
type TestApi1 = Get '[JSON, PlainText] (Headers '[Header "Location" String] Int)
|
type TestApi1 = Get '[JSON, PlainText] (Headers '[Header "Location" String] Int)
|
||||||
:<|> ReqBody '[JSON] String :> Post '[JSON] Datatype1
|
:<|> "postJson" :> ReqBody '[JSON] String :> Post '[JSON] Datatype1
|
||||||
:<|> Header "X-Test" Int :> Put '[JSON] Int
|
:<|> "qparam" :> QueryParam "query" String :> Get '[JSON] Datatype1
|
||||||
:<|> "empty-api" :> EmptyAPI
|
:<|> "qparamform" :> QueryParamForm "form" Datatype1 :> Get '[JSON] Datatype1
|
||||||
|
:<|> "header" :> Header "X-Test" Int :> Put '[JSON] Int
|
||||||
|
:<|> "empty-api" :> EmptyAPI
|
||||||
|
|
||||||
data TT = TT1 | TT2 deriving (Show, Eq)
|
data TT = TT1 | TT2 deriving (Show, Eq)
|
||||||
data UT = UT1 | UT2 deriving (Show, Eq)
|
data UT = UT1 | UT2 deriving (Show, Eq)
|
||||||
|
|
|
@ -88,6 +88,7 @@ data ArgType
|
||||||
= Normal
|
= Normal
|
||||||
| Flag
|
| Flag
|
||||||
| List
|
| List
|
||||||
|
| Form
|
||||||
deriving (Data, Eq, Show, Typeable)
|
deriving (Data, Eq, Show, Typeable)
|
||||||
|
|
||||||
makePrisms ''ArgType
|
makePrisms ''ArgType
|
||||||
|
@ -324,6 +325,19 @@ instance
|
||||||
{ _argName = PathSegment str
|
{ _argName = PathSegment str
|
||||||
, _argType = typeFor lang ftype (Proxy :: Proxy Bool) }
|
, _argType = typeFor lang ftype (Proxy :: Proxy Bool) }
|
||||||
|
|
||||||
|
instance (KnownSymbol sym, HasForeignType lang ftype (RequiredArgument mods a), HasForeign lang ftype api)
|
||||||
|
=> HasForeign lang ftype (QueryParamForm' mods sym a :> api) where
|
||||||
|
type Foreign ftype (QueryParamForm' mods sym a :> api) = Foreign ftype api
|
||||||
|
|
||||||
|
foreignFor lang Proxy Proxy req =
|
||||||
|
foreignFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy api) $
|
||||||
|
req & reqUrl.queryStr <>~ [QueryArg arg Form]
|
||||||
|
where
|
||||||
|
arg = Arg
|
||||||
|
{ _argName = PathSegment ""
|
||||||
|
, _argType = typeFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy (RequiredArgument mods a)) }
|
||||||
|
|
||||||
|
|
||||||
instance HasForeign lang ftype Raw where
|
instance HasForeign lang ftype Raw where
|
||||||
type Foreign ftype Raw = HTTP.Method -> Req ftype
|
type Foreign ftype Raw = HTTP.Method -> Req ftype
|
||||||
|
|
||||||
|
|
|
@ -68,9 +68,21 @@ instance {-# OVERLAPPABLE #-} HasForeignType LangX String a => HasForeignType La
|
||||||
instance (HasForeignType LangX String a) => HasForeignType LangX String (Maybe a) where
|
instance (HasForeignType LangX String a) => HasForeignType LangX String (Maybe a) where
|
||||||
typeFor lang ftype _ = "maybe " <> typeFor lang ftype (Proxy :: Proxy a)
|
typeFor lang ftype _ = "maybe " <> typeFor lang ftype (Proxy :: Proxy a)
|
||||||
|
|
||||||
|
data ContactForm = ContactForm {
|
||||||
|
name :: String
|
||||||
|
, message :: String
|
||||||
|
, email :: String
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
instance HasForeignType LangX String ContactForm where
|
||||||
|
typeFor _ _ _ = "contactFormX"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type TestApi
|
type TestApi
|
||||||
= "test" :> Header "header" [String] :> QueryFlag "flag" :> Get '[JSON] Int
|
= "test" :> Header "header" [String] :> QueryFlag "flag" :> Get '[JSON] Int
|
||||||
:<|> "test" :> QueryParam "param" Int :> ReqBody '[JSON] [String] :> Post '[JSON] NoContent
|
:<|> "test" :> QueryParam "param" Int :> ReqBody '[JSON] [String] :> Post '[JSON] NoContent
|
||||||
|
:<|> "test" :> QueryParamForm "contact" ContactForm :> Post '[JSON] NoContent
|
||||||
:<|> "test" :> QueryParams "params" Int :> ReqBody '[JSON] String :> Put '[JSON] NoContent
|
:<|> "test" :> QueryParams "params" Int :> ReqBody '[JSON] String :> Put '[JSON] NoContent
|
||||||
:<|> "test" :> Capture "id" Int :> Delete '[JSON] NoContent
|
:<|> "test" :> Capture "id" Int :> Delete '[JSON] NoContent
|
||||||
:<|> "test" :> CaptureAll "ids" Int :> Get '[JSON] [Int]
|
:<|> "test" :> CaptureAll "ids" Int :> Get '[JSON] [Int]
|
||||||
|
@ -82,9 +94,9 @@ testApi = listFromAPI (Proxy :: Proxy LangX) (Proxy :: Proxy String) (Proxy :: P
|
||||||
listFromAPISpec :: Spec
|
listFromAPISpec :: Spec
|
||||||
listFromAPISpec = describe "listFromAPI" $ do
|
listFromAPISpec = describe "listFromAPI" $ do
|
||||||
it "generates 5 endpoints for TestApi" $ do
|
it "generates 5 endpoints for TestApi" $ do
|
||||||
length testApi `shouldBe` 5
|
length testApi `shouldBe` 6
|
||||||
|
|
||||||
let [getReq, postReq, putReq, deleteReq, captureAllReq] = testApi
|
let [getReq, postReq, contactReq, putReq, deleteReq, captureAllReq] = testApi
|
||||||
|
|
||||||
it "collects all info for get request" $ do
|
it "collects all info for get request" $ do
|
||||||
shouldBe getReq $ defReq
|
shouldBe getReq $ defReq
|
||||||
|
@ -110,6 +122,17 @@ listFromAPISpec = describe "listFromAPI" $ do
|
||||||
, _reqFuncName = FunctionName ["post", "test"]
|
, _reqFuncName = FunctionName ["post", "test"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "collects all info for a queryparamform" $ do
|
||||||
|
shouldBe contactReq $ defReq
|
||||||
|
{ _reqUrl = Url
|
||||||
|
[ Segment $ Static "test" ]
|
||||||
|
[ QueryArg (Arg "" "maybe contactFormX") Form ]
|
||||||
|
, _reqMethod = "POST"
|
||||||
|
, _reqHeaders = []
|
||||||
|
, _reqReturnType = Just "voidX"
|
||||||
|
, _reqFuncName = FunctionName ["post", "test"]
|
||||||
|
}
|
||||||
|
|
||||||
it "collects all info for put request" $ do
|
it "collects all info for put request" $ do
|
||||||
shouldBe putReq $ defReq
|
shouldBe putReq $ defReq
|
||||||
{ _reqUrl = Url
|
{ _reqUrl = Url
|
||||||
|
@ -148,3 +171,4 @@ listFromAPISpec = describe "listFromAPI" $ do
|
||||||
, _reqReturnType = Just "listX of intX"
|
, _reqReturnType = Just "listX of intX"
|
||||||
, _reqFuncName = FunctionName ["get", "test", "by", "ids"]
|
, _reqFuncName = FunctionName ["get", "test", "by", "ids"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ import Control.Concurrent
|
||||||
import Control.DeepSeq
|
import Control.DeepSeq
|
||||||
(NFData (..))
|
(NFData (..))
|
||||||
import Control.Exception
|
import Control.Exception
|
||||||
(bracket, fromException, IOException)
|
(IOException, bracket, fromException)
|
||||||
import Control.Monad.Error.Class
|
import Control.Monad.Error.Class
|
||||||
(throwError)
|
(throwError)
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
|
@ -48,9 +48,9 @@ import Data.Semigroup
|
||||||
((<>))
|
((<>))
|
||||||
import GHC.Generics
|
import GHC.Generics
|
||||||
(Generic)
|
(Generic)
|
||||||
import qualified Network.HTTP.Types as HTTP
|
import qualified Network.HTTP.Types as HTTP
|
||||||
import Network.Socket
|
import Network.Socket
|
||||||
import qualified Network.Wai as Wai
|
import qualified Network.Wai as Wai
|
||||||
import Network.Wai.Handler.Warp
|
import Network.Wai.Handler.Warp
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.QuickCheck
|
import Test.Hspec.QuickCheck
|
||||||
|
@ -64,9 +64,10 @@ import Servant.API
|
||||||
BasicAuthData (..), Capture, CaptureAll, Delete,
|
BasicAuthData (..), Capture, CaptureAll, Delete,
|
||||||
DeleteNoContent, EmptyAPI, FormUrlEncoded, Get, Header,
|
DeleteNoContent, EmptyAPI, FormUrlEncoded, Get, Header,
|
||||||
Headers, JSON, NoContent (NoContent), Post, Put, QueryFlag,
|
Headers, JSON, NoContent (NoContent), Post, Put, QueryFlag,
|
||||||
QueryParam, QueryParams, Raw, ReqBody, addHeader, getHeaders)
|
QueryParam, QueryParamForm, QueryParams, Raw, ReqBody,
|
||||||
import qualified Servant.Client.Core.Auth as Auth
|
addHeader, getHeaders)
|
||||||
import qualified Servant.Client.Core.Request as Req
|
import qualified Servant.Client.Core.Auth as Auth
|
||||||
|
import qualified Servant.Client.Core.Request as Req
|
||||||
import Servant.HttpStreams
|
import Servant.HttpStreams
|
||||||
import Servant.Server
|
import Servant.Server
|
||||||
import Servant.Server.Experimental.Auth
|
import Servant.Server.Experimental.Auth
|
||||||
|
@ -110,6 +111,14 @@ alice = Person "Alice" 42
|
||||||
carol :: Person
|
carol :: Person
|
||||||
carol = Person "Carol" 17
|
carol = Person "Carol" 17
|
||||||
|
|
||||||
|
data PersonSearch = PersonSearch
|
||||||
|
{ nameStartsWith :: String
|
||||||
|
, ageGreaterThan :: Integer
|
||||||
|
} deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
|
instance ToForm PersonSearch
|
||||||
|
instance FromForm PersonSearch
|
||||||
|
|
||||||
type TestHeaders = '[Header "X-Example1" Int, Header "X-Example2" String]
|
type TestHeaders = '[Header "X-Example1" Int, Header "X-Example2" String]
|
||||||
|
|
||||||
type Api =
|
type Api =
|
||||||
|
@ -121,6 +130,7 @@ type Api =
|
||||||
:<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person
|
:<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person
|
||||||
:<|> "param" :> QueryParam "name" String :> Get '[FormUrlEncoded,JSON] Person
|
:<|> "param" :> QueryParam "name" String :> Get '[FormUrlEncoded,JSON] Person
|
||||||
:<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]
|
:<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]
|
||||||
|
:<|> "paramform" :> QueryParamForm "names" PersonSearch :> Get '[JSON] [Person]
|
||||||
:<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool
|
:<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool
|
||||||
:<|> "rawSuccess" :> Raw
|
:<|> "rawSuccess" :> Raw
|
||||||
:<|> "rawFailure" :> Raw
|
:<|> "rawFailure" :> Raw
|
||||||
|
@ -146,6 +156,7 @@ getCaptureAll :: [String] -> ClientM [Person]
|
||||||
getBody :: Person -> ClientM Person
|
getBody :: Person -> ClientM Person
|
||||||
getQueryParam :: Maybe String -> ClientM Person
|
getQueryParam :: Maybe String -> ClientM Person
|
||||||
getQueryParams :: [String] -> ClientM [Person]
|
getQueryParams :: [String] -> ClientM [Person]
|
||||||
|
getQueryParamForm :: Maybe PersonSearch -> ClientM [Person]
|
||||||
getQueryFlag :: Bool -> ClientM Bool
|
getQueryFlag :: Bool -> ClientM Bool
|
||||||
getRawSuccess :: HTTP.Method -> ClientM Response
|
getRawSuccess :: HTTP.Method -> ClientM Response
|
||||||
getRawFailure :: HTTP.Method -> ClientM Response
|
getRawFailure :: HTTP.Method -> ClientM Response
|
||||||
|
@ -163,6 +174,7 @@ getRoot
|
||||||
:<|> getBody
|
:<|> getBody
|
||||||
:<|> getQueryParam
|
:<|> getQueryParam
|
||||||
:<|> getQueryParams
|
:<|> getQueryParams
|
||||||
|
:<|> getQueryParamForm
|
||||||
:<|> getQueryFlag
|
:<|> getQueryFlag
|
||||||
:<|> getRawSuccess
|
:<|> getRawSuccess
|
||||||
:<|> getRawFailure
|
:<|> getRawFailure
|
||||||
|
@ -185,6 +197,10 @@ server = serve api (
|
||||||
Just n -> throwError $ ServerError 400 (n ++ " not found") "" []
|
Just n -> throwError $ ServerError 400 (n ++ " not found") "" []
|
||||||
Nothing -> throwError $ ServerError 400 "missing parameter" "" [])
|
Nothing -> throwError $ ServerError 400 "missing parameter" "" [])
|
||||||
:<|> (\ names -> return (zipWith Person names [0..]))
|
:<|> (\ names -> return (zipWith Person names [0..]))
|
||||||
|
:<|> (\ psearch -> case psearch of
|
||||||
|
Just (Right psearch) -> return [alice, carol]
|
||||||
|
Just (Left err) -> throwError $ ServerError 400 "failed to decode form" "" []
|
||||||
|
Nothing -> return [])
|
||||||
:<|> return
|
:<|> return
|
||||||
:<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "rawSuccess")
|
:<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "rawSuccess")
|
||||||
:<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.badRequest400 [] "rawFailure")
|
:<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.badRequest400 [] "rawFailure")
|
||||||
|
@ -305,6 +321,11 @@ sucessSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
|
||||||
left show <$> runClient (getQueryParams ["alice", "bob"]) baseUrl
|
left show <$> runClient (getQueryParams ["alice", "bob"]) baseUrl
|
||||||
`shouldReturn` Right [Person "alice" 0, Person "bob" 1]
|
`shouldReturn` Right [Person "alice" 0, Person "bob" 1]
|
||||||
|
|
||||||
|
it "Servant.API.QueryParam.QueryParamForm" $ \(_, baseUrl) -> do
|
||||||
|
left show <$> runClient (getQueryParamForm Nothing) baseUrl `shouldReturn` Right []
|
||||||
|
left show <$> runClient (getQueryParamForm (Just $ PersonSearch "a" 10)) baseUrl
|
||||||
|
`shouldReturn` Right [alice, carol]
|
||||||
|
|
||||||
context "Servant.API.QueryParam.QueryFlag" $
|
context "Servant.API.QueryParam.QueryFlag" $
|
||||||
forM_ [False, True] $ \ flag -> it (show flag) $ \(_, baseUrl) -> do
|
forM_ [False, True] $ \ flag -> it (show flag) $ \(_, baseUrl) -> do
|
||||||
left show <$> runClient (getQueryFlag flag) baseUrl `shouldReturn` Right flag
|
left show <$> runClient (getQueryFlag flag) baseUrl `shouldReturn` Right flag
|
||||||
|
|
|
@ -148,6 +148,7 @@ test-suite spec
|
||||||
, base-compat
|
, base-compat
|
||||||
, base64-bytestring
|
, base64-bytestring
|
||||||
, bytestring
|
, bytestring
|
||||||
|
, http-api-data
|
||||||
, http-types
|
, http-types
|
||||||
, mtl
|
, mtl
|
||||||
, resourcet
|
, resourcet
|
||||||
|
|
|
@ -72,7 +72,7 @@ import Servant.API
|
||||||
((:<|>) (..), (:>), Accept (..), BasicAuth, Capture',
|
((:<|>) (..), (:>), Accept (..), BasicAuth, Capture',
|
||||||
CaptureAll, Description, EmptyAPI, FramingRender (..),
|
CaptureAll, Description, EmptyAPI, FramingRender (..),
|
||||||
FramingUnrender (..), FromSourceIO (..), Header', If,
|
FramingUnrender (..), FromSourceIO (..), Header', If,
|
||||||
IsSecure (..), QueryFlag, QueryParam', QueryParams, Raw,
|
IsSecure (..), QueryFlag, QueryParam', QueryParamForm', QueryParams, Raw,
|
||||||
ReflectMethod (reflectMethod), RemoteHost, ReqBody',
|
ReflectMethod (reflectMethod), RemoteHost, ReqBody',
|
||||||
SBool (..), SBoolI (..), SourceIO, Stream, StreamBody',
|
SBool (..), SBoolI (..), SourceIO, Stream, StreamBody',
|
||||||
Summary, ToSourceIO (..), Vault, Verb, NoContentVerb,
|
Summary, ToSourceIO (..), Vault, Verb, NoContentVerb,
|
||||||
|
@ -86,6 +86,8 @@ import Servant.API.Modifiers
|
||||||
unfoldRequestArgument)
|
unfoldRequestArgument)
|
||||||
import Servant.API.ResponseHeaders
|
import Servant.API.ResponseHeaders
|
||||||
(GetHeaders, Headers, getHeaders, getResponse)
|
(GetHeaders, Headers, getHeaders, getResponse)
|
||||||
|
import Web.FormUrlEncoded
|
||||||
|
(FromForm(..), urlDecodeAsForm)
|
||||||
import qualified Servant.Types.SourceT as S
|
import qualified Servant.Types.SourceT as S
|
||||||
import Web.HttpApiData
|
import Web.HttpApiData
|
||||||
(FromHttpApiData, parseHeader, parseQueryParam,
|
(FromHttpApiData, parseHeader, parseQueryParam,
|
||||||
|
@ -556,6 +558,73 @@ instance (KnownSymbol sym, HasServer api context)
|
||||||
examine v | v == "true" || v == "1" || v == "" = True
|
examine v | v == "true" || v == "1" || v == "" = True
|
||||||
| otherwise = False
|
| otherwise = False
|
||||||
|
|
||||||
|
-- | If you define a custom record type, for example @BookSearchParams@, then you can use
|
||||||
|
-- @'QueryParamForm' "formName" BookSearchParams@ in one of the endpoints for your API
|
||||||
|
-- to translate a collection of query-string parameters into a value of your record type.
|
||||||
|
--
|
||||||
|
-- Your server-side handler must be a function that takes an argument of type
|
||||||
|
-- @'Maybe' ('Either' BookSearchParams)@.
|
||||||
|
--
|
||||||
|
-- You can control how the individual values are converted from the query string
|
||||||
|
-- into a value of your type by simply providing an instance of 'FromForm' for your type.
|
||||||
|
-- All of the record's values utilize 'FromHttpApiData'.
|
||||||
|
--
|
||||||
|
-- Note: anytime you use a 'QueryParamForm', your server will assume it's present
|
||||||
|
-- if the query-string is non-empty. This modifier does not check if any specific
|
||||||
|
-- keys from the record are present: it just attempts to 'urlDecodeAsForm' the whole query
|
||||||
|
-- string if any query-string parameters have been provided.
|
||||||
|
--
|
||||||
|
-- Example:
|
||||||
|
--
|
||||||
|
-- > data BookSearchParams = BookSearchParams
|
||||||
|
-- > { title :: Text
|
||||||
|
-- > , authors :: [Text]
|
||||||
|
-- > , page :: Maybe Int
|
||||||
|
-- > } deriving (Eq, Show, Generic)
|
||||||
|
-- > instance FromForm BookSearchParams
|
||||||
|
-- > type MyApi = "books" :> QueryParamForm "searchQ" BookSearchParams :> Get '[JSON] [Book]
|
||||||
|
--
|
||||||
|
-- Example Handler Signature:
|
||||||
|
-- Maybe (Either Text BookSearchParams) -> Handler [Book]
|
||||||
|
instance
|
||||||
|
( KnownSymbol sym, FromForm a, HasServer api context
|
||||||
|
, SBoolI (FoldRequired mods), SBoolI (FoldLenient mods)
|
||||||
|
)
|
||||||
|
=> HasServer (QueryParamForm' mods sym a :> api) context where
|
||||||
|
------
|
||||||
|
type ServerT (QueryParamForm' mods sym a :> api) m =
|
||||||
|
RequestArgument mods a -> ServerT api m
|
||||||
|
|
||||||
|
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
|
||||||
|
|
||||||
|
route Proxy context subserver =
|
||||||
|
|
||||||
|
let formName = cs $ symbolVal (Proxy :: Proxy sym)
|
||||||
|
|
||||||
|
parseParamForm req =
|
||||||
|
unfoldRequestArgument (Proxy :: Proxy mods) errReq errSt mev
|
||||||
|
where
|
||||||
|
rawQS = rawQueryString req
|
||||||
|
|
||||||
|
mev :: Maybe (Either T.Text a)
|
||||||
|
mev = case B.length rawQS of
|
||||||
|
0 -> Nothing
|
||||||
|
_ -> Just $ urlDecodeAsForm (BL.drop 1 . BL.fromStrict $ rawQS)
|
||||||
|
|
||||||
|
errReq = delayedFailFatal err400
|
||||||
|
{ errBody = cs $ "Query parameter form " <> formName <> " is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
errSt e = delayedFailFatal err400
|
||||||
|
{ errBody = cs $ "Error parsing query parameter form "
|
||||||
|
<> formName <> " failed: " <> e
|
||||||
|
}
|
||||||
|
|
||||||
|
delayed = addParameterCheck subserver . withRequest $ \req ->
|
||||||
|
parseParamForm req
|
||||||
|
|
||||||
|
in route (Proxy :: Proxy api) context delayed
|
||||||
|
|
||||||
-- | Just pass the request to the underlying application and serve its response.
|
-- | Just pass the request to the underlying application and serve its response.
|
||||||
--
|
--
|
||||||
-- Example:
|
-- Example:
|
||||||
|
|
|
@ -49,7 +49,7 @@ import Servant.API
|
||||||
BasicAuthData (BasicAuthData), Capture, Capture', CaptureAll, Lenient, Strict, Delete,
|
BasicAuthData (BasicAuthData), Capture, Capture', CaptureAll, Lenient, Strict, Delete,
|
||||||
EmptyAPI, Get, Header, Headers, HttpVersion, IsSecure (..),
|
EmptyAPI, Get, Header, Headers, HttpVersion, IsSecure (..),
|
||||||
JSON, NoContent (..), NoFraming, OctetStream, Patch,
|
JSON, NoContent (..), NoFraming, OctetStream, Patch,
|
||||||
PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw,
|
PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, QueryParamForm, Raw,
|
||||||
RemoteHost, ReqBody, SourceIO, StdMethod (..), Stream, Verb,
|
RemoteHost, ReqBody, SourceIO, StdMethod (..), Stream, Verb,
|
||||||
NoContentVerb, addHeader)
|
NoContentVerb, addHeader)
|
||||||
import Servant.Server
|
import Servant.Server
|
||||||
|
@ -63,6 +63,8 @@ import Test.Hspec.Wai
|
||||||
(get, liftIO, matchHeaders, matchStatus, shouldRespondWith,
|
(get, liftIO, matchHeaders, matchStatus, shouldRespondWith,
|
||||||
with, (<:>))
|
with, (<:>))
|
||||||
import qualified Test.Hspec.Wai as THW
|
import qualified Test.Hspec.Wai as THW
|
||||||
|
import Web.FormUrlEncoded
|
||||||
|
(FromForm)
|
||||||
|
|
||||||
import Servant.Server.Experimental.Auth
|
import Servant.Server.Experimental.Auth
|
||||||
(AuthHandler, AuthServerData, mkAuthHandler)
|
(AuthHandler, AuthServerData, mkAuthHandler)
|
||||||
|
@ -88,6 +90,7 @@ spec = do
|
||||||
captureSpec
|
captureSpec
|
||||||
captureAllSpec
|
captureAllSpec
|
||||||
queryParamSpec
|
queryParamSpec
|
||||||
|
queryParamFormSpec
|
||||||
reqBodySpec
|
reqBodySpec
|
||||||
headerSpec
|
headerSpec
|
||||||
rawSpec
|
rawSpec
|
||||||
|
@ -451,6 +454,124 @@ queryParamSpec = do
|
||||||
name = "Alice"
|
name = "Alice"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- * queryParamFormSpec {{{
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
data AnimalSearch = AnimalSearch {
|
||||||
|
sName :: String
|
||||||
|
, sLegs :: Integer
|
||||||
|
} deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
|
instance FromForm AnimalSearch
|
||||||
|
|
||||||
|
type QueryParamFormApi =
|
||||||
|
QueryParamForm "octopus" AnimalSearch :> Get '[JSON] Animal
|
||||||
|
:<|> "before-param"
|
||||||
|
:> QueryParam "before" Bool
|
||||||
|
:> QueryParamForm "before" AnimalSearch
|
||||||
|
:> Get '[JSON] Animal
|
||||||
|
:<|> "mixed-param"
|
||||||
|
:> QueryParam "before" Bool
|
||||||
|
:> QueryParamForm "multiple" AnimalSearch
|
||||||
|
:> QueryParam "after" Bool
|
||||||
|
:> Get '[JSON] Animal
|
||||||
|
|
||||||
|
queryParamFormApi :: Proxy QueryParamFormApi
|
||||||
|
queryParamFormApi = Proxy
|
||||||
|
|
||||||
|
qpFormServer :: Server QueryParamFormApi
|
||||||
|
qpFormServer = searchAnimal :<|> searchWithBeforeParms :<|> searchWithAroundParms
|
||||||
|
|
||||||
|
where searchAnimal (Just (Right search)) = return $ Animal { species = sName search, numberOfLegs = sLegs search}
|
||||||
|
searchAnimal (Just (Left _)) = return $ Animal { species = "broken", numberOfLegs = 0}
|
||||||
|
searchAnimal Nothing = return bimac
|
||||||
|
|
||||||
|
searchWithBeforeParms (Just _) (Just (Right search)) = return $ Animal { species = sName search, numberOfLegs = sLegs search}
|
||||||
|
searchWithBeforeParms _ _ = return bimac
|
||||||
|
|
||||||
|
searchWithAroundParms (Just _) (Just (Right search)) (Just True) = return $ Animal { species = sName search, numberOfLegs = sLegs search}
|
||||||
|
searchWithAroundParms _ _ _ = return bimac
|
||||||
|
|
||||||
|
|
||||||
|
queryParamFormSpec :: Spec
|
||||||
|
queryParamFormSpec = do
|
||||||
|
describe "Servant.API.QueryParamForm" $ do
|
||||||
|
it "allows query params into form" $
|
||||||
|
(flip runSession) (serve queryParamFormApi qpFormServer) $ do
|
||||||
|
let params1 = "?sName=bimac&sLegs=7"
|
||||||
|
response1 <- Network.Wai.Test.request defaultRequest{
|
||||||
|
rawQueryString = params1,
|
||||||
|
queryString = parseQuery params1
|
||||||
|
}
|
||||||
|
liftIO $ do
|
||||||
|
decode' (simpleBody response1) `shouldBe` (Just $ Animal { species = "bimac", numberOfLegs = 7})
|
||||||
|
it "allows no query params at all" $
|
||||||
|
(flip runSession) (serve queryParamFormApi qpFormServer) $ do
|
||||||
|
response1 <- Network.Wai.Test.request defaultRequest
|
||||||
|
liftIO $ do
|
||||||
|
decode' (simpleBody response1) `shouldBe` Just bimac
|
||||||
|
it "does not generate an error for incomplete form" $
|
||||||
|
(flip runSession) (serve queryParamFormApi qpFormServer) $ do
|
||||||
|
let paramsBork = "?sName=bimac"
|
||||||
|
responseBork <- Network.Wai.Test.request defaultRequest{
|
||||||
|
rawQueryString = paramsBork,
|
||||||
|
queryString = parseQuery paramsBork
|
||||||
|
}
|
||||||
|
liftIO $ do
|
||||||
|
decode' (simpleBody responseBork) `shouldBe` (Just $ Animal { species = "broken", numberOfLegs = 0})
|
||||||
|
return ()
|
||||||
|
it "does not generate an error for duplicated keys" $
|
||||||
|
(flip runSession) (serve queryParamFormApi qpFormServer) $ do
|
||||||
|
let paramsBork = "?sName=bimac&sName=dup&sLegs=12"
|
||||||
|
responseBork <- Network.Wai.Test.request defaultRequest{
|
||||||
|
rawQueryString = paramsBork,
|
||||||
|
queryString = parseQuery paramsBork
|
||||||
|
}
|
||||||
|
liftIO $ do
|
||||||
|
decode' (simpleBody responseBork) `shouldBe` (Just $ Animal { species = "broken", numberOfLegs = 0})
|
||||||
|
return ()
|
||||||
|
it "does not generate an error for form with bad input types" $
|
||||||
|
(flip runSession) (serve queryParamFormApi qpFormServer) $ do
|
||||||
|
let paramsBork = "?sName=bimac&sLegs=ocean"
|
||||||
|
responseBork <- Network.Wai.Test.request defaultRequest{
|
||||||
|
rawQueryString = paramsBork,
|
||||||
|
queryString = parseQuery paramsBork
|
||||||
|
}
|
||||||
|
liftIO $ do
|
||||||
|
decode' (simpleBody responseBork) `shouldBe` (Just $ Animal { species = "broken", numberOfLegs = 0})
|
||||||
|
return ()
|
||||||
|
|
||||||
|
it "allows query params into form even with other params" $
|
||||||
|
(flip runSession) (serve queryParamFormApi qpFormServer) $ do
|
||||||
|
let params1 = "?before=true&sName=bimac&sLegs=6"
|
||||||
|
response1 <- Network.Wai.Test.request defaultRequest{
|
||||||
|
rawQueryString = params1,
|
||||||
|
queryString = parseQuery params1,
|
||||||
|
pathInfo = ["before-param"]
|
||||||
|
}
|
||||||
|
liftIO $ do
|
||||||
|
decode' (simpleBody response1) `shouldBe` (Just $ Animal { species = "bimac", numberOfLegs = 6})
|
||||||
|
|
||||||
|
let params2 = "?sName=bimac&before=true&sLegs=5"
|
||||||
|
response2 <- Network.Wai.Test.request defaultRequest{
|
||||||
|
rawQueryString = params2,
|
||||||
|
queryString = parseQuery params2,
|
||||||
|
pathInfo = ["before-param"]
|
||||||
|
}
|
||||||
|
liftIO $ do
|
||||||
|
decode' (simpleBody response2) `shouldBe` (Just $ Animal { species = "bimac", numberOfLegs = 5})
|
||||||
|
it "allows completely mixed up params with QueryParamForm" $
|
||||||
|
(flip runSession) (serve queryParamFormApi qpFormServer) $ do
|
||||||
|
let params1 = "?sLegs=1&before=true&sName=bimac&after=true&unknown=ignoreThis"
|
||||||
|
response1 <- Network.Wai.Test.request defaultRequest{
|
||||||
|
rawQueryString = params1,
|
||||||
|
queryString = parseQuery params1,
|
||||||
|
pathInfo = ["mixed-param"]
|
||||||
|
}
|
||||||
|
liftIO $ do
|
||||||
|
decode' (simpleBody response1) `shouldBe` (Just $ Animal { species = "bimac", numberOfLegs = 1})
|
||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
-- * reqBodySpec {{{
|
-- * reqBodySpec {{{
|
||||||
|
@ -811,4 +932,8 @@ chimera = Animal "Chimera" (-1)
|
||||||
|
|
||||||
beholder :: Animal
|
beholder :: Animal
|
||||||
beholder = Animal "Beholder" 0
|
beholder = Animal "Beholder" 0
|
||||||
|
|
||||||
|
bimac :: Animal
|
||||||
|
bimac = Animal { species = "Octopus bimaculoides" , numberOfLegs = 8}
|
||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
|
@ -161,6 +161,7 @@ test-suite spec
|
||||||
, base-compat
|
, base-compat
|
||||||
, aeson
|
, aeson
|
||||||
, bytestring
|
, bytestring
|
||||||
|
, http-api-data
|
||||||
, http-media
|
, http-media
|
||||||
, mtl
|
, mtl
|
||||||
, servant
|
, servant
|
||||||
|
|
|
@ -101,7 +101,8 @@ import Servant.API.IsSecure
|
||||||
import Servant.API.Modifiers
|
import Servant.API.Modifiers
|
||||||
(Lenient, Optional, Required, Strict)
|
(Lenient, Optional, Required, Strict)
|
||||||
import Servant.API.QueryParam
|
import Servant.API.QueryParam
|
||||||
(QueryFlag, QueryParam, QueryParam', QueryParams)
|
(QueryFlag, QueryParam, QueryParam', QueryParams,
|
||||||
|
QueryParamForm, QueryParamForm')
|
||||||
import Servant.API.Raw
|
import Servant.API.Raw
|
||||||
(Raw)
|
(Raw)
|
||||||
import Servant.API.RemoteHost
|
import Servant.API.RemoteHost
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{-# LANGUAGE PolyKinds #-}
|
{-# LANGUAGE PolyKinds #-}
|
||||||
{-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE TypeOperators #-}
|
||||||
{-# OPTIONS_HADDOCK not-home #-}
|
{-# OPTIONS_HADDOCK not-home #-}
|
||||||
module Servant.API.QueryParam (QueryFlag, QueryParam, QueryParam', QueryParams) where
|
module Servant.API.QueryParam (QueryFlag, QueryParam, QueryParam', QueryParams, QueryParamForm, QueryParamForm') where
|
||||||
|
|
||||||
import Data.Typeable
|
import Data.Typeable
|
||||||
(Typeable)
|
(Typeable)
|
||||||
|
@ -38,6 +38,39 @@ data QueryParam' (mods :: [*]) (sym :: Symbol) (a :: *)
|
||||||
data QueryParams (sym :: Symbol) (a :: *)
|
data QueryParams (sym :: Symbol) (a :: *)
|
||||||
deriving Typeable
|
deriving Typeable
|
||||||
|
|
||||||
|
-- | Lookup the values associated with a collection of query string parameters
|
||||||
|
-- and try to extract them as a value of type @a@. This is typically
|
||||||
|
-- meant to query string parameters of the form
|
||||||
|
-- @param1=val1¶m2=val2@ and so on into a custom type represented by the form.
|
||||||
|
--
|
||||||
|
-- Note: Unlike with 'QueryParam', by default 'QueryParamForm' is parsed in a
|
||||||
|
-- 'Lenient' way, because it's difficult to know if it should be parsed
|
||||||
|
-- or not (when other 'QueryParam's are present). As a result, most users
|
||||||
|
-- of 'QueryParamForm' are going to implement handlers that take a value
|
||||||
|
-- of type (Maybe (Either Text a)). This also means that in a server implementation
|
||||||
|
-- if there as a query string of any length (even just a "?"), we'll try to parse
|
||||||
|
-- the 'QueryParamForm' into the custom type specified.
|
||||||
|
--
|
||||||
|
-- Example:
|
||||||
|
--
|
||||||
|
-- > data BookSearchParams = BookSearchParams
|
||||||
|
-- > { title :: Text
|
||||||
|
-- > , authors :: [Text]
|
||||||
|
-- > , page :: Maybe Int
|
||||||
|
-- > } deriving (Eq, Show, Generic)
|
||||||
|
-- > instance FromForm BookSearchParams
|
||||||
|
-- > type MyApi = "books" :> QueryParamForm "searchQ" BookSearchParams :> Get '[JSON] [Book]
|
||||||
|
--
|
||||||
|
-- Example Handler Signature:
|
||||||
|
-- Maybe (Either Text BookSearchParams) -> Handler [Book]
|
||||||
|
|
||||||
|
type QueryParamForm = QueryParamForm' '[Optional, Lenient]
|
||||||
|
|
||||||
|
-- | 'QueryParamForm' which can be 'Required', 'Lenient', or modified otherwise.
|
||||||
|
data QueryParamForm' (mods :: [*]) (sym :: Symbol) (a :: *)
|
||||||
|
deriving Typeable
|
||||||
|
|
||||||
|
|
||||||
-- | Lookup a potentially value-less query string parameter
|
-- | Lookup a potentially value-less query string parameter
|
||||||
-- with boolean semantics. If the param @sym@ is there without any value,
|
-- with boolean semantics. If the param @sym@ is there without any value,
|
||||||
-- or if it's there with value "true" or "1", it's interpreted as 'True'.
|
-- or if it's there with value "true" or "1", it's interpreted as 'True'.
|
||||||
|
|
|
@ -53,7 +53,7 @@ import Servant.API.Capture
|
||||||
import Servant.API.Header
|
import Servant.API.Header
|
||||||
(Header)
|
(Header)
|
||||||
import Servant.API.QueryParam
|
import Servant.API.QueryParam
|
||||||
(QueryFlag, QueryParam, QueryParams)
|
(QueryFlag, QueryParam, QueryParams, QueryParamForm)
|
||||||
import Servant.API.ReqBody
|
import Servant.API.ReqBody
|
||||||
(ReqBody)
|
(ReqBody)
|
||||||
import Servant.API.Sub
|
import Servant.API.Sub
|
||||||
|
@ -127,6 +127,7 @@ type family IsElem endpoint api :: Constraint where
|
||||||
= IsElem sa sb
|
= IsElem sa sb
|
||||||
IsElem sa (QueryParam x y :> sb) = IsElem sa sb
|
IsElem sa (QueryParam x y :> sb) = IsElem sa sb
|
||||||
IsElem sa (QueryParams x y :> sb) = IsElem sa sb
|
IsElem sa (QueryParams x y :> sb) = IsElem sa sb
|
||||||
|
IsElem sa (QueryParamForm x :> sb) = IsElem sa sb
|
||||||
IsElem sa (QueryFlag x :> sb) = IsElem sa sb
|
IsElem sa (QueryFlag x :> sb) = IsElem sa sb
|
||||||
IsElem (Verb m s ct typ) (Verb m s ct' typ)
|
IsElem (Verb m s ct typ) (Verb m s ct' typ)
|
||||||
= IsSubList ct ct'
|
= IsSubList ct ct'
|
||||||
|
|
|
@ -122,6 +122,8 @@ module Servant.Links (
|
||||||
, linkQueryParams
|
, linkQueryParams
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as LBSC
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Proxy
|
import Data.Proxy
|
||||||
(Proxy (..))
|
(Proxy (..))
|
||||||
|
@ -139,6 +141,8 @@ import Network.URI
|
||||||
(URI (..), escapeURIString, isUnreserved)
|
(URI (..), escapeURIString, isUnreserved)
|
||||||
import Prelude ()
|
import Prelude ()
|
||||||
import Prelude.Compat
|
import Prelude.Compat
|
||||||
|
import Web.FormUrlEncoded
|
||||||
|
(ToForm(..), urlEncodeAsFormStable)
|
||||||
|
|
||||||
import Servant.API.Alternative
|
import Servant.API.Alternative
|
||||||
((:<|>) ((:<|>)))
|
((:<|>) ((:<|>)))
|
||||||
|
@ -162,7 +166,7 @@ import Servant.API.IsSecure
|
||||||
import Servant.API.Modifiers
|
import Servant.API.Modifiers
|
||||||
(FoldRequired)
|
(FoldRequired)
|
||||||
import Servant.API.QueryParam
|
import Servant.API.QueryParam
|
||||||
(QueryFlag, QueryParam', QueryParams)
|
(QueryFlag, QueryParam', QueryParams, QueryParamForm')
|
||||||
import Servant.API.Raw
|
import Servant.API.Raw
|
||||||
(Raw)
|
(Raw)
|
||||||
import Servant.API.RemoteHost
|
import Servant.API.RemoteHost
|
||||||
|
@ -219,6 +223,7 @@ data Param
|
||||||
= SingleParam String Text.Text
|
= SingleParam String Text.Text
|
||||||
| ArrayElemParam String Text.Text
|
| ArrayElemParam String Text.Text
|
||||||
| FlagParam String
|
| FlagParam String
|
||||||
|
| FormParam LBS.ByteString
|
||||||
deriving Show
|
deriving Show
|
||||||
|
|
||||||
addSegment :: Escaped -> Link -> Link
|
addSegment :: Escaped -> Link -> Link
|
||||||
|
@ -284,6 +289,7 @@ linkURI' addBrackets (Link segments q_params) =
|
||||||
makeQuery (ArrayElemParam k v) = escape k <> style <> escape (Text.unpack v)
|
makeQuery (ArrayElemParam k v) = escape k <> style <> escape (Text.unpack v)
|
||||||
makeQuery (SingleParam k v) = escape k <> "=" <> escape (Text.unpack v)
|
makeQuery (SingleParam k v) = escape k <> "=" <> escape (Text.unpack v)
|
||||||
makeQuery (FlagParam k) = escape k
|
makeQuery (FlagParam k) = escape k
|
||||||
|
makeQuery (FormParam f) = LBSC.unpack f
|
||||||
|
|
||||||
style = case addBrackets of
|
style = case addBrackets of
|
||||||
LinkArrayElementBracket -> "[]="
|
LinkArrayElementBracket -> "[]="
|
||||||
|
@ -472,6 +478,16 @@ instance (KnownSymbol sym, HasLink sub)
|
||||||
where
|
where
|
||||||
k = symbolVal (Proxy :: Proxy sym)
|
k = symbolVal (Proxy :: Proxy sym)
|
||||||
|
|
||||||
|
instance (KnownSymbol sym, ToForm v, HasLink sub, SBoolI (FoldRequired mods))
|
||||||
|
=> HasLink (QueryParamForm' mods sym v :> sub)
|
||||||
|
where
|
||||||
|
type MkLink (QueryParamForm' mods sym v :> sub) a = If (FoldRequired mods) v (Maybe v) -> MkLink sub a
|
||||||
|
toLink toA _ l mv =
|
||||||
|
toLink toA (Proxy :: Proxy sub) $
|
||||||
|
case sbool :: SBool (FoldRequired mods) of
|
||||||
|
STrue -> (addQueryParam . FormParam . urlEncodeAsFormStable) mv l
|
||||||
|
SFalse -> maybe id (addQueryParam . FormParam . urlEncodeAsFormStable) mv l
|
||||||
|
|
||||||
-- :<|> instance - Generate all links at once
|
-- :<|> instance - Generate all links at once
|
||||||
instance (HasLink a, HasLink b) => HasLink (a :<|> b) where
|
instance (HasLink a, HasLink b) => HasLink (a :<|> b) where
|
||||||
type MkLink (a :<|> b) r = MkLink a r :<|> MkLink b r
|
type MkLink (a :<|> b) r = MkLink a r :<|> MkLink b r
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{-# LANGUAGE ConstraintKinds #-}
|
{-# LANGUAGE ConstraintKinds #-}
|
||||||
{-# LANGUAGE DataKinds #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE PolyKinds #-}
|
{-# LANGUAGE PolyKinds #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
@ -9,8 +10,11 @@ import Data.Proxy
|
||||||
(Proxy (..))
|
(Proxy (..))
|
||||||
import Data.String
|
import Data.String
|
||||||
(fromString)
|
(fromString)
|
||||||
|
import GHC.Generics
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
(Expectation, Spec, describe, it, shouldBe)
|
(Expectation, Spec, describe, it, shouldBe)
|
||||||
|
import Web.FormUrlEncoded
|
||||||
|
(ToForm(..))
|
||||||
|
|
||||||
import Servant.API
|
import Servant.API
|
||||||
import Servant.Test.ComprehensiveAPI
|
import Servant.Test.ComprehensiveAPI
|
||||||
|
@ -21,6 +25,8 @@ type TestApi =
|
||||||
-- Capture and query params
|
-- Capture and query params
|
||||||
"hello" :> Capture "name" String :> QueryParam "capital" Bool :> Delete '[JSON] NoContent
|
"hello" :> Capture "name" String :> QueryParam "capital" Bool :> Delete '[JSON] NoContent
|
||||||
:<|> "hi" :> Capture "name" String :> QueryParam' '[Required] "capital" Bool :> Delete '[JSON] NoContent
|
:<|> "hi" :> Capture "name" String :> QueryParam' '[Required] "capital" Bool :> Delete '[JSON] NoContent
|
||||||
|
:<|> "formR" :> QueryParamForm' '[Required, Strict] "someform" TestForm :> Delete '[JSON] NoContent
|
||||||
|
:<|> "form-opt" :> QueryParamForm "someform" TestForm :> Delete '[JSON] NoContent
|
||||||
:<|> "all" :> CaptureAll "names" String :> Get '[JSON] NoContent
|
:<|> "all" :> CaptureAll "names" String :> Get '[JSON] NoContent
|
||||||
|
|
||||||
-- Flags
|
-- Flags
|
||||||
|
@ -43,6 +49,13 @@ apiLink :: (IsElem endpoint TestApi, HasLink endpoint)
|
||||||
=> Proxy endpoint -> MkLink endpoint Link
|
=> Proxy endpoint -> MkLink endpoint Link
|
||||||
apiLink = safeLink (Proxy :: Proxy TestApi)
|
apiLink = safeLink (Proxy :: Proxy TestApi)
|
||||||
|
|
||||||
|
data TestForm = TestForm {
|
||||||
|
testing :: String
|
||||||
|
, time :: String
|
||||||
|
} deriving (Eq, Generic)
|
||||||
|
|
||||||
|
instance ToForm TestForm
|
||||||
|
|
||||||
-- | Convert a link to a URI and ensure that this maps to the given string
|
-- | Convert a link to a URI and ensure that this maps to the given string
|
||||||
-- given string
|
-- given string
|
||||||
shouldBeLink :: Link -> String -> Expectation
|
shouldBeLink :: Link -> String -> Expectation
|
||||||
|
@ -65,6 +78,17 @@ spec = describe "Servant.Links" $ do
|
||||||
:> Delete '[JSON] NoContent)
|
:> Delete '[JSON] NoContent)
|
||||||
apiLink l4 "privet" False `shouldBeLink` "hi/privet?capital=false"
|
apiLink l4 "privet" False `shouldBeLink` "hi/privet?capital=false"
|
||||||
|
|
||||||
|
it "generates query param form links" $ do
|
||||||
|
-- most who use QueryParamForm are not going to use it Required, Strict, so we'll test it both ways
|
||||||
|
let l3 = Proxy :: Proxy ("formR" :> QueryParamForm' '[Required, Strict] "someform" TestForm
|
||||||
|
:> Delete '[JSON] NoContent)
|
||||||
|
-- We allow `urlEncodeAsFormStable` to uri Escape for us. Validating that assumption here:
|
||||||
|
apiLink l3 (TestForm "sure" "später") `shouldBeLink` "formR?testing=sure&time=sp%C3%A4ter"
|
||||||
|
|
||||||
|
let l4 = Proxy :: Proxy ("form-opt" :> QueryParamForm "someform" TestForm
|
||||||
|
:> Delete '[JSON] NoContent)
|
||||||
|
apiLink l4 (Just $ TestForm "sure" "später") `shouldBeLink` "form-opt?testing=sure&time=sp%C3%A4ter"
|
||||||
|
|
||||||
it "generates correct links for CaptureAll" $ do
|
it "generates correct links for CaptureAll" $ do
|
||||||
apiLink (Proxy :: Proxy ("all" :> CaptureAll "names" String :> Get '[JSON] NoContent))
|
apiLink (Proxy :: Proxy ("all" :> CaptureAll "names" String :> Get '[JSON] NoContent))
|
||||||
["roads", "lead", "to", "rome"]
|
["roads", "lead", "to", "rome"]
|
||||||
|
|
Loading…
Reference in a new issue