Connor/metadata improvements (#52)

* make metadatamap a newtype of multimap

* update tests for new MetadataMap

* remove multimap dependency, reproduce needed functionality instead

* add repeated key to payload test

* bump LTS version for sorted-list, use SortedList for MetadataMap

* remove unneeded conversion

* MetadataMap documentation

* minor fixes
This commit is contained in:
Connor Clark 2016-08-01 12:38:35 -07:00
parent 50ece70720
commit f8bbe3abd2
9 changed files with 101 additions and 59 deletions

View file

@ -41,6 +41,7 @@ library
, tasty-hunit >= 0.9 && <0.10 , tasty-hunit >= 0.9 && <0.10
, safe ==0.3.* , safe ==0.3.*
, vector , vector
, sorted-list >=0.1.6.1 && <=0.3
c-sources: c-sources:
cbits/grpc_haskell.c cbits/grpc_haskell.c
@ -176,6 +177,7 @@ test-suite test
, async , async
, tasty >= 0.11 && <0.12 , tasty >= 0.11 && <0.12
, tasty-hunit >= 0.9 && <0.10 , tasty-hunit >= 0.9 && <0.10
, tasty-quickcheck ==0.8.*
, containers ==0.5.* , containers ==0.5.*
, managed >= 1.0.0 && < 1.1 , managed >= 1.0.0 && < 1.1
, pipes ==4.1.* , pipes ==4.1.*

View file

@ -22,7 +22,7 @@ GRPC
, GRPCMethodType(..) , GRPCMethodType(..)
, RegisteredMethod , RegisteredMethod
, NormalRequestResult(..) , NormalRequestResult(..)
, MetadataMap , MetadataMap(..)
, MethodName(..) , MethodName(..)
, StatusDetails(..) , StatusDetails(..)

View file

@ -2,21 +2,30 @@
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE StandaloneDeriving #-}
module Network.GRPC.LowLevel.GRPC where module Network.GRPC.LowLevel.GRPC(
GRPC
, withGRPC
, GRPCIOError(..)
, throwIfCallError
, grpcDebug
, threadDelaySecs
, C.MetadataMap(..)
, StatusDetails(..)
) where
import Control.Concurrent (threadDelay, myThreadId) import Control.Concurrent (threadDelay, myThreadId)
import Control.Exception import Control.Exception
import Data.String (IsString) import Data.String (IsString)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.Map as M import qualified Data.Map as M
import Data.Typeable import Data.Typeable
import qualified Network.GRPC.Unsafe as C import qualified Network.GRPC.Unsafe as C
import qualified Network.GRPC.Unsafe.Op as C import qualified Network.GRPC.Unsafe.Op as C
import qualified Network.GRPC.Unsafe.Metadata as C
import Proto3.Wire.Decode (ParseError) import Proto3.Wire.Decode (ParseError)
type MetadataMap = M.Map B.ByteString B.ByteString newtype StatusDetails = StatusDetails ByteString
newtype StatusDetails = StatusDetails B.ByteString
deriving (Eq, IsString, Monoid, Show) deriving (Eq, IsString, Monoid, Show)
-- | Functions as a proof that the gRPC core has been started. The gRPC core -- | Functions as a proof that the gRPC core has been started. The gRPC core
@ -72,33 +81,3 @@ grpcDebug' str = do
threadDelaySecs :: Int -> IO () threadDelaySecs :: Int -> IO ()
threadDelaySecs = threadDelay . (* 10^(6::Int)) threadDelaySecs = threadDelay . (* 10^(6::Int))
{-
-- TODO: remove this once finally decided on whether to use it.
-- | Monad for running gRPC operations.
newtype GRPCIO a = GRPCIO {unGRPCIO :: ExceptT GRPCIOError IO a}
deriving (Functor, Applicative, Monad, MonadIO)
deriving instance MonadError GRPCIOError GRPCIO
runGRPCIO :: GRPCIO a -> IO (Either GRPCIOError a)
runGRPCIO = runExceptT . unGRPCIO
unrunGRPCIO :: IO (Either GRPCIOError a) -> GRPCIO a
unrunGRPCIO = GRPCIO . ExceptT
continueFrom :: (a -> GRPCIO b) -> (Either GRPCIOError a) -> GRPCIO b
continueFrom f (Left x) = throwError x
continueFrom f (Right x) = f x
wrapGRPC :: Either GRPCIOError a -> GRPCIO a
wrapGRPC (Left x) = throwError x
wrapGRPC (Right x) = return x
grpcBracket :: GRPCIO a -> (a -> GRPCIO b) -> (a -> GRPCIO c) -> GRPCIO c
grpcBracket create destroy f = unrunGRPCIO $ do
let createAction = runGRPCIO create
let fAction = runGRPCIO . continueFrom f
let destroyAction = runGRPCIO . continueFrom destroy
bracket createAction destroyAction fAction
-}

View file

@ -12,7 +12,6 @@ import Control.Monad.Trans.Class (MonadTrans(lift))
import Control.Monad.Trans.Except import Control.Monad.Trans.Except
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes) import Data.Maybe (catMaybes)
import Foreign.C.String (CString) import Foreign.C.String (CString)
import Foreign.C.Types (CInt) import Foreign.C.Types (CInt)
@ -20,6 +19,7 @@ import Foreign.Marshal.Alloc (free, malloc,
mallocBytes) mallocBytes)
import Foreign.Ptr (Ptr, nullPtr) import Foreign.Ptr (Ptr, nullPtr)
import Foreign.Storable (peek, poke) import Foreign.Storable (peek, poke)
import GHC.Exts (IsList(..))
import Network.GRPC.LowLevel.CompletionQueue import Network.GRPC.LowLevel.CompletionQueue
import Network.GRPC.LowLevel.GRPC import Network.GRPC.LowLevel.GRPC
import qualified Network.GRPC.Unsafe as C (Call) import qualified Network.GRPC.Unsafe as C (Call)
@ -41,7 +41,7 @@ data Op = OpSendInitialMetadata MetadataMap
| OpRecvMessage | OpRecvMessage
| OpRecvStatusOnClient | OpRecvStatusOnClient
| OpRecvCloseOnServer | OpRecvCloseOnServer
deriving (Eq, Show) deriving (Show)
-- | Container holding the pointers to the C and gRPC data needed to execute the -- | Container holding the pointers to the C and gRPC data needed to execute the
-- corresponding 'Op'. These are obviously unsafe, and should only be used with -- corresponding 'Op'. These are obviously unsafe, and should only be used with
@ -70,14 +70,14 @@ createOpContext :: Op -> IO OpContext
createOpContext (OpSendInitialMetadata m) = createOpContext (OpSendInitialMetadata m) =
OpSendInitialMetadataContext OpSendInitialMetadataContext
<$> C.createMetadata m <$> C.createMetadata m
<*> return (M.size m) <*> return (length $ toList m)
createOpContext (OpSendMessage bs) = createOpContext (OpSendMessage bs) =
fmap OpSendMessageContext (C.createByteBuffer bs) fmap OpSendMessageContext (C.createByteBuffer bs)
createOpContext (OpSendCloseFromClient) = return OpSendCloseFromClientContext createOpContext (OpSendCloseFromClient) = return OpSendCloseFromClientContext
createOpContext (OpSendStatusFromServer m code (StatusDetails str)) = createOpContext (OpSendStatusFromServer m code (StatusDetails str)) =
OpSendStatusFromServerContext OpSendStatusFromServerContext
<$> C.createMetadata m <$> C.createMetadata m
<*> return (M.size m) <*> return (length $ toList m)
<*> return code <*> return code
<*> return str <*> return str
createOpContext OpRecvInitialMetadata = createOpContext OpRecvInitialMetadata =
@ -158,7 +158,7 @@ data OpRecvResult =
-- which case this will be 'Nothing'. -- which case this will be 'Nothing'.
| OpRecvStatusOnClientResult MetadataMap C.StatusCode B.ByteString | OpRecvStatusOnClientResult MetadataMap C.StatusCode B.ByteString
| OpRecvCloseOnServerResult Bool -- ^ True if call was cancelled. | OpRecvCloseOnServerResult Bool -- ^ True if call was cancelled.
deriving (Eq, Show) deriving (Show)
-- | For the given 'OpContext', if the 'Op' receives data, copies the data out -- | For the given 'OpContext', if the 'Op' receives data, copies the data out
-- of the 'OpContext' and into GC-managed Haskell types. After this, it is safe -- of the 'OpContext' and into GC-managed Haskell types. After this, it is safe

