packages feed

curl-runnings 0.16.4 → 0.17.0

raw patch · 9 files changed

+122/−105 lines, 9 filesdep −timedep ~aesonPVP ok

version bump matches the API change (PVP)

Dependencies removed: time

Dependency ranges changed: aeson

API changes (from Hackage documentation)

+ Testing.CurlRunnings.Internal.Aeson: findWithDefault :: a -> KeyType -> MapType a -> a
+ Testing.CurlRunnings.Internal.Aeson: fromText :: Text -> Key
+ Testing.CurlRunnings.Internal.Aeson: toText :: Key -> Text
+ Testing.CurlRunnings.Internal.Aeson: type KeyType = Key
+ Testing.CurlRunnings.Internal.Aeson: type MapType v = KeyMap v
+ Testing.CurlRunnings.Internal.KeyValuePairs: instance GHC.Classes.Eq Testing.CurlRunnings.Internal.KeyValuePairs.KeyValuePair
+ Testing.CurlRunnings.Internal.KeyValuePairs: instance GHC.Classes.Eq Testing.CurlRunnings.Internal.KeyValuePairs.KeyValuePairs
- Testing.CurlRunnings.Internal.KeyValuePairs: KeyValuePair :: Text -> Text -> KeyValuePair
+ Testing.CurlRunnings.Internal.KeyValuePairs: KeyValuePair :: KeyType -> Text -> KeyValuePair
- Testing.CurlRunnings.Types: KeyValuePair :: Text -> Text -> KeyValuePair
+ Testing.CurlRunnings.Types: KeyValuePair :: KeyType -> Text -> KeyValuePair

Files

