HasClient instance for Stream

This commit is contained in:
Gershom 2017-10-20 15:09:11 -04:00
parent 9132a5bb84
commit 9a2ac6f4dd
4 changed files with 66 additions and 11 deletions

View file

@ -18,6 +18,7 @@ import Prelude ()
import Prelude.Compat import Prelude.Compat
import Data.Foldable (toList) import Data.Foldable (toList)
import qualified Data.ByteString.Lazy as BL
import Data.List (foldl') import Data.List (foldl')
import Data.Proxy (Proxy (Proxy)) import Data.Proxy (Proxy (Proxy))
import Data.Sequence (fromList) import Data.Sequence (fromList)
@ -29,8 +30,10 @@ import Servant.API ((:<|>) ((:<|>)), (:>),
AuthProtect, BasicAuth, AuthProtect, BasicAuth,
BasicAuthData, BasicAuthData,
BuildHeadersTo (..), BuildHeadersTo (..),
BuildFromStream (..),
Capture, CaptureAll, Capture, CaptureAll,
Description, EmptyAPI, Description, EmptyAPI,
FramingUnrender (..),
Header, Headers (..), Header, Headers (..),
HttpVersion, IsSecure, HttpVersion, IsSecure,
MimeRender (mimeRender), MimeRender (mimeRender),
@ -40,6 +43,7 @@ import Servant.API ((:<|>) ((:<|>)), (:>),
QueryParams, Raw, QueryParams, Raw,
ReflectMethod (..), ReflectMethod (..),
RemoteHost, ReqBody, RemoteHost, ReqBody,
Stream,
Summary, ToHttpApiData, Summary, ToHttpApiData,
Vault, Verb, Vault, Verb,
WithNamedContext, WithNamedContext,
@ -244,6 +248,44 @@ instance OVERLAPPING_
, getHeadersHList = buildHeadersTo . toList $ responseHeaders response , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
} }
instance OVERLAPPABLE_
( RunClient m, MimeUnrender ct a, ReflectMethod method,
FramingUnrender framing a, BuildFromStream a (f a)
) => HasClient m (Stream method framing ct (f a)) where
type Client m (Stream method framing ct (f a)) = m (f a)
clientWithRoute _pm Proxy req = do
response <- runRequest req
{ requestAccept = fromList [contentType (Proxy :: Proxy ct)]
, requestMethod = reflectMethod (Proxy :: Proxy method)
}
return $ decodeFramed (Proxy :: Proxy framing) (Proxy :: Proxy ct) (Proxy :: Proxy a) response
instance OVERLAPPING_
( RunClient m, BuildHeadersTo ls, MimeUnrender ct a, ReflectMethod method,
FramingUnrender framing a, BuildFromStream a (f a)
) => HasClient m (Stream method framing ct (Headers ls (f a))) where
type Client m (Stream method framing ct (Headers ls (f a))) = m (Headers ls (f a))
clientWithRoute _pm Proxy req = do
response <- runRequest req
{ requestAccept = fromList [contentType (Proxy :: Proxy ct)]
, requestMethod = reflectMethod (Proxy :: Proxy method)
}
return Headers { getResponse = decodeFramed (Proxy :: Proxy framing) (Proxy :: Proxy ct) (Proxy :: Proxy a) response
, getHeadersHList = buildHeadersTo . toList $ responseHeaders response
}
decodeFramed :: forall ctype strategy a b.
(MimeUnrender ctype a, FramingUnrender strategy a, BuildFromStream a b) =>
Proxy strategy -> Proxy ctype -> Proxy a -> Response -> b
decodeFramed framingproxy ctypeproxy typeproxy response =
let (body, uncons) = unrenderFrames framingproxy typeproxy (responseBody response)
loop b | BL.null b = []
| otherwise = case uncons b of
(Right x, r) -> case mimeUnrender ctypeproxy x :: Either String a of
Left err -> Left err : loop r
Right x' -> Right x' : loop r
(Left err, r) -> Left err : loop r
in buildFromStream $ loop body
-- | If you use a 'Header' in one of your endpoints in your API, -- | If you use a 'Header' in one of your endpoints in your API,
-- the corresponding querying function will automatically take -- the corresponding querying function will automatically take

