Add URI fragment as a separate combinator (#1324)

This commit is contained in:
Andrey Prokopenko 2020-11-18 21:57:20 +03:00 committed by GitHub
parent 339eec6a90
commit da0c83d318
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 418 additions and 120 deletions

View file

@ -1,4 +1,5 @@
{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
@ -13,6 +14,10 @@
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
#if MIN_VERSION_base(4,9,0) && __GLASGOW_HASKELL__ >= 802
#define HAS_TYPE_ERROR
#endif
module Servant.Client.Core.HasClient ( module Servant.Client.Core.HasClient (
clientIn, clientIn,
HasClient (..), HasClient (..),
@ -63,17 +68,18 @@ import qualified Network.HTTP.Types as H
import Servant.API import Servant.API
((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth, BasicAuthData, ((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth, BasicAuthData,
BuildHeadersTo (..), Capture', CaptureAll, Description, BuildHeadersTo (..), Capture', CaptureAll, Description,
EmptyAPI, FramingRender (..), FramingUnrender (..), EmptyAPI, Fragment, FramingRender (..), FramingUnrender (..),
FromSourceIO (..), Header', Headers (..), HttpVersion, FromSourceIO (..), Header', Headers (..), HttpVersion,
IsSecure, MimeRender (mimeRender), IsSecure, MimeRender (mimeRender),
MimeUnrender (mimeUnrender), NoContent (NoContent), QueryFlag, MimeUnrender (mimeUnrender), NoContent (NoContent),
QueryParam', QueryParams, Raw, ReflectMethod (..), RemoteHost, NoContentVerb, QueryFlag, QueryParam', QueryParams, Raw,
ReqBody', SBoolI, Stream, StreamBody', Summary, ToHttpApiData, ReflectMethod (..), RemoteHost, ReqBody', SBoolI, Stream,
ToSourceIO (..), Vault, Verb, NoContentVerb, WithNamedContext, StreamBody', Summary, ToHttpApiData, ToSourceIO (..), Vault,
contentType, getHeadersHList, getResponse, toQueryParam, Verb, WithNamedContext, contentType, getHeadersHList,
toUrlPiece) getResponse, toQueryParam, toUrlPiece)
import Servant.API.ContentTypes import Servant.API.ContentTypes
(contentTypes, AllMime (allMime), AllMimeUnrender (allMimeUnrender)) (contentTypes, AllMime (allMime), AllMimeUnrender (allMimeUnrender))
import Servant.API.TypeLevel (FragmentUnique, AtLeastOneFragment)
import Servant.API.Modifiers import Servant.API.Modifiers
(FoldRequired, RequiredArgument, foldRequiredArgument) (FoldRequired, RequiredArgument, foldRequiredArgument)
import Servant.API.UVerb import Servant.API.UVerb
@ -745,6 +751,34 @@ instance ( HasClient m api
hoistClientMonad pm _ f cl = \authreq -> hoistClientMonad pm _ f cl = \authreq ->
hoistClientMonad pm (Proxy :: Proxy api) f (cl authreq) hoistClientMonad pm (Proxy :: Proxy api) f (cl authreq)
-- | Ignore @'Fragment'@ in client functions.
-- See <https://ietf.org/rfc/rfc2616.html#section-15.1.3> for more details.
--
-- Example:
--
-- > type MyApi = "books" :> Fragment Text :> Get '[JSON] [Book]
-- >
-- > myApi :: Proxy MyApi
-- > myApi = Proxy
-- >
-- > getBooksBy :: Maybe Text -> ClientM [Book]
-- > getBooksBy = client myApi
-- > -- then you can just use "getBooksBy" to query that endpoint.
-- > -- 'getBooksBy Nothing' for all books
-- > -- 'getBooksBy (Just "Isaac Asimov")' to get all books by Isaac Asimov
#ifdef HAS_TYPE_ERROR
instance (AtLeastOneFragment api, FragmentUnique (Fragment a :> api), HasClient m api
#else
instance ( HasClient m api
#endif
) => HasClient m (Fragment a :> api) where
type Client m (Fragment a :> api) = Client m api
clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api)
hoistClientMonad pm _ = hoistClientMonad pm (Proxy :: Proxy api)
-- * Basic Authentication -- * Basic Authentication
instance HasClient m api => HasClient m (BasicAuth realm usr :> api) where instance HasClient m api => HasClient m (BasicAuth realm usr :> api) where

View file

@ -1,18 +1,18 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-} {-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -freduction-depth=100 #-} {-# OPTIONS_GHC -freduction-depth=100 #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-} {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
@ -27,20 +27,22 @@ import Control.Concurrent
import Control.Monad.Error.Class import Control.Monad.Error.Class
(throwError) (throwError)
import Data.Aeson import Data.Aeson
import qualified Data.ByteString.Lazy as LazyByteString import qualified Data.ByteString.Lazy as LazyByteString
import Data.Char import Data.Char
(chr, isPrint) (chr, isPrint)
import Data.Monoid () import Data.Monoid ()
import Data.Proxy import Data.Proxy
import Data.Text (Text) import Data.Text
import qualified Data.Text as Text (Text)
import Data.Text.Encoding (encodeUtf8, decodeUtf8) import qualified Data.Text as Text
import Data.Text.Encoding
(decodeUtf8, encodeUtf8)
import GHC.Generics import GHC.Generics
(Generic) (Generic)
import qualified Network.HTTP.Client as C import qualified Network.HTTP.Client as C
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 System.IO.Unsafe import System.IO.Unsafe
(unsafePerformIO) (unsafePerformIO)
@ -50,15 +52,14 @@ import Web.FormUrlEncoded
import Servant.API import Servant.API
((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth, ((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth,
BasicAuthData (..), Capture, CaptureAll, BasicAuthData (..), Capture, CaptureAll, DeleteNoContent,
DeleteNoContent, EmptyAPI, FormUrlEncoded, Get, Header, EmptyAPI, FormUrlEncoded, Fragment, Get, Header, Headers,
Headers, JSON, MimeRender(mimeRender), JSON, MimeRender (mimeRender), MimeUnrender (mimeUnrender),
MimeUnrender(mimeUnrender), NoContent (NoContent), PlainText, NoContent (NoContent), PlainText, Post, QueryFlag, QueryParam,
Post, QueryFlag, QueryParam, QueryParams, Raw, ReqBody, QueryParams, Raw, ReqBody, StdMethod (GET), UVerb, Union,
StdMethod(GET), Union, UVerb, WithStatus(WithStatus), WithStatus (WithStatus), addHeader)
addHeader)
import Servant.Client import Servant.Client
import qualified Servant.Client.Core.Auth as Auth import qualified Servant.Client.Core.Auth as Auth
import Servant.Server import Servant.Server
import Servant.Server.Experimental.Auth import Servant.Server.Experimental.Auth
import Servant.Test.ComprehensiveAPI import Servant.Test.ComprehensiveAPI
@ -109,6 +110,7 @@ type Api =
:<|> "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]
:<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool :<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool
:<|> "fragment" :> Fragment String :> Get '[JSON] Person
:<|> "rawSuccess" :> Raw :<|> "rawSuccess" :> Raw
:<|> "rawSuccessPassHeaders" :> Raw :<|> "rawSuccessPassHeaders" :> Raw
:<|> "rawFailure" :> Raw :<|> "rawFailure" :> Raw
@ -141,6 +143,7 @@ getBody :: Person -> ClientM Person
getQueryParam :: Maybe String -> ClientM Person getQueryParam :: Maybe String -> ClientM Person
getQueryParams :: [String] -> ClientM [Person] getQueryParams :: [String] -> ClientM [Person]
getQueryFlag :: Bool -> ClientM Bool getQueryFlag :: Bool -> ClientM Bool
getFragment :: ClientM Person
getRawSuccess :: HTTP.Method -> ClientM Response getRawSuccess :: HTTP.Method -> ClientM Response
getRawSuccessPassHeaders :: HTTP.Method -> ClientM Response getRawSuccessPassHeaders :: HTTP.Method -> ClientM Response
getRawFailure :: HTTP.Method -> ClientM Response getRawFailure :: HTTP.Method -> ClientM Response
@ -163,6 +166,7 @@ getRoot
:<|> getQueryParam :<|> getQueryParam
:<|> getQueryParams :<|> getQueryParams
:<|> getQueryFlag :<|> getQueryFlag
:<|> getFragment
:<|> getRawSuccess :<|> getRawSuccess
:<|> getRawSuccessPassHeaders :<|> getRawSuccessPassHeaders
:<|> getRawFailure :<|> getRawFailure
@ -188,6 +192,7 @@ server = serve api (
Nothing -> throwError $ ServerError 400 "missing parameter" "" []) Nothing -> throwError $ ServerError 400 "missing parameter" "" [])
:<|> (\ names -> return (zipWith Person names [0..])) :<|> (\ names -> return (zipWith Person names [0..]))
:<|> return :<|> return
:<|> return alice
:<|> (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.ok200 (Wai.requestHeaders $ request) "rawSuccess")) :<|> (Tagged $ \ request respond -> (respond $ Wai.responseLBS HTTP.ok200 (Wai.requestHeaders $ request) "rawSuccess"))
:<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.badRequest400 [] "rawFailure") :<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.badRequest400 [] "rawFailure")

View file

@ -1,17 +1,17 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-} {-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -freduction-depth=100 #-} {-# OPTIONS_GHC -freduction-depth=100 #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-} {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
@ -34,20 +34,21 @@ import Data.Maybe
import Data.Monoid () import Data.Monoid ()
import Data.Text import Data.Text
(Text) (Text)
import qualified Network.HTTP.Client as C import qualified Network.HTTP.Client as C
import qualified Network.HTTP.Types as HTTP import qualified Network.HTTP.Types as HTTP
import Test.Hspec import Test.Hspec
import Test.Hspec.QuickCheck import Test.Hspec.QuickCheck
import Test.HUnit import Test.HUnit
import Test.QuickCheck import Test.QuickCheck
import Servant.API import Servant.API
(NoContent (NoContent), WithStatus(WithStatus), getHeaders) (NoContent (NoContent), WithStatus (WithStatus), getHeaders)
import Servant.Client import Servant.Client
import qualified Servant.Client.Core.Request as Req import qualified Servant.Client.Core.Request as Req
import Servant.Client.Internal.HttpClient (defaultMakeClientRequest) import Servant.Client.Internal.HttpClient
import Servant.Test.ComprehensiveAPI (defaultMakeClientRequest)
import Servant.ClientTestUtils import Servant.ClientTestUtils
import Servant.Test.ComprehensiveAPI
-- This declaration simply checks that all instances are in place. -- This declaration simply checks that all instances are in place.
_ = client comprehensiveAPIWithoutStreaming _ = client comprehensiveAPIWithoutStreaming
@ -103,6 +104,8 @@ successSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
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
it "Servant.API.Fragment" $ \(_, baseUrl) -> do
left id <$> runClient getFragment baseUrl `shouldReturn` Right alice
it "Servant.API.Raw on success" $ \(_, baseUrl) -> do it "Servant.API.Raw on success" $ \(_, baseUrl) -> do
res <- runClient (getRawSuccess HTTP.methodGet) baseUrl res <- runClient (getRawSuccess HTTP.methodGet) baseUrl
case res of case res of

View file

@ -182,6 +182,28 @@
``` ```
## GET /fragment
### Fragment:
- *foo*: Fragment Int
### Response:
- Status code 200
- Headers: []
- Supported content types are:
- `application/json;charset=utf-8`
- `application/json`
- Example (`application/json;charset=utf-8`, `application/json`):
```javascript
```
## GET /get-int ## GET /get-int
### Response: ### Response:

View file

@ -25,8 +25,8 @@ import Control.Applicative
import Control.Arrow import Control.Arrow
(second) (second)
import Control.Lens import Control.Lens
(makeLenses, mapped, over, traversed, view, (%~), (&), (.~), (makeLenses, mapped, over, set, traversed, view, (%~), (&),
(<>~), (^.), (|>)) (.~), (<>~), (^.), (|>))
import qualified Data.ByteString.Char8 as BSC import qualified Data.ByteString.Char8 as BSC
import Data.ByteString.Lazy.Char8 import Data.ByteString.Lazy.Char8
(ByteString) (ByteString)
@ -64,7 +64,7 @@ import Servant.API
import Servant.API.ContentTypes import Servant.API.ContentTypes
import Servant.API.TypeLevel import Servant.API.TypeLevel
import qualified Data.Universe.Helpers as U import qualified Data.Universe.Helpers as U
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.Text as T import qualified Data.Text as T
@ -161,6 +161,20 @@ data DocQueryParam = DocQueryParam
, _paramKind :: ParamKind , _paramKind :: ParamKind
} deriving (Eq, Ord, Show) } deriving (Eq, Ord, Show)
-- | A type to represent fragment. Holds the name of the fragment and its description.
--
-- Write a 'ToFragment' instance for your fragment types.
data DocFragment = DocFragment
{ _fragSymbol :: String -- type supplied
, _fragDesc :: String -- user supplied
} deriving (Eq, Ord, Show)
-- | There should be at most one 'Fragment' per API endpoint.
-- So here we are keeping the first occurrence.
combineFragment :: Maybe DocFragment -> Maybe DocFragment -> Maybe DocFragment
Nothing `combineFragment` mdocFragment = mdocFragment
Just docFragment `combineFragment` _ = Just docFragment
-- | An introductory paragraph for your documentation. You can pass these to -- | An introductory paragraph for your documentation. You can pass these to
-- 'docsWithIntros'. -- 'docsWithIntros'.
data DocIntro = DocIntro data DocIntro = DocIntro
@ -283,6 +297,7 @@ data Action = Action
, _captures :: [DocCapture] -- type collected + user supplied info , _captures :: [DocCapture] -- type collected + user supplied info
, _headers :: [Text] -- type collected , _headers :: [Text] -- type collected
, _params :: [DocQueryParam] -- type collected + user supplied info , _params :: [DocQueryParam] -- type collected + user supplied info
, _fragment :: Maybe DocFragment -- type collected + user supplied info
, _notes :: [DocNote] -- user supplied , _notes :: [DocNote] -- user supplied
, _mxParams :: [(String, [DocQueryParam])] -- type collected + user supplied info , _mxParams :: [(String, [DocQueryParam])] -- type collected + user supplied info
, _rqtypes :: [M.MediaType] -- type collected , _rqtypes :: [M.MediaType] -- type collected
@ -296,8 +311,9 @@ data Action = Action
-- As such, we invent a non-commutative, left associative operation -- As such, we invent a non-commutative, left associative operation
-- 'combineAction' to mush two together taking the response from the very left. -- 'combineAction' to mush two together taking the response from the very left.
combineAction :: Action -> Action -> Action combineAction :: Action -> Action -> Action
Action a c h p n m ts body resp `combineAction` Action a' c' h' p' n' m' ts' body' resp' = Action a c h p f n m ts body resp
Action (a <> a') (c <> c') (h <> h') (p <> p') (n <> n') (m <> m') (ts <> ts') (body <> body') (resp `combineResponse` resp') `combineAction` Action a' c' h' p' f' n' m' ts' body' resp' =
Action (a <> a') (c <> c') (h <> h') (p <> p') (f `combineFragment` f') (n <> n') (m <> m') (ts <> ts') (body <> body') (resp `combineResponse` resp')
-- | Default 'Action'. Has no 'captures', no query 'params', expects -- | Default 'Action'. Has no 'captures', no query 'params', expects
-- no request body ('rqbody') and the typical response is 'defResponse'. -- no request body ('rqbody') and the typical response is 'defResponse'.
@ -305,10 +321,10 @@ Action a c h p n m ts body resp `combineAction` Action a' c' h' p' n' m' ts' bod
-- Tweakable with lenses. -- Tweakable with lenses.
-- --
-- >>> defAction -- >>> defAction
-- Action {_authInfo = [], _captures = [], _headers = [], _params = [], _notes = [], _mxParams = [], _rqtypes = [], _rqbody = [], _response = Response {_respStatus = 200, _respTypes = [], _respBody = [], _respHeaders = []}} -- Action {_authInfo = [], _captures = [], _headers = [], _params = [], _fragment = Nothing, _notes = [], _mxParams = [], _rqtypes = [], _rqbody = [], _response = Response {_respStatus = 200, _respTypes = [], _respBody = [], _respHeaders = []}}
-- --
-- >>> defAction & response.respStatus .~ 201 -- >>> defAction & response.respStatus .~ 201
-- Action {_authInfo = [], _captures = [], _headers = [], _params = [], _notes = [], _mxParams = [], _rqtypes = [], _rqbody = [], _response = Response {_respStatus = 201, _respTypes = [], _respBody = [], _respHeaders = []}} -- Action {_authInfo = [], _captures = [], _headers = [], _params = [], _fragment = Nothing, _notes = [], _mxParams = [], _rqtypes = [], _rqbody = [], _response = Response {_respStatus = 201, _respTypes = [], _respBody = [], _respHeaders = []}}
-- --
defAction :: Action defAction :: Action
defAction = defAction =
@ -316,6 +332,7 @@ defAction =
[] []
[] []
[] []
Nothing
[] []
[] []
[] []
@ -368,6 +385,7 @@ makeLenses ''API
makeLenses ''Endpoint makeLenses ''Endpoint
makeLenses ''DocCapture makeLenses ''DocCapture
makeLenses ''DocQueryParam makeLenses ''DocQueryParam
makeLenses ''DocFragment
makeLenses ''DocIntro makeLenses ''DocIntro
makeLenses ''DocNote makeLenses ''DocNote
makeLenses ''Response makeLenses ''Response
@ -587,6 +605,15 @@ class ToCapture c where
class ToAuthInfo a where class ToAuthInfo a where
toAuthInfo :: Proxy a -> DocAuthentication toAuthInfo :: Proxy a -> DocAuthentication
-- | The class that helps us get documentation for URL fragments.
--
-- Example of an instance:
--
-- > instance ToFragment (Fragment a) where
-- > toFragment _ = DocFragment "fragment" "fragment description"
class ToFragment t where
toFragment :: Proxy t -> DocFragment
-- | Generate documentation in Markdown format for -- | Generate documentation in Markdown format for
-- the given 'API'. -- the given 'API'.
-- --
@ -629,6 +656,7 @@ markdownWith RenderingOptions{..} api = unlines $
capturesStr (action ^. captures) ++ capturesStr (action ^. captures) ++
headersStr (action ^. headers) ++ headersStr (action ^. headers) ++
paramsStr meth (action ^. params) ++ paramsStr meth (action ^. params) ++
fragmentStr (action ^. fragment) ++
rqbodyStr (action ^. rqtypes) (action ^. rqbody) ++ rqbodyStr (action ^. rqtypes) (action ^. rqbody) ++
responseStr (action ^. response) ++ responseStr (action ^. response) ++
[] []
@ -730,6 +758,14 @@ markdownWith RenderingOptions{..} api = unlines $
where values = param ^. paramValues where values = param ^. paramValues
fragmentStr :: Maybe DocFragment -> [String]
fragmentStr Nothing = []
fragmentStr (Just frag) =
[ "### Fragment:", ""
, "- *" ++ (frag ^. fragSymbol) ++ "*: " ++ (frag ^. fragDesc)
, ""
]
rqbodyStr :: [M.MediaType] -> [(Text, M.MediaType, ByteString)]-> [String] rqbodyStr :: [M.MediaType] -> [(Text, M.MediaType, ByteString)]-> [String]
rqbodyStr [] [] = [] rqbodyStr [] [] = []
rqbodyStr types s = rqbodyStr types s =
@ -959,6 +995,15 @@ 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
instance (ToFragment (Fragment a), HasDocs api)
=> HasDocs (Fragment a :> api) where
docsFor Proxy (endpoint, action) =
docsFor subApiP (endpoint, action')
where subApiP = Proxy :: Proxy api
fragmentP = Proxy :: Proxy (Fragment a)
action' = set fragment (Just (toFragment fragmentP)) action
instance HasDocs Raw where instance HasDocs Raw where
docsFor _proxy (endpoint, action) _ = docsFor _proxy (endpoint, action) _ =

View file

@ -58,6 +58,8 @@ instance ToCapture (Capture "foo" Int) where
toCapture _ = DocCapture "foo" "Capture foo Int" toCapture _ = DocCapture "foo" "Capture foo Int"
instance ToCapture (CaptureAll "foo" Int) where instance ToCapture (CaptureAll "foo" Int) where
toCapture _ = DocCapture "foo" "Capture all foo Int" toCapture _ = DocCapture "foo" "Capture all foo Int"
instance ToFragment (Fragment Int) where
toFragment _ = DocFragment "foo" "Fragment Int"
-- * specs -- * specs

View file

@ -84,6 +84,11 @@ captureArg _ = error "captureArg called on non capture"
type Path f = [Segment f] type Path f = [Segment f]
newtype Frag f = Frag { unFragment :: Arg f }
deriving (Data, Eq, Show, Typeable)
makePrisms ''Frag
data ArgType data ArgType
= Normal = Normal
| Flag | Flag
@ -115,11 +120,12 @@ makePrisms ''HeaderArg
data Url f = Url data Url f = Url
{ _path :: Path f { _path :: Path f
, _queryStr :: [QueryArg f] , _queryStr :: [QueryArg f]
, _frag :: Maybe f
} }
deriving (Data, Eq, Show, Typeable) deriving (Data, Eq, Show, Typeable)
defUrl :: Url f defUrl :: Url f
defUrl = Url [] [] defUrl = Url [] [] Nothing
makeLenses ''Url makeLenses ''Url
@ -324,6 +330,16 @@ instance
{ _argName = PathSegment str { _argName = PathSegment str
, _argType = typeFor lang ftype (Proxy :: Proxy Bool) } , _argType = typeFor lang ftype (Proxy :: Proxy Bool) }
instance
(HasForeignType lang ftype (Maybe a), HasForeign lang ftype api)
=> HasForeign lang ftype (Fragment a :> api) where
type Foreign ftype (Fragment a :> api) = Foreign ftype api
foreignFor lang Proxy Proxy req =
foreignFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy api) $
req & reqUrl . frag .~ Just argT
where
argT = typeFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy (Maybe 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

View file

@ -14,8 +14,8 @@ module Servant.ForeignSpec where
import Data.Monoid import Data.Monoid
((<>)) ((<>))
import Data.Proxy import Data.Proxy
import Servant.Test.ComprehensiveAPI
import Servant.Foreign import Servant.Foreign
import Servant.Test.ComprehensiveAPI
import Servant.Types.SourceT import Servant.Types.SourceT
(SourceT) (SourceT)
@ -91,6 +91,7 @@ listFromAPISpec = describe "listFromAPI" $ do
{ _reqUrl = Url { _reqUrl = Url
[ Segment $ Static "test" ] [ Segment $ Static "test" ]
[ QueryArg (Arg "flag" "boolX") Flag ] [ QueryArg (Arg "flag" "boolX") Flag ]
Nothing
, _reqMethod = "GET" , _reqMethod = "GET"
, _reqHeaders = [HeaderArg $ Arg "header" "maybe listX of stringX"] , _reqHeaders = [HeaderArg $ Arg "header" "maybe listX of stringX"]
, _reqBody = Nothing , _reqBody = Nothing
@ -103,6 +104,7 @@ listFromAPISpec = describe "listFromAPI" $ do
{ _reqUrl = Url { _reqUrl = Url
[ Segment $ Static "test" ] [ Segment $ Static "test" ]
[ QueryArg (Arg "param" "maybe intX") Normal ] [ QueryArg (Arg "param" "maybe intX") Normal ]
Nothing
, _reqMethod = "POST" , _reqMethod = "POST"
, _reqHeaders = [] , _reqHeaders = []
, _reqBody = Just "listX of stringX" , _reqBody = Just "listX of stringX"
@ -116,6 +118,7 @@ listFromAPISpec = describe "listFromAPI" $ do
[ Segment $ Static "test" ] [ Segment $ Static "test" ]
-- Should this be |intX| or |listX of intX| ? -- Should this be |intX| or |listX of intX| ?
[ QueryArg (Arg "params" "listX of intX") List ] [ QueryArg (Arg "params" "listX of intX") List ]
Nothing
, _reqMethod = "PUT" , _reqMethod = "PUT"
, _reqHeaders = [] , _reqHeaders = []
, _reqBody = Just "stringX" , _reqBody = Just "stringX"
@ -129,6 +132,7 @@ listFromAPISpec = describe "listFromAPI" $ do
[ Segment $ Static "test" [ Segment $ Static "test"
, Segment $ Cap (Arg "id" "intX") ] , Segment $ Cap (Arg "id" "intX") ]
[] []
Nothing
, _reqMethod = "DELETE" , _reqMethod = "DELETE"
, _reqHeaders = [] , _reqHeaders = []
, _reqBody = Nothing , _reqBody = Nothing
@ -142,6 +146,7 @@ listFromAPISpec = describe "listFromAPI" $ do
[ Segment $ Static "test" [ Segment $ Static "test"
, Segment $ Cap (Arg "ids" "listX of intX") ] , Segment $ Cap (Arg "ids" "listX of intX") ]
[] []
Nothing
, _reqMethod = "GET" , _reqMethod = "GET"
, _reqHeaders = [] , _reqHeaders = []
, _reqBody = Nothing , _reqBody = Nothing

View file

@ -71,17 +71,17 @@ import Prelude ()
import Prelude.Compat import Prelude.Compat
import Servant.API import Servant.API
((:<|>) (..), (:>), Accept (..), BasicAuth, Capture', ((:<|>) (..), (:>), Accept (..), BasicAuth, Capture',
CaptureAll, Description, EmptyAPI, FramingRender (..), CaptureAll, Description, EmptyAPI, Fragment,
FramingUnrender (..), FromSourceIO (..), Header', If, FramingRender (..), FramingUnrender (..), FromSourceIO (..),
IsSecure (..), QueryFlag, QueryParam', QueryParams, Raw, Header', If, IsSecure (..), NoContentVerb, QueryFlag,
ReflectMethod (reflectMethod), RemoteHost, ReqBody', QueryParam', QueryParams, Raw, ReflectMethod (reflectMethod),
SBool (..), SBoolI (..), SourceIO, Stream, StreamBody', RemoteHost, ReqBody', SBool (..), SBoolI (..), SourceIO,
Summary, ToSourceIO (..), Vault, Verb, NoContentVerb, Stream, StreamBody', Summary, ToSourceIO (..), Vault, Verb,
WithNamedContext) WithNamedContext)
import Servant.API.ContentTypes import Servant.API.ContentTypes
(AcceptHeader (..), AllCTRender (..), AllCTUnrender (..), (AcceptHeader (..), AllCTRender (..), AllCTUnrender (..),
AllMime, MimeRender (..), MimeUnrender (..), canHandleAcceptH, AllMime, MimeRender (..), MimeUnrender (..), NoContent,
NoContent) canHandleAcceptH)
import Servant.API.Modifiers import Servant.API.Modifiers
(FoldLenient, FoldRequired, RequestArgument, (FoldLenient, FoldRequired, RequestArgument,
unfoldRequestArgument) unfoldRequestArgument)
@ -89,8 +89,8 @@ import Servant.API.ResponseHeaders
(GetHeaders, Headers, getHeaders, getResponse) (GetHeaders, Headers, getHeaders, getResponse)
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, parseUrlPiece,
parseUrlPieces, parseUrlPiece) parseUrlPieces)
import Servant.Server.Internal.BasicAuth import Servant.Server.Internal.BasicAuth
import Servant.Server.Internal.Context import Servant.Server.Internal.Context
@ -106,6 +106,8 @@ import Servant.Server.Internal.ServerError
#ifdef HAS_TYPE_ERROR #ifdef HAS_TYPE_ERROR
import GHC.TypeLits import GHC.TypeLits
(ErrorMessage (..), TypeError) (ErrorMessage (..), TypeError)
import Servant.API.TypeLevel
(AtLeastOneFragment, FragmentUnique)
#endif #endif
class HasServer api context where class HasServer api context where
@ -880,5 +882,28 @@ type HasServerArrowTypeError a b =
':$$: 'ShowType b ':$$: 'ShowType b
#endif #endif
-- | Ignore @'Fragment'@ in server handlers.
-- See <https://ietf.org/rfc/rfc2616.html#section-15.1.3> for more details.
--
-- Example:
--
-- > type MyApi = "books" :> Fragment Text :> Get '[JSON] [Book]
-- >
-- > server :: Server MyApi
-- > server = getBooksBy
-- > where getBooksBy :: Handler [Book]
-- > getBooksBy = ...return all books...
#ifdef HAS_TYPE_ERROR
instance (AtLeastOneFragment api, FragmentUnique (Fragment a1 :> api), HasServer api context)
#else
instance (HasServer api context)
#endif
=> HasServer (Fragment a1 :> api) context where
type ServerT (Fragment a1 :> api) m = ServerT api m
route _ = route (Proxy :: Proxy api)
hoistServerWithContext _ = hoistServerWithContext (Proxy :: Proxy api)
-- $setup -- $setup
-- >>> import Servant -- >>> import Servant

View file

@ -11,7 +11,7 @@ module Servant.Server.Internal.RoutingApplicationSpec (spec) where
import Prelude () import Prelude ()
import Prelude.Compat import Prelude.Compat
import Control.Exception hiding import Control.Exception hiding
(Handler) (Handler)
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Trans.Resource import Control.Monad.Trans.Resource
@ -28,7 +28,7 @@ import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
(request, shouldRespondWith, with) (request, shouldRespondWith, with)
import qualified Data.Text as T import qualified Data.Text as T
import System.IO.Unsafe import System.IO.Unsafe
(unsafePerformIO) (unsafePerformIO)

View file

@ -1,12 +1,12 @@
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-} {-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -freduction-depth=100 #-} {-# OPTIONS_GHC -freduction-depth=100 #-}
module Servant.ServerSpec where module Servant.ServerSpec where
@ -48,12 +48,12 @@ import Network.Wai.Test
import Servant.API import Servant.API
((:<|>) (..), (:>), AuthProtect, BasicAuth, ((:<|>) (..), (:>), AuthProtect, BasicAuth,
BasicAuthData (BasicAuthData), Capture, Capture', CaptureAll, BasicAuthData (BasicAuthData), Capture, Capture', CaptureAll,
Delete, EmptyAPI, Get, HasStatus(StatusOf), Header, Headers, Delete, EmptyAPI, Fragment, Get, HasStatus (StatusOf), Header,
HttpVersion, IsSecure (..), JSON, Lenient, NoContent (..), Headers, HttpVersion, IsSecure (..), JSON, Lenient,
NoContentVerb, NoFraming, OctetStream, Patch, PlainText, Post, NoContent (..), NoContentVerb, NoFraming, OctetStream, Patch,
Put, QueryFlag, QueryParam, QueryParams, Raw, RemoteHost, PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw,
ReqBody, SourceIO, StdMethod (..), Stream, Strict, Union, RemoteHost, ReqBody, SourceIO, StdMethod (..), Stream, Strict,
UVerb, Verb, addHeader) UVerb, Union, Verb, addHeader)
import Servant.Server import Servant.Server
(Context ((:.), EmptyContext), Handler, Server, Tagged (..), (Context ((:.), EmptyContext), Handler, Server, Tagged (..),
emptyServer, err401, err403, err404, respond, serve, emptyServer, err401, err403, err404, respond, serve,
@ -92,6 +92,7 @@ spec = do
captureSpec captureSpec
captureAllSpec captureAllSpec
queryParamSpec queryParamSpec
fragmentSpec
reqBodySpec reqBodySpec
headerSpec headerSpec
rawSpec rawSpec
@ -461,6 +462,37 @@ queryParamSpec = do
{ name = "Alice" { name = "Alice"
} }
-- }}}
------------------------------------------------------------------------------
-- * fragmentSpec {{{
------------------------------------------------------------------------------
type FragmentApi = "name" :> Fragment String :> Get '[JSON] Person
:<|> "age" :> Fragment Integer :> Get '[JSON] Person
fragmentApi :: Proxy FragmentApi
fragmentApi = Proxy
fragServer :: Server FragmentApi
fragServer = fragmentServer :<|> fragAge
where
fragmentServer = return alice
fragAge = return alice
fragmentSpec :: Spec
fragmentSpec = do
let mkRequest params pinfo = Network.Wai.Test.request defaultRequest
{ rawQueryString = params
, queryString = parseQuery params
, pathInfo = pinfo
}
describe "Servant.API.Fragment" $ do
it "ignores fragment even if it is present in query" $ do
flip runSession (serve fragmentApi fragServer) $ do
response1 <- mkRequest "#Alice" ["name"]
liftIO $ decode' (simpleBody response1) `shouldBe` Just alice
-- }}} -- }}}
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- * reqBodySpec {{{ -- * reqBodySpec {{{

View file

@ -40,6 +40,7 @@ library
Servant.API.Description Servant.API.Description
Servant.API.Empty Servant.API.Empty
Servant.API.Experimental.Auth Servant.API.Experimental.Auth
Servant.API.Fragment
Servant.API.Generic Servant.API.Generic
Servant.API.Header Servant.API.Header
Servant.API.HttpVersion Servant.API.HttpVersion

View file

@ -19,6 +19,8 @@ module Servant.API (
-- | Retrieving the HTTP version of the request -- | Retrieving the HTTP version of the request
module Servant.API.QueryParam, module Servant.API.QueryParam,
-- | Retrieving parameters from the query string of the 'URI': @'QueryParam'@ -- | Retrieving parameters from the query string of the 'URI': @'QueryParam'@
module Servant.API.Fragment,
-- | Documenting the fragment of the 'URI': @'Fragment'@
module Servant.API.ReqBody, module Servant.API.ReqBody,
-- | Accessing the request body as a JSON-encoded type: @'ReqBody'@ -- | Accessing the request body as a JSON-encoded type: @'ReqBody'@
module Servant.API.RemoteHost, module Servant.API.RemoteHost,
@ -93,6 +95,8 @@ import Servant.API.Empty
(EmptyAPI (..)) (EmptyAPI (..))
import Servant.API.Experimental.Auth import Servant.API.Experimental.Auth
(AuthProtect) (AuthProtect)
import Servant.API.Fragment
(Fragment)
import Servant.API.Header import Servant.API.Header
(Header, Header') (Header, Header')
import Servant.API.HttpVersion import Servant.API.HttpVersion
@ -121,21 +125,20 @@ import Servant.API.Stream
ToSourceIO (..)) ToSourceIO (..))
import Servant.API.Sub import Servant.API.Sub
((:>)) ((:>))
import Servant.API.UVerb
(HasStatus, IsMember, StatusOf, Statuses, UVerb, Union,
Unique, WithStatus (..), inject, statusOf)
import Servant.API.Vault import Servant.API.Vault
(Vault) (Vault)
import Servant.API.Verbs import Servant.API.Verbs
(Delete, DeleteAccepted, DeleteNoContent, (Delete, DeleteAccepted, DeleteNoContent,
DeleteNonAuthoritative, Get, GetAccepted, GetNoContent, DeleteNonAuthoritative, Get, GetAccepted, GetNoContent,
GetNonAuthoritative, GetPartialContent, GetResetContent, GetNonAuthoritative, GetPartialContent, GetResetContent,
Patch, PatchAccepted, PatchNoContent, PatchNonAuthoritative, NoContentVerb, Patch, PatchAccepted, PatchNoContent,
Post, PostAccepted, PostCreated, PostNoContent, PatchNonAuthoritative, Post, PostAccepted, PostCreated,
PostNonAuthoritative, PostResetContent, Put, PutAccepted, PostNoContent, PostNonAuthoritative, PostResetContent, Put,
PutCreated, PutNoContent, PutNonAuthoritative, PutAccepted, PutCreated, PutNoContent, PutNonAuthoritative,
ReflectMethod (reflectMethod), StdMethod (..), ReflectMethod (reflectMethod), StdMethod (..), Verb)
Verb, NoContentVerb)
import Servant.API.UVerb
(UVerb, Union, HasStatus, StatusOf, statusOf, Statuses,
WithStatus (..), IsMember, Unique, inject)
import Servant.API.WithNamedContext import Servant.API.WithNamedContext
(WithNamedContext) (WithNamedContext)
import Servant.Links import Servant.Links

View file

@ -0,0 +1,25 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_HADDOCK not-home #-}
module Servant.API.Fragment (Fragment) where
import Data.Typeable
(Typeable)
-- | Document the URI fragment in API. Useful in combination with 'Link'.
--
-- Example:
--
-- >>> -- /post#TRACKING
-- >>> type MyApi = "post" :> Fragment Text :> Get '[JSON] Tracking
data Fragment (a :: *)
deriving Typeable
-- $setup
-- >>> import Servant.API
-- >>> import Data.Aeson
-- >>> import Data.Text
-- >>> data Tracking
-- >>> instance ToJSON Tracking where { toJSON = undefined }

View file

@ -131,8 +131,6 @@ type RequestArgument mods a =
(If (FoldLenient mods) (Either Text a) a) (If (FoldLenient mods) (Either Text a) a)
(Maybe (If (FoldLenient mods) (Either Text a) a)) (Maybe (If (FoldLenient mods) (Either Text a) a))
-- | Unfold a value into a 'RequestArgument'. -- | Unfold a value into a 'RequestArgument'.
unfoldRequestArgument unfoldRequestArgument
:: forall mods m a. (Monad m, SBoolI (FoldRequired mods), SBoolI (FoldLenient mods)) :: forall mods m a. (Monad m, SBoolI (FoldRequired mods), SBoolI (FoldLenient mods))

View file

@ -1,12 +1,16 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PolyKinds #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE UndecidableSuperClasses #-}
{-| {-|
This module collects utilities for manipulating @servant@ API types. The This module collects utilities for manipulating @servant@ API types. The
@ -41,6 +45,9 @@ module Servant.API.TypeLevel (
-- ** Logic -- ** Logic
Or, Or,
And, And,
-- ** Fragment
FragmentUnique,
AtLeastOneFragment
) where ) where
@ -50,6 +57,7 @@ import Servant.API.Alternative
(type (:<|>)) (type (:<|>))
import Servant.API.Capture import Servant.API.Capture
(Capture, CaptureAll) (Capture, CaptureAll)
import Servant.API.Fragment
import Servant.API.Header import Servant.API.Header
(Header) (Header)
import Servant.API.QueryParam import Servant.API.QueryParam
@ -60,6 +68,8 @@ import Servant.API.Sub
(type (:>)) (type (:>))
import Servant.API.Verbs import Servant.API.Verbs
(Verb) (Verb)
import Servant.API.UVerb
(UVerb)
import GHC.TypeLits import GHC.TypeLits
(ErrorMessage (..), TypeError) (ErrorMessage (..), TypeError)
@ -128,6 +138,7 @@ type family IsElem endpoint api :: Constraint where
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 (QueryFlag x :> sb) = IsElem sa sb IsElem sa (QueryFlag x :> sb) = IsElem sa sb
IsElem sa (Fragment 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'
IsElem e e = () IsElem e e = ()
@ -241,6 +252,43 @@ We might try to factor these our more cleanly, but the type synonyms and type
families are not evaluated (see https://ghc.haskell.org/trac/ghc/ticket/12048). families are not evaluated (see https://ghc.haskell.org/trac/ghc/ticket/12048).
-} -}
-- ** Fragment
class FragmentUnique api => AtLeastOneFragment api
-- | If fragment appeared in API endpoint twice, compile-time error would be raised.
--
-- >>> -- type FailAPI = Fragment Bool :> Fragment Int :> Get '[JSON] NoContent
-- >>> instance AtLeastOneFragment FailAPI
-- ...
-- ...Only one Fragment allowed per endpoint in api...
-- ...
-- ...In the instance declaration for...
instance AtLeastOneFragment (Verb m s ct typ)
instance AtLeastOneFragment (UVerb m cts as)
instance AtLeastOneFragment (Fragment a)
type family FragmentUnique api :: Constraint where
FragmentUnique (sa :<|> sb) = And (FragmentUnique sa) (FragmentUnique sb)
FragmentUnique (Fragment a :> sa) = FragmentNotIn sa (Fragment a :> sa)
FragmentUnique (x :> sa) = FragmentUnique sa
FragmentUnique (Fragment a) = ()
FragmentUnique x = ()
type family FragmentNotIn api orig :: Constraint where
FragmentNotIn (sa :<|> sb) orig =
And (FragmentNotIn sa orig) (FragmentNotIn sb orig)
FragmentNotIn (Fragment c :> sa) orig = TypeError (NotUniqueFragmentInApi orig)
FragmentNotIn (x :> sa) orig = FragmentNotIn sa orig
FragmentNotIn (Fragment c) orig = TypeError (NotUniqueFragmentInApi orig)
FragmentNotIn x orig = ()
type NotUniqueFragmentInApi api =
'Text "Only one Fragment allowed per endpoint in api "
':<>: 'ShowType api
':<>: 'Text "."
-- $setup -- $setup
-- --
@ -248,6 +296,7 @@ families are not evaluated (see https://ghc.haskell.org/trac/ghc/ticket/12048).
-- --
-- >>> :set -XPolyKinds -- >>> :set -XPolyKinds
-- >>> :set -XGADTs -- >>> :set -XGADTs
-- >>> :set -XTypeSynonymInstances -XFlexibleInstances
-- >>> import Data.Proxy -- >>> import Data.Proxy
-- >>> import Data.Type.Equality -- >>> import Data.Type.Equality
-- >>> import Servant.API -- >>> import Servant.API
@ -255,4 +304,5 @@ families are not evaluated (see https://ghc.haskell.org/trac/ghc/ticket/12048).
-- >>> instance Show (OK ctx) where show _ = "OK" -- >>> instance Show (OK ctx) where show _ = "OK"
-- >>> let ok :: ctx => Proxy ctx -> OK ctx; ok _ = OK -- >>> let ok :: ctx => Proxy ctx -> OK ctx; ok _ = OK
-- >>> type SampleAPI = "hello" :> Get '[JSON] Int :<|> "bye" :> Capture "name" String :> Post '[JSON, PlainText] Bool -- >>> type SampleAPI = "hello" :> Get '[JSON] Int :<|> "bye" :> Capture "name" String :> Post '[JSON, PlainText] Bool
-- >>> type FailAPI = Fragment Bool :> Fragment Int :> Get '[JSON] NoContent
-- >>> let sampleAPI = Proxy :: Proxy SampleAPI -- >>> let sampleAPI = Proxy :: Proxy SampleAPI

View file

@ -120,6 +120,7 @@ module Servant.Links (
, Param (..) , Param (..)
, linkSegments , linkSegments
, linkQueryParams , linkQueryParams
, linkFragment
) where ) where
import Data.List import Data.List
@ -152,6 +153,8 @@ import Servant.API.Empty
(EmptyAPI (..)) (EmptyAPI (..))
import Servant.API.Experimental.Auth import Servant.API.Experimental.Auth
(AuthProtect) (AuthProtect)
import Servant.API.Fragment
(Fragment)
import Servant.API.Generic import Servant.API.Generic
import Servant.API.Header import Servant.API.Header
(Header') (Header')
@ -188,10 +191,13 @@ import Web.HttpApiData
data Link = Link data Link = Link
{ _segments :: [Escaped] { _segments :: [Escaped]
, _queryParams :: [Param] , _queryParams :: [Param]
, _fragment :: Fragment'
} deriving Show } deriving Show
newtype Escaped = Escaped String newtype Escaped = Escaped String
type Fragment' = Maybe String
escaped :: String -> Escaped escaped :: String -> Escaped
escaped = Escaped . escapeURIString isUnreserved escaped = Escaped . escapeURIString isUnreserved
@ -208,11 +214,14 @@ linkSegments = map getEscaped . _segments
linkQueryParams :: Link -> [Param] linkQueryParams :: Link -> [Param]
linkQueryParams = _queryParams linkQueryParams = _queryParams
linkFragment :: Link -> Fragment'
linkFragment = _fragment
instance ToHttpApiData Link where instance ToHttpApiData Link where
toHeader = TE.encodeUtf8 . toUrlPiece toHeader = TE.encodeUtf8 . toUrlPiece
toUrlPiece l = toUrlPiece l =
let uri = linkURI l let uri = linkURI l
in Text.pack $ uriPath uri ++ uriQuery uri in Text.pack $ uriPath uri ++ uriQuery uri ++ uriFragment uri
-- | Query parameter. -- | Query parameter.
data Param data Param
@ -228,6 +237,9 @@ addQueryParam :: Param -> Link -> Link
addQueryParam qp l = addQueryParam qp l =
l { _queryParams = _queryParams l <> [qp] } l { _queryParams = _queryParams l <> [qp] }
addFragment :: Fragment' -> Link -> Link
addFragment fr l = l { _fragment = fr }
-- | Transform 'Link' into 'URI'. -- | Transform 'Link' into 'URI'.
-- --
-- >>> type API = "something" :> Get '[JSON] Int -- >>> type API = "something" :> Get '[JSON] Int
@ -245,7 +257,7 @@ addQueryParam qp l =
-- >>> type SomeRoute = "abc" :> Capture "email" String :> Put '[JSON] () -- >>> type SomeRoute = "abc" :> Capture "email" String :> Put '[JSON] ()
-- >>> let someRoute = Proxy :: Proxy SomeRoute -- >>> let someRoute = Proxy :: Proxy SomeRoute
-- >>> safeLink someRoute someRoute "test@example.com" -- >>> safeLink someRoute someRoute "test@example.com"
-- Link {_segments = ["abc","test%40example.com"], _queryParams = []} -- Link {_segments = ["abc","test%40example.com"], _queryParams = [], _fragment = Nothing}
-- --
-- >>> linkURI $ safeLink someRoute someRoute "test@example.com" -- >>> linkURI $ safeLink someRoute someRoute "test@example.com"
-- abc/test%40example.com -- abc/test%40example.com
@ -269,11 +281,12 @@ data LinkArrayElementStyle
-- sum?x=1&x=2&x=3 -- sum?x=1&x=2&x=3
-- --
linkURI' :: LinkArrayElementStyle -> Link -> URI linkURI' :: LinkArrayElementStyle -> Link -> URI
linkURI' addBrackets (Link segments q_params) = linkURI' addBrackets (Link segments q_params mfragment) =
URI mempty -- No scheme (relative) URI mempty -- No scheme (relative)
Nothing -- Or authority (relative) Nothing -- Or authority (relative)
(intercalate "/" $ map getEscaped segments) (intercalate "/" $ map getEscaped segments)
(makeQueries q_params) mempty (makeQueries q_params)
(makeFragment mfragment)
where where
makeQueries :: [Param] -> String makeQueries :: [Param] -> String
makeQueries [] = "" makeQueries [] = ""
@ -285,6 +298,10 @@ linkURI' addBrackets (Link segments q_params) =
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
makeFragment :: Fragment' -> String
makeFragment Nothing = ""
makeFragment (Just fr) = "#" <> escape fr
style = case addBrackets of style = case addBrackets of
LinkArrayElementBracket -> "[]=" LinkArrayElementBracket -> "[]="
LinkArrayElementPlain -> "=" LinkArrayElementPlain -> "="
@ -310,7 +327,7 @@ safeLink'
-> Proxy api -- ^ The whole API that this endpoint is a part of -> Proxy api -- ^ The whole API that this endpoint is a part of
-> Proxy endpoint -- ^ The API endpoint you would like to point to -> Proxy endpoint -- ^ The API endpoint you would like to point to
-> MkLink endpoint a -> MkLink endpoint a
safeLink' toA _ endpoint = toLink toA endpoint (Link mempty mempty) safeLink' toA _ endpoint = toLink toA endpoint (Link mempty mempty mempty)
-- | Create all links in an API. -- | Create all links in an API.
-- --
@ -341,7 +358,7 @@ allLinks'
=> (Link -> a) => (Link -> a)
-> Proxy api -> Proxy api
-> MkLink api a -> MkLink api a
allLinks' toA api = toLink toA api (Link mempty mempty) allLinks' toA api = toLink toA api (Link mempty mempty mempty)
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- Generics -- Generics
@ -563,6 +580,13 @@ instance HasLink sub => HasLink (AuthProtect tag :> sub) where
type MkLink (AuthProtect tag :> sub) a = MkLink sub a type MkLink (AuthProtect tag :> sub) a = MkLink sub a
toLink = simpleToLink (Proxy :: Proxy sub) toLink = simpleToLink (Proxy :: Proxy sub)
instance (HasLink sub, ToHttpApiData v)
=> HasLink (Fragment v :> sub) where
type MkLink (Fragment v :> sub) a = v -> MkLink sub a
toLink toA _ l mv =
toLink toA (Proxy :: Proxy sub) $
addFragment ((Just . Text.unpack . toQueryParam) mv) l
-- | Helper for implementing 'toLink' for combinators not affecting link -- | Helper for implementing 'toLink' for combinators not affecting link
-- structure. -- structure.
simpleToLink simpleToLink

View file

@ -71,6 +71,7 @@ type ComprehensiveAPIWithoutStreamingOrRaw' endpoint =
:<|> "summary" :> Summary "foo" :> GET :<|> "summary" :> Summary "foo" :> GET
:<|> "description" :> Description "foo" :> GET :<|> "description" :> Description "foo" :> GET
:<|> "alternative" :> ("left" :> GET :<|> "right" :> GET) :<|> "alternative" :> ("left" :> GET :<|> "right" :> GET)
:<|> "fragment" :> Fragment Int :> GET
:<|> endpoint :<|> endpoint
type ComprehensiveAPIWithoutStreamingOrRaw = ComprehensiveAPIWithoutStreamingOrRaw' EmptyEndpoint type ComprehensiveAPIWithoutStreamingOrRaw = ComprehensiveAPIWithoutStreamingOrRaw' EmptyEndpoint

View file

@ -13,9 +13,9 @@ import Test.Hspec
(Expectation, Spec, describe, it, shouldBe) (Expectation, Spec, describe, it, shouldBe)
import Servant.API import Servant.API
import Servant.Links
import Servant.Test.ComprehensiveAPI import Servant.Test.ComprehensiveAPI
(comprehensiveAPIWithoutRaw) (comprehensiveAPIWithoutRaw)
import Servant.Links
type TestApi = type TestApi =
-- Capture and query params -- Capture and query params
@ -26,6 +26,9 @@ type TestApi =
-- Flags -- Flags
:<|> "balls" :> QueryFlag "bouncy" :> QueryFlag "fast" :> Delete '[JSON] NoContent :<|> "balls" :> QueryFlag "bouncy" :> QueryFlag "fast" :> Delete '[JSON] NoContent
-- Fragment
:<|> "say" :> Fragment String :> Get '[JSON] NoContent
-- All of the verbs -- All of the verbs
:<|> "get" :> Get '[JSON] NoContent :<|> "get" :> Get '[JSON] NoContent
:<|> "put" :> Put '[JSON] NoContent :<|> "put" :> Put '[JSON] NoContent
@ -76,6 +79,10 @@ spec = describe "Servant.Links" $ do
apiLink l1 True True `shouldBeLink` "balls?bouncy&fast" apiLink l1 True True `shouldBeLink` "balls?bouncy&fast"
apiLink l1 False True `shouldBeLink` "balls?fast" apiLink l1 False True `shouldBeLink` "balls?fast"
it "generates correct link for fragment" $ do
let l1 = Proxy :: Proxy ("say" :> Fragment String :> Get '[JSON] NoContent)
apiLink l1 "something" `shouldBeLink` "say#something"
it "generates correct links for all of the verbs" $ do it "generates correct links for all of the verbs" $ do
apiLink (Proxy :: Proxy ("get" :> Get '[JSON] NoContent)) `shouldBeLink` "get" apiLink (Proxy :: Proxy ("get" :> Get '[JSON] NoContent)) `shouldBeLink` "get"
apiLink (Proxy :: Proxy ("put" :> Put '[JSON] NoContent)) `shouldBeLink` "put" apiLink (Proxy :: Proxy ("put" :> Put '[JSON] NoContent)) `shouldBeLink` "put"