curl-runnings.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 3c27906a20aba4f4ab885acf4616178457bdbd82ce1dcd57942ff7e06a0bba25+-- hash: f7b42ddbfbcd70bee034e064181b72a998e326a895efdf2c524583c4f47131ca  name:           curl-runnings-version:        0.16.4+version:        0.17.0 synopsis:       A framework for declaratively writing curl based API tests description:    Please see the README on Github at <https://github.com/aviaviavi/curl-runnings#readme> category:       Testing@@ -37,6 +37,7 @@       Testing.CurlRunnings       Testing.CurlRunnings.Types       Testing.CurlRunnings.Internal+      Testing.CurlRunnings.Internal.Aeson       Testing.CurlRunnings.Internal.Parser       Testing.CurlRunnings.Internal.Headers       Testing.CurlRunnings.Internal.KeyValuePairs@@ -46,7 +47,7 @@   hs-source-dirs:       src   build-depends:-      aeson >=1.2.4.0 && <2.0+      aeson >=1.2.4.0     , base >=4.0 && <5     , base64-bytestring >=1.0.0.2     , bytestring >=0.10.8.2@@ -57,8 +58,6 @@     , dhall-json >=1.0.9     , directory >=1.3.0.2     , hashable >=1.2.7.0-    , hspec >=2.4.4-    , hspec-expectations >=0.8.2     , http-client-tls >=0.3.5.3     , http-conduit >=2.3.6     , http-types >=0.12.3@@ -66,7 +65,6 @@     , pretty-simple >=2.0.2.1     , regex-posix >=0.95.2     , text >=1.2.2.2-    , time >=1.8.0.2     , transformers >=0.5.2.0     , unordered-containers >=0.2.8.0     , vector >=0.12.0@@ -81,7 +79,7 @@       app   ghc-options: -threaded -rtsopts -with-rtsopts=-N   build-depends:-      aeson >=1.2.4.0 && <2.0+      aeson >=1.2.4.0     , base >=4.7     , bytestring >=0.10.8.2     , cmdargs >=0.10.20@@ -102,7 +100,7 @@       test   ghc-options: -threaded -rtsopts -with-rtsopts=-N   build-depends:-      aeson >=1.2.4.0 && <2.0+      aeson >=1.2.4.0     , base >=4.0 && <5     , bytestring >=0.10.8.2     , curl-runnings
src/Testing/CurlRunnings.hs view
@@ -15,7 +15,6 @@  import           Control.Arrow import           Control.Exception-import qualified Control.Exception import           Control.Monad import           Control.Monad.IO.Class import           Control.Monad.Trans.Except@@ -26,19 +25,13 @@ import qualified Data.ByteString.Lazy                 as B import qualified Data.CaseInsensitive                 as CI import           Data.Either-import qualified Data.HashMap.Strict                  as H-import           Data.List+import           Data.List                            (find) import           Data.Maybe-import           Data.Monoid+import           Data.String                          (fromString) import qualified Data.Text                            as T import qualified Data.Text.Encoding                   as T import qualified Data.Text.IO                         as TIO-import qualified Data.Text.Lazy                       as TL-import qualified Data.Text.Lazy.IO                    as TLIO-import           Data.Time.Clock import qualified Data.Vector                          as V-import qualified Data.Vector                          as V-import qualified Data.Yaml                            as Y import qualified Data.Yaml.Include                    as YI import qualified Dhall import qualified Dhall.Import@@ -46,7 +39,6 @@ import qualified Dhall.Parser import qualified Dhall.TypeCheck import           Network.Connection                   (TLSSettings (..))-import           Network.HTTP.Client.TLS              (mkManagerSettings) import           Network.HTTP.Conduit import           Network.HTTP.Simple                  hiding (Header) import qualified Network.HTTP.Simple                  as HTTP@@ -54,6 +46,7 @@ import           System.Directory import           System.Environment import           Testing.CurlRunnings.Internal+import qualified Testing.CurlRunnings.Internal.Aeson  as A import           Testing.CurlRunnings.Internal.Parser import           Testing.CurlRunnings.Types import           Text.Printf@@ -110,7 +103,7 @@ appendQueryParameters :: [KeyValuePair] -> Request -> Request appendQueryParameters newParams r = setQueryString (existing ++ newQuery) r where   existing = NT.parseQuery $ queryString r-  newQuery = NT.simpleQueryToQuery $ fmap (\(KeyValuePair k v) -> (T.encodeUtf8 k, T.encodeUtf8 v)) newParams+  newQuery = NT.simpleQueryToQuery $ fmap (\(KeyValuePair k v) -> (T.encodeUtf8 . A.toText $ k, T.encodeUtf8 v)) newParams  setPayload :: Maybe Payload -> Request -> Request -- TODO - for backwards compatability, empty requests will set an empty json@@ -121,7 +114,7 @@ setPayload Nothing = setRequestBodyJSON emptyObject setPayload (Just (JSON v)) = setRequestBodyJSON v setPayload (Just (URLEncoded (KeyValuePairs xs))) = setRequestBodyURLEncoded $ kvpairs xs where-  kvpairs = fmap (\(KeyValuePair k v) -> (T.encodeUtf8 k, T.encodeUtf8 v))+  kvpairs = fmap (\(KeyValuePair k v) -> (T.encodeUtf8 . A.toText $ k, T.encodeUtf8 v))  -- | Run a single test case, and returns the result. IO is needed here since this method is responsible -- for actually curling the test case endpoint and parsing the result.@@ -249,15 +242,17 @@         (Just (BasicAuthentication u p)) ->           let interpolated = sequence [interpolateQueryString state u, interpolateQueryString state p] in           case interpolated of-            Right [u, p] -> Right [Header "Authorization" ("Basic " <> (makeBasicAuthToken u p))]+            Right [u', p'] -> Right [Header "Authorization" ("Basic " <> (makeBasicAuthToken u' p'))]             Left l -> Left l-        Nothing -> Right []+            _ -> Left $ QueryValidationError "FIXME Pattern match error in interpolatingHeaders"+        _ -> Right []   mainHeaders <- (mapM         (\(Header k v) ->           case sequence                   [interpolateQueryString state k, interpolateQueryString state v] of             Left err       -> Left err-            Right [k', v'] -> Right $ Header k' v')+            Right [k', v'] -> Right $ Header k' v'+            _ -> Left $ QueryValidationError "FIXME Pattern match error in interpolatingHeaders")       headerList)   Right . HeaderSet $ mainHeaders <> authHeaders @@ -282,7 +277,8 @@ runSuite :: CurlSuite -> LogLevel -> TLSCheckType -> IO [CaseResult] runSuite (CurlSuite cases filterRegex) logLevel tlsType = do   fullEnv <- getEnvironment-  let envMap = H.fromList $ map (\(x, y) -> (T.pack x, T.pack y)) fullEnv+  let envMap :: A.MapType T.Text+      envMap = A.fromList $ map (\(x, y) -> (fromString x :: A.KeyType, T.pack y)) fullEnv :: A.MapType T.Text       filterNameByRegexp curlCase =         maybe           True@@ -389,19 +385,19 @@ -- | runReplacements runReplacements :: CurlRunningsState -> Value -> Either QueryError Value runReplacements state (Object o) =-  let keys = H.keys o+  let keys = A.keys o       keysWithUpdatedKeyVal =         map           (\key ->-             let value = fromJust $ H.lookup key o+             let value = fromJust $ A.lookup key o               -- (old key, new key, new value)              in ( key-                , interpolateQueryString state key+                , interpolateQueryString state (A.toText key)                 , runReplacements state value))           keys   in mapRight Object $      foldr-       (\((key, eKeyResult, eValueResult) :: ( T.Text+       (\((key, eKeyResult, eValueResult) :: ( A.KeyType                                              , Either QueryError T.Text                                              , Either QueryError Value)) (eObjectToUpdate :: Either QueryError Object) ->           case (eKeyResult, eValueResult, eObjectToUpdate)@@ -412,11 +408,12 @@             (_, Left queryErr, _) -> Left queryErr             (_, _, Left queryErr) -> Left queryErr             (Right newKey, Right newValue, Right objectToUpdate) ->-              if key /= newKey-                then let inserted = H.insert newKey newValue objectToUpdate-                         deleted = H.delete key inserted+              let newKey' = A.fromText newKey+              in if key /= newKey'+                then let inserted = A.insert newKey' newValue objectToUpdate+                         deleted = A.delete key inserted                      in Right deleted-                else Right $ H.insert key newValue objectToUpdate)+                else Right $ A.insert key newValue objectToUpdate)        (Right o)        keysWithUpdatedKeyVal runReplacements p (Array a) =@@ -442,9 +439,9 @@ interpolateViaJSON :: (ToJSON a, FromJSON a) => CurlRunningsState -> a -> Either QueryError a interpolateViaJSON state i = do   replaced <- runReplacements state $ toJSON i-  resultToEither $ fromJSON replaced where-    resultToEither (Error e)   = Left $ QueryValidationError $ T.pack e-    resultToEither (Success a) = Right a+  resultToEither' $ fromJSON replaced where+    resultToEither' (Error e)   = Left $ QueryValidationError $ T.pack e+    resultToEither' (Success a) = Right a  -- | Given a query string, return some text with interpolated values. Type -- errors will be returned if queries don't resolve to strings@@ -474,7 +471,7 @@     Right (String s) -> Right $ rawText <> s     (Right o)        -> Left $ QueryTypeMismatch "Expected a string" o getStringValueForQuery (CurlRunningsState env _ _ _) (InterpolatedQuery rawText (EnvironmentVariable v)) =-  Right $ rawText <> H.lookupDefault "" v env+  Right $ rawText <> A.findWithDefault "" (A.fromText v) env  -- | Lookup the value for the specified query getValueForQuery ::@@ -499,7 +496,7 @@                         case (eitherVal, index) of                           (Left l, _) -> Left l                           (Right (Object o), KeyIndex k) ->-                            Right $ H.lookupDefault Null k o+                            Right $ A.findWithDefault Null (A.fromText k :: A.KeyType) o                           (Right (Array a), ArrayIndex i') ->                             maybe                               (Left $@@ -524,7 +521,7 @@       "'$< ... >' queries must start with a RESPONSES[index] query: " ++       show full getValueForQuery (CurlRunningsState env _ _ _) (NonInterpolatedQuery (EnvironmentVariable var)) =-  Right . String $ H.lookupDefault "" var env+  Right . String $ A.findWithDefault "" (A.fromText var) env getValueForQuery state (InterpolatedQuery _ q) =   case getValueForQuery state (NonInterpolatedQuery q) of     Right (String s) -> Right . String $ s@@ -539,12 +536,12 @@   -> Bool jsonContains f jsonValue =   let traversedValue = traverseValue jsonValue-  in f $ \match ->-       case match of+  in f $ \match' ->+       case match' of          ValueMatch subval -> subval `elem` traversedValue-         KeyMatch key -> any (`containsKey` key) traversedValue+         KeyMatch key -> any (`containsKey` (A.fromText key)) traversedValue          KeyValueMatch key subval ->-           any (\o -> containsKeyVal o key subval) traversedValue+           any (\o -> containsKeyVal o (A.fromText key) subval) traversedValue  -- | Does the json value contain all of these sub-values? jsonContainsAll :: Value -> [JsonSubExpr] -> Bool@@ -555,24 +552,24 @@ jsonContainsAny = jsonContains any  -- | Does the json value contain the given key value pair?-containsKeyVal :: Value -> T.Text -> Value -> Bool+containsKeyVal :: Value -> A.KeyType -> Value -> Bool containsKeyVal jsonValue key val =   case jsonValue of-    Object o -> H.lookup key o == Just val+    Object o -> A.lookup key o == Just val     _        -> False  -- | Does the json value contain the given key value pair?-containsKey :: Value -> T.Text -> Bool+containsKey :: Value -> A.KeyType -> Bool containsKey jsonValue key =   case jsonValue of-    Object o -> isJust $ H.lookup key o+    Object o -> isJust $ A.lookup key o     _        -> False  -- | Fully traverse the json and return a list of all the values traverseValue :: Value -> [Value] traverseValue val =   case val of-    Object o     -> val : concatMap traverseValue (H.elems o)+    Object o     -> val : concatMap traverseValue (A.elems o)     Array o      -> val : concatMap traverseValue o     n@(Number _) -> [n]     s@(String _) -> [s]
src/Testing/CurlRunnings/Internal.hs view
@@ -20,15 +20,12 @@   ) where  import           Control.Monad-import           Data.Monoid import qualified Data.Text          as T import qualified Data.Text.Lazy     as TL import           Debug.Trace import           System.Clock import qualified Text.Pretty.Simple as P-import           Text.Printf - makeGreen :: T.Text -> T.Text makeGreen s = "\x1B[32m" <> s <> "\x1B[0m" @@ -83,9 +80,6 @@ nowMillis = do   t <- getTime Realtime   return $ (toNanoSecs t) `div` 1000000--roundToStr :: (PrintfArg a, Floating a) => a -> String-roundToStr = printf "%0.2f"  millisToS :: Integer -> Double millisToS t = (fromIntegral t :: Double) / 1000.0
+ src/Testing/CurlRunnings/Internal/Aeson.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE CPP #-}++-- | Module for the purpose of Aeson compatibility+module Testing.CurlRunnings.Internal.Aeson+  ( module Map+  , KeyType+  , MapType+  , findWithDefault+  , fromText+  , toText+  ) where++#if MIN_VERSION_aeson(2,0,0)++import Data.Aeson.KeyMap as Map+import Data.Aeson.Key (Key, fromText, toText)++type MapType v = Map.KeyMap v+type KeyType = Key++#else++import Data.Text (Text)+import Data.HashMap.Strict as Map hiding (findWithDefault)++type MapType v = Map.HashMap Text v+type KeyType = Text++fromText :: Text -> Text+fromText = id++toText :: Text -> Text+toText = id++#endif++findWithDefault :: a -> KeyType -> MapType a -> a+findWithDefault def k kv = case Map.lookup k kv of+    Just v -> v+    _      -> def+{-# INLINABLE findWithDefault #-}
src/Testing/CurlRunnings/Internal/KeyValuePairs.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings  #-}+{-# LANGUAGE StandaloneDeriving #-}  -- | A module defining the KeyValuePairs type. This type may be used to -- represent a structure in a specification that is a collection of@@ -17,24 +18,31 @@ import           Data.Aeson import           Data.Aeson.Types import qualified Data.ByteString.Lazy as LBS-import           Data.HashMap.Strict  as H+import           Data.List            ((\\)) import qualified Data.Text            as T import           Data.Text.Encoding   as T+import qualified Testing.CurlRunnings.Internal.Aeson as A  -- | A container for a list of key-value pairs newtype KeyValuePairs = KeyValuePairs [KeyValuePair] deriving Show  -- | A representation of a single key-value pair-data KeyValuePair = KeyValuePair T.Text T.Text deriving Show+data KeyValuePair = KeyValuePair A.KeyType T.Text deriving Show +deriving instance Eq KeyValuePair++-- KeyValuePairs should be considered equal if they contain the same elements.+instance Eq KeyValuePairs where+  (==) (KeyValuePairs x) (KeyValuePairs y) = null (x \\ y) && null (y \\ x)+ instance ToJSON KeyValuePairs where   toJSON (KeyValuePairs qs) =     object (fmap (\(KeyValuePair k v) -> k .= toJSON v) qs)  instance FromJSON KeyValuePairs where   parseJSON = withObject "keyValuePairs" parseKeyValuePairs where-    parseKeyValuePairs o = KeyValuePairs <$> traverse parseKeyValuePair (H.toList o)-    parseKeyValuePair (t, v) = KeyValuePair t <$> parseSingleValueType v+    parseKeyValuePairs o = KeyValuePairs <$> traverse parseKeyValuePair (A.toList o)+    parseKeyValuePair (k, v) = KeyValuePair k <$> parseSingleValueType v  parseSingleValueType :: Value -> Parser T.Text parseSingleValueType (Bool b)   = parseToText b
src/Testing/CurlRunnings/Internal/Parser.hs view
@@ -34,9 +34,9 @@       parseQuery     ) where -import           Data.Bifunctor             (Bifunctor (..))-import           Data.Char                  (isAscii)-import           Data.List+import           Data.Bifunctor             ()+import           Data.Char                  ()+import           Data.List                  (isInfixOf) import qualified Data.Text                  as T import           Data.Void import           Testing.CurlRunnings.Types
src/Testing/CurlRunnings/Internal/Payload.hs view
@@ -53,28 +53,28 @@  import           Data.Aeson import qualified Data.Char                                   as C-import qualified Data.HashMap.Strict                         as H import qualified Data.Text                                   as T import           GHC.Generics+import qualified Testing.CurlRunnings.Internal.Aeson         as A import           Testing.CurlRunnings.Internal.KeyValuePairs  data Payload = JSON Value | URLEncoded KeyValuePairs deriving Generic  instance Show Payload where   show (JSON v) = show v-  show (URLEncoded (KeyValuePairs xs)) = T.unpack $ T.intercalate "&" $ fmap (\(KeyValuePair k v) -> k <> "=" <> v) xs+  show (URLEncoded (KeyValuePairs xs)) = T.unpack $ T.intercalate "&" $ fmap (\(KeyValuePair k v) -> A.toText k <> "=" <> v) xs -payloadTagFieldName :: T.Text+payloadTagFieldName :: A.KeyType payloadTagFieldName = "bodyType" -payloadContentsFieldName :: T.Text+payloadContentsFieldName :: A.KeyType payloadContentsFieldName = "content"  instance FromJSON Payload where   parseJSON v = withObject "payload" parsePayload v where-    parsePayload o = if not (H.member payloadTagFieldName o) then return (JSON v) else genericParseJSON payloadOptions v-    payloadOptions = defaultOptions { sumEncoding = TaggedObject { tagFieldName = T.unpack payloadTagFieldName-                                                                 , contentsFieldName = T.unpack payloadContentsFieldName+    parsePayload o = if not (A.member payloadTagFieldName o) then return (JSON v) else genericParseJSON payloadOptions v+    payloadOptions = defaultOptions { sumEncoding = TaggedObject { tagFieldName = T.unpack . A.toText $ payloadTagFieldName+                                                                 , contentsFieldName = T.unpack . A.toText $ payloadContentsFieldName                                                                  }                                     , constructorTagModifier = fmap C.toLower                                     }
src/Testing/CurlRunnings/Types.hs view
@@ -40,15 +40,12 @@  import           Data.Aeson import           Data.Aeson.Types-import qualified Data.Char                                   as C-import           Data.Hashable                               (Hashable)-import qualified Data.HashMap.Strict                         as H import           Data.Maybe-import           Data.Monoid import qualified Data.Text                                   as T import qualified Data.Vector                                 as V import           GHC.Generics import           Testing.CurlRunnings.Internal+import qualified Testing.CurlRunnings.Internal.Aeson         as A import           Testing.CurlRunnings.Internal.Headers import           Testing.CurlRunnings.Internal.KeyValuePairs import           Testing.CurlRunnings.Internal.Payload@@ -92,9 +89,9 @@     | justAndNotEmpty "notContains" v = NotContains <$> v .: "notContains"   parseJSON invalid = typeMismatch "JsonMatcher" invalid -justAndNotEmpty :: (Eq k, Hashable k) => k -> H.HashMap k Value -> Bool+justAndNotEmpty :: A.KeyType -> A.MapType Value -> Bool justAndNotEmpty key obj =-  (isJust $ H.lookup key obj) && (H.lookup key obj /= Just Null)+  isJust (A.lookup key obj) && A.lookup key obj /= Just Null  -- | Simple predicate to check value constructor type isContains :: JsonMatcher -> Bool@@ -177,12 +174,12 @@ instance FromJSON JsonSubExpr where   parseJSON (Object v)     | justAndNotEmpty "keyValueMatch" v =-      let toParse = fromJust $ H.lookup "keyValueMatch" v+      let toParse = fromJust $ A.lookup "keyValueMatch" v       in case toParse of            Object o -> KeyValueMatch <$> o .: "key" <*> o .: "value"            _        -> typeMismatch "JsonSubExpr" toParse     | justAndNotEmpty "keyMatch" v =-      let toParse = fromJust $ H.lookup "keyMatch" v+      let toParse = fromJust $ A.lookup "keyMatch" v       in case toParse of            String s -> return $ KeyMatch s            _        -> typeMismatch "JsonSubExpr" toParse@@ -403,7 +400,7 @@ isFailing = not . isPassing  -- | A map of the system environment-type Environment = H.HashMap T.Text T.Text+type Environment = A.MapType T.Text  data TLSCheckType = SkipTLSCheck | DoTLSCheck deriving (Show, Eq) @@ -428,11 +425,6 @@   | ArrayIndex Integer   deriving (Show) -printOriginalQuery :: Index -> String-printOriginalQuery (CaseResultIndex t) = "RESPONSES[" ++ show t ++ "]"-printOriginalQuery (KeyIndex key)      = "." ++ T.unpack key-printOriginalQuery (ArrayIndex i)      = printf "[%d]" i- -- | A single entity to be queries from a json value data Query =   -- | A single query contains a list of discrete index operations@@ -451,12 +443,6 @@   -- | Just a query, no leading text   | NonInterpolatedQuery Query   deriving (Show)--printQueryString :: InterpolatedQuery -> String-printQueryString (LiteralText t) = show t-printQueryString (InterpolatedQuery raw (Query indexes)) =-  printf "%s$<%s>" raw $ concatMap show indexes-printQueryString (NonInterpolatedQuery (Query indexes)) = printf "$<%s>" (concatMap show indexes)  -- | The full string in which a query appears, eg "prefix-${{RESPONSES[0].key.another_key[0].last_key}}" type FullQueryText = T.Text
test/Spec.hs view
@@ -1,18 +1,17 @@ {-# LANGUAGE OverloadedStrings  #-} {-# LANGUAGE QuasiQuotes        #-}-{-# LANGUAGE StandaloneDeriving #-}  module Main where  import           Data.Aeson import           Data.ByteString                             (ByteString) import           Data.Either-import           Data.List import qualified Data.Text                                   as T import           System.Directory import           Test.Hspec import           Testing.CurlRunnings import           Testing.CurlRunnings.Internal+import qualified Testing.CurlRunnings.Internal.Aeson         as A import           Testing.CurlRunnings.Internal.Headers import           Testing.CurlRunnings.Internal.KeyValuePairs import           Testing.CurlRunnings.Internal.Parser@@ -92,12 +91,12 @@   it "arrayGet should handle positive and negative indexes correctly" $ do     let a = [1, 2, 3]         b = [] :: [Int]-    (arrayGet a 0) `shouldBe` Just 1-    (arrayGet a 1) `shouldBe` Just 2-    (arrayGet a 2) `shouldBe` Just 3-    (arrayGet a (-1)) `shouldBe` Just 3-    (arrayGet a (-2)) `shouldBe` Just 2-    (arrayGet a (-3)) `shouldBe` Just 1+    (arrayGet a 0) `shouldBe` Just (1 :: Integer)+    (arrayGet a 1) `shouldBe` Just (2 :: Integer)+    (arrayGet a 2) `shouldBe` Just (3 :: Integer)+    (arrayGet a (-1)) `shouldBe` Just (3 :: Integer)+    (arrayGet a (-2)) `shouldBe` Just (2 :: Integer)+    (arrayGet a (-3)) `shouldBe` Just (1 :: Integer)     (arrayGet a (-4)) `shouldBe` Nothing     (arrayGet a 3) `shouldBe` Nothing     (arrayGet b 0) `shouldBe` Nothing@@ -156,16 +155,10 @@   let expectedHeaders = Right . HeaderSet $ uncurry Header <$> expected in     actual `shouldBe` expectedHeaders -shouldBeKeyValuePairs :: ByteString -> [(T.Text, T.Text)] -> Expectation-shouldBeKeyValuePairs json expected = actual `shouldBe` expectedKeyValuePairs where-  actual = eitherDecodeStrict json+shouldBeKeyValuePairs :: ByteString -> [(A.KeyType, T.Text)] -> Expectation+shouldBeKeyValuePairs j expected = actual `shouldBe` expectedKeyValuePairs where+  actual = eitherDecodeStrict j   expectedKeyValuePairs = Right . KeyValuePairs $ uncurry KeyValuePair <$> expected--deriving instance Eq KeyValuePair---- KeyValuePairs should be considered equal if they contain the same elements.-instance Eq KeyValuePairs where-  (==) (KeyValuePairs x) (KeyValuePairs y) = null (x \\ y) && null (y \\ x)  isURLEncoded :: Either String Payload -> Bool isURLEncoded (Right (URLEncoded _)) = True