View file

@ -349,7 +349,7 @@ streamRouter splitHeaders method framingproxy ctypeproxy action = leafRouter $ \
BoundaryStrategyGeneral f -> BoundaryStrategyGeneral f ->
let go = (>> flush) . write . BB.lazyByteString . f . mimeRender ctypeproxy let go = (>> flush) . write . BB.lazyByteString . f . mimeRender ctypeproxy
in k go go in k go go
write . BB.lazyByteString $ terminate framingproxy ctypeproxy write . BB.lazyByteString $ trailer framingproxy ctypeproxy
-- | If you use 'Header' in one of the endpoints for your API, -- | If you use 'Header' in one of the endpoints for your API,
-- this automatically requires your server-side handler to be a function -- this automatically requires your server-side handler to be a function

View file

@ -83,9 +83,10 @@ import Servant.API.IsSecure (IsSecure (..))
import Servant.API.QueryParam (QueryFlag, QueryParam, import Servant.API.QueryParam (QueryFlag, QueryParam,
QueryParams) QueryParams)
import Servant.API.Raw (Raw) import Servant.API.Raw (Raw)
import Servant.API.Stream (Stream, StreamGenerator(..), ToStreamGenerator(..), import Servant.API.Stream (Stream, StreamGenerator (..),
FramingRender(..), BoundaryStrategy(..), ToStreamGenerator (..), BuildFromStream (..),
FramingUnrender(..), FramingRender (..), BoundaryStrategy (..),
FramingUnrender (..),
NewlineFraming) NewlineFraming)
import Servant.API.RemoteHost (RemoteHost) import Servant.API.RemoteHost (RemoteHost)
import Servant.API.ReqBody (ReqBody) import Servant.API.ReqBody (ReqBody)

View file