View file

@ -1,21 +1,55 @@
{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
module Network.GRPC.Unsafe.Metadata where module Network.GRPC.Unsafe.Metadata where
import Control.Exception import Control.Exception
import Control.Monad import Control.Monad
import Data.ByteString (ByteString, useAsCString, packCString) import Data.ByteString (ByteString, useAsCString, packCString)
import Data.Map.Strict as M import Data.Function (on)
import Data.List (sortBy, groupBy)
import qualified Data.SortedList as SL
import qualified Data.Map.Strict as M
import Data.Ord (comparing)
import Foreign.C.String import Foreign.C.String
import Foreign.Ptr import Foreign.Ptr
import Foreign.Storable import Foreign.Storable
import GHC.Exts
#include <grpc/grpc.h> #include <grpc/grpc.h>
#include <grpc/status.h> #include <grpc/status.h>
#include <grpc/impl/codegen/grpc_types.h> #include <grpc/impl/codegen/grpc_types.h>
#include <grpc_haskell.h> #include <grpc_haskell.h>
-- | Represents metadata for a given RPC, consisting of key-value pairs. Keys
-- are allowed to be repeated. Since repeated keys are unlikely in practice,
-- the 'IsList' instance uses key-value pairs as items. For example,
-- @fromList [("key1","val1"),("key2","val2"),("key1","val3")]@.
newtype MetadataMap = MetadataMap {unMap :: M.Map ByteString (SL.SortedList ByteString)}
deriving Eq
instance Show MetadataMap where
show m = "fromList " ++ show (M.toList (unMap m))
instance Monoid MetadataMap where
mempty = MetadataMap $ M.empty
mappend (MetadataMap m1) (MetadataMap m2) =
MetadataMap $ M.unionWith mappend m1 m2
instance IsList MetadataMap where
type Item MetadataMap = (ByteString, ByteString)
fromList = MetadataMap
. M.fromList
. map (\xs -> ((fst . head) xs, fromList $ map snd xs))
. groupBy ((==) `on` fst)
. sortBy (comparing fst)
toList = concatMap (\(k,vs) -> map (k,) vs)
. map (fmap toList)
. M.toList
. unMap
-- | Represents a pointer to one or more metadata key/value pairs. This type -- | Represents a pointer to one or more metadata key/value pairs. This type
-- is intended to be used when sending metadata. -- is intended to be used when sending metadata.
{#pointer *grpc_metadata as MetadataKeyValPtr newtype#} {#pointer *grpc_metadata as MetadataKeyValPtr newtype#}
@ -87,22 +121,22 @@ getMetadataKey m = getMetadataKey' m >=> packCString
getMetadataVal :: MetadataKeyValPtr -> Int -> IO ByteString getMetadataVal :: MetadataKeyValPtr -> Int -> IO ByteString
getMetadataVal m = getMetadataVal' m >=> packCString getMetadataVal m = getMetadataVal' m >=> packCString
createMetadata :: M.Map ByteString ByteString -> IO MetadataKeyValPtr createMetadata :: MetadataMap -> IO MetadataKeyValPtr
createMetadata m = do createMetadata m = do
let l = M.size m let indexedKeyVals = zip [0..] $ toList m
let indexedKeyVals = zip [0..] $ M.toList m l = length indexedKeyVals
metadata <- metadataAlloc l metadata <- metadataAlloc l
forM_ indexedKeyVals $ \(i,(k,v)) -> setMetadataKeyVal k v metadata i forM_ indexedKeyVals $ \(i,(k,v)) -> setMetadataKeyVal k v metadata i
return metadata return metadata
getAllMetadataArray :: MetadataArray -> IO (M.Map ByteString ByteString) getAllMetadataArray :: MetadataArray -> IO MetadataMap
getAllMetadataArray m = do getAllMetadataArray m = do
kvs <- metadataArrayGetMetadata m kvs <- metadataArrayGetMetadata m
l <- metadataArrayGetCount m l <- metadataArrayGetCount m
getAllMetadata kvs l getAllMetadata kvs l
getAllMetadata :: MetadataKeyValPtr -> Int -> IO (M.Map ByteString ByteString) getAllMetadata :: MetadataKeyValPtr -> Int -> IO MetadataMap
getAllMetadata m count = do getAllMetadata m count = do
let indices = [0..count-1] let indices = [0..count-1]
fmap M.fromList $ forM indices $ fmap fromList $ forM indices $
\i -> liftM2 (,) (getMetadataKey m i) (getMetadataVal m i) \i -> liftM2 (,) (getMetadataKey m i) (getMetadataVal m i)

View file

@ -2,7 +2,7 @@
# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/ # For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2) # Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
resolver: lts-5.10 resolver: lts-6.9
# Local packages, usually specified by relative directory name # Local packages, usually specified by relative directory name
packages: packages:

View file

@ -19,7 +19,9 @@ import Control.Monad.Managed
import Data.ByteString (ByteString, import Data.ByteString (ByteString,
isPrefixOf, isPrefixOf,
isSuffixOf) isSuffixOf)
import qualified Data.Map as M import qualified Data.Map.Strict as M
import qualified Data.Set as S
import GHC.Exts (fromList, toList)
import Network.GRPC.LowLevel import Network.GRPC.LowLevel
import Network.GRPC.LowLevel.GRPC (threadDelaySecs) import Network.GRPC.LowLevel.GRPC (threadDelaySecs)
import qualified Network.GRPC.LowLevel.Call.Unregistered as U import qualified Network.GRPC.LowLevel.Call.Unregistered as U
@ -130,7 +132,9 @@ testPayload :: TestTree
testPayload = testPayload =
csTest "registered normal request/response" client server (["/foo"],[],[],[]) csTest "registered normal request/response" client server (["/foo"],[],[],[])
where where
clientMD = [("foo_key", "foo_val"), ("bar_key", "bar_val")] clientMD = [ ("foo_key", "foo_val")
, ("bar_key", "bar_val")
, ("bar_key", "bar_repeated_val")]
client c = do client c = do
rm <- clientRegisterMethodNormal c "/foo" rm <- clientRegisterMethodNormal c "/foo"
clientRequest c rm 10 "Hello!" clientMD >>= do clientRequest c rm 10 "Hello!" clientMD >>= do
@ -452,7 +456,10 @@ testCustomUserAgent =
server = TestServer (serverConf (["/foo"],[],[],[])) $ \s -> do server = TestServer (serverConf (["/foo"],[],[],[])) $ \s -> do
let rm = head (normalMethods s) let rm = head (normalMethods s)
serverHandleNormalCall s rm mempty $ \c -> do serverHandleNormalCall s rm mempty $ \c -> do
let ua = metadata c M.! "user-agent" ua <- case toList $ (unMap $ metadata c) M.! "user-agent" of
[] -> fail "user-agent missing from metadata."
[ua] -> return ua
_ -> fail "multiple user-agent keys."
assertBool "User agent prefix is present" $ isPrefixOf "prefix!" ua assertBool "User agent prefix is present" $ isPrefixOf "prefix!" ua
assertBool "User agent suffix is present" $ isSuffixOf "suffix!" ua assertBool "User agent suffix is present" $ isSuffixOf "suffix!" ua
return dummyResp return dummyResp
@ -510,7 +517,7 @@ testClientServerCompression =
is :: (Eq a, Show a, MonadIO m) => m a -> a -> m () is :: (Eq a, Show a, MonadIO m) => m a -> a -> m ()
is act x = act >>= liftIO . (@?= x) is act x = act >>= liftIO . (@?= x)
dummyMeta :: M.Map ByteString ByteString dummyMeta :: MetadataMap
dummyMeta = [("foo","bar")] dummyMeta = [("foo","bar")]
dummyResp :: (ByteString, MetadataMap, StatusCode, StatusDetails) dummyResp :: (ByteString, MetadataMap, StatusCode, StatusDetails)
@ -565,10 +572,10 @@ csTest' nm tc ts =
-- @actual@, or when values differ for matching keys. -- @actual@, or when values differ for matching keys.
checkMD :: String -> MetadataMap -> MetadataMap -> Assertion checkMD :: String -> MetadataMap -> MetadataMap -> Assertion
checkMD desc expected actual = checkMD desc expected actual =
unless (M.null $ expected `diff` actual) $ assertEqual desc expected' (actual' `S.intersection` expected')
assertEqual desc expected (actual `M.intersection` expected)
where where
diff = M.differenceWith $ \a b -> if a == b then Nothing else Just b expected' = fromList . toList $ expected
actual' = fromList . toList $ actual
checkReqRslt :: Show a => (b -> Assertion) -> Either a b -> Assertion checkReqRslt :: Show a => (b -> Assertion) -> Either a b -> Assertion
checkReqRslt = either clientFail checkReqRslt = either clientFail

View file

@ -7,6 +7,7 @@ import GeneratedTests
main :: IO () main :: IO ()
main = defaultMain $ testGroup "GRPC Unit Tests" main = defaultMain $ testGroup "GRPC Unit Tests"
[ unsafeTests [ unsafeTests
, unsafeProperties
, lowLevelOpTests , lowLevelOpTests
, lowLevelTests , lowLevelTests
, generatedTests , generatedTests

View file

@ -1,6 +1,7 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module UnsafeTests (unsafeTests) where module UnsafeTests (unsafeTests, unsafeProperties) where
import Control.Concurrent (threadDelay) import Control.Concurrent (threadDelay)
import Control.Exception (bracket_) import Control.Exception (bracket_)
@ -8,6 +9,7 @@ import Control.Monad
import qualified Data.ByteString as B import qualified Data.ByteString as B
import Foreign.Marshal.Alloc import Foreign.Marshal.Alloc
import Foreign.Storable import Foreign.Storable
import GHC.Exts
import Network.GRPC.LowLevel.GRPC (threadDelaySecs) import Network.GRPC.LowLevel.GRPC (threadDelaySecs)
import Network.GRPC.Unsafe import Network.GRPC.Unsafe
import Network.GRPC.Unsafe.ByteBuffer import Network.GRPC.Unsafe.ByteBuffer
@ -17,6 +19,9 @@ import Network.GRPC.Unsafe.Time
import Network.GRPC.Unsafe.ChannelArgs import Network.GRPC.Unsafe.ChannelArgs
import System.Clock import System.Clock
import Test.Tasty import Test.Tasty
import Test.Tasty.HUnit as HU (testCase, (@?=),
assertBool)
import Test.Tasty.QuickCheck as QC
import Test.Tasty.HUnit as HU (testCase, (@?=)) import Test.Tasty.HUnit as HU (testCase, (@?=))
unsafeTests :: TestTree unsafeTests :: TestTree
@ -35,6 +40,20 @@ unsafeTests = testGroup "Unit tests for unsafe C bindings"
, testCreateDestroyChannelArgs , testCreateDestroyChannelArgs
] ]
unsafeProperties :: TestTree
unsafeProperties = testGroup "QuickCheck properties for unsafe C bindings"
[ metadataIsList ]
instance Arbitrary B.ByteString where
arbitrary = B.pack <$> arbitrary
instance Arbitrary MetadataMap where
arbitrary = fromList <$> arbitrary
metadataIsList :: TestTree
metadataIsList = QC.testProperty "Metadata IsList instance" $
\(md :: MetadataMap) -> md == (fromList $ toList md)
largeByteString :: B.ByteString largeByteString :: B.ByteString
largeByteString = B.pack $ take (32*1024*1024) $ cycle [97..99] largeByteString = B.pack $ take (32*1024*1024) $ cycle [97..99]