@ -1,3 +1,4 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
@ -17,11 +18,15 @@ import Data.Proxy (Proxy)
import Data.Typeable (Typeable) import Data.Typeable (Typeable)
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Text.Read (readMaybe) import Text.Read (readMaybe)
import Network.HTTP.Types.Method (StdMethod (..))
-- | A stream endpoint for a given method emits a stream of encoded values at a given Content-Type, delimited by a framing strategy. -- | A Stream endpoint for a given method emits a stream of encoded values at a given Content-Type, delimited by a framing strategy. Steam endpoints always return response code 200 on success. Type synonyms are provided for standard methods.
data Stream (method :: k1) (framing :: *) (contentType :: *) a data Stream (method :: k1) (framing :: *) (contentType :: *) a
deriving (Typeable, Generic) deriving (Typeable, Generic)
type StreamGet = Stream 'GET
type StreamPost = Stream 'POST
-- | Stream endpoints may be implemented as producing a @StreamGenerator@ -- a function that itself takes two emit functions -- the first to be used on the first value the stream emits, and the second to be used on all subsequent values (to allow interspersed framing strategies such as comma separation). -- | Stream endpoints may be implemented as producing a @StreamGenerator@ -- a function that itself takes two emit functions -- the first to be used on the first value the stream emits, and the second to be used on all subsequent values (to allow interspersed framing strategies such as comma separation).
newtype StreamGenerator a = StreamGenerator {getStreamGenerator :: (a -> IO ()) -> (a -> IO ()) -> IO ()} newtype StreamGenerator a = StreamGenerator {getStreamGenerator :: (a -> IO ()) -> (a -> IO ()) -> IO ()}
@ -32,11 +37,18 @@ class ToStreamGenerator f a where
instance ToStreamGenerator StreamGenerator a instance ToStreamGenerator StreamGenerator a
where toStreamGenerator x = x where toStreamGenerator x = x
-- | BuildFromStream is intended to be implemented for types such as Conduit, Pipe, etc. By implementing this class, all such streaming abstractions can be used directly on the client side for talking to streaming endpoints. The streams we build from are represented as lazy lists of elements interspersed with possible errors.
class BuildFromStream a b where
buildFromStream :: [Either String a] -> b
instance BuildFromStream a [Either String a]
where buildFromStream x = x
-- | The FramingRender class provides the logic for emitting a framing strategy. The strategy emits a header, followed by boundary-delimited data, and finally a termination character. For many strategies, some of these will just be empty bytestrings. -- | The FramingRender class provides the logic for emitting a framing strategy. The strategy emits a header, followed by boundary-delimited data, and finally a termination character. For many strategies, some of these will just be empty bytestrings.
class FramingRender strategy a where class FramingRender strategy a where
header :: Proxy strategy -> Proxy a -> ByteString header :: Proxy strategy -> Proxy a -> ByteString
boundary :: Proxy strategy -> Proxy a -> BoundaryStrategy boundary :: Proxy strategy -> Proxy a -> BoundaryStrategy
terminate :: Proxy strategy -> Proxy a -> ByteString trailer :: Proxy strategy -> Proxy a -> ByteString
-- | The bracketing strategy generates things to precede and follow the content, as with netstrings. -- | The bracketing strategy generates things to precede and follow the content, as with netstrings.
-- The intersperse strategy inserts seperators between things, as with newline framing. -- The intersperse strategy inserts seperators between things, as with newline framing.
@ -45,7 +57,7 @@ data BoundaryStrategy = BoundaryStrategyBracket (ByteString -> (ByteString,ByteS
| BoundaryStrategyIntersperse ByteString | BoundaryStrategyIntersperse ByteString
| BoundaryStrategyGeneral (ByteString -> ByteString) | BoundaryStrategyGeneral (ByteString -> ByteString)
-- | The FramingUnrender class provides the logic for parsing a framing strategy. Given a ByteString, it strips the header, and returns a tuple of the remainder along with a step function that can progressively "uncons" elements from this remainder. The error state is presented per-frame so that protocols that can resume after errors are able to do so. -- | The FramingUnrender class provides the logic for parsing a framing strategy. Given a ByteString, it strips the header, and returns a tuple of the remainder along with a step function that can progressively "uncons" elements from this remainder. The error state is presented per-frame so that protocols that can resume after errors are able to do so. Termination of the unrendering is indicated by return of an empty reminder.
class FramingUnrender strategy a where class FramingUnrender strategy a where
unrenderFrames :: Proxy strategy -> Proxy a -> ByteString -> (ByteString, ByteString -> (Either String ByteString, ByteString)) unrenderFrames :: Proxy strategy -> Proxy a -> ByteString -> (ByteString, ByteString -> (Either String ByteString, ByteString))
@ -57,7 +69,7 @@ data NewlineFraming
instance FramingRender NewlineFraming a where instance FramingRender NewlineFraming a where
header _ _ = empty header _ _ = empty
boundary _ _ = BoundaryStrategyIntersperse "\n" boundary _ _ = BoundaryStrategyIntersperse "\n"
terminate _ _ = empty trailer _ _ = empty
instance FramingUnrender NewlineFraming a where instance FramingUnrender NewlineFraming a where
unrenderFrames _ _ = (, (Right *** LB.drop 1) . LB.break (== '\n')) unrenderFrames _ _ = (, (Right *** LB.drop 1) . LB.break (== '\n'))
@ -68,7 +80,7 @@ data NetstringFraming
instance FramingRender NetstringFraming a where instance FramingRender NetstringFraming a where
header _ _ = empty header _ _ = empty
boundary _ _ = BoundaryStrategyBracket $ \b -> (LB.pack . show . LB.length $ b, "") boundary _ _ = BoundaryStrategyBracket $ \b -> (LB.pack . show . LB.length $ b, "")
terminate _ _ = empty trailer _ _ = empty
instance FramingUnrender NetstringFraming a where instance FramingUnrender NetstringFraming a where
unrenderFrames _ _ = (, \b -> let (i,r) = LB.break (==':') b unrenderFrames _ _ = (, \b -> let (i,r) = LB.break (==':') b