packages feed

hal 0.4.7 → 0.4.8

raw patch · 10 files changed

+699/−30 lines, 10 filesdep +hashabledep +hedgehogdep +hspec-hedgehogdep ~http-typesPVP ok

version bump matches the API change (PVP)

Dependencies added: hashable, hedgehog, hspec-hedgehog, scientific, transformers, vector

Dependency ranges changed: http-types

API changes (from Hackage documentation)

+ AWS.Lambda.Events.ApiGateway.ProxyRequest: instance Data.Aeson.Types.ToJSON.ToJSON AWS.Lambda.Events.ApiGateway.ProxyRequest.Identity
+ AWS.Lambda.Events.ApiGateway.ProxyRequest: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (AWS.Lambda.Events.ApiGateway.ProxyRequest.ProxyRequest a)
+ AWS.Lambda.Events.ApiGateway.ProxyRequest: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (AWS.Lambda.Events.ApiGateway.ProxyRequest.RequestContext a)
+ AWS.Lambda.Events.ApiGateway.ProxyRequest: instance GHC.Classes.Eq AWS.Lambda.Events.ApiGateway.ProxyRequest.Identity
+ AWS.Lambda.Events.ApiGateway.ProxyRequest: instance GHC.Classes.Eq a => GHC.Classes.Eq (AWS.Lambda.Events.ApiGateway.ProxyRequest.ProxyRequest a)
+ AWS.Lambda.Events.ApiGateway.ProxyRequest: instance GHC.Classes.Eq a => GHC.Classes.Eq (AWS.Lambda.Events.ApiGateway.ProxyRequest.RequestContext a)
+ AWS.Lambda.Events.ApiGateway.ProxyRequest: instance GHC.Generics.Generic (AWS.Lambda.Events.ApiGateway.ProxyRequest.RequestContext a)
+ AWS.Lambda.Events.ApiGateway.ProxyRequest: instance GHC.Show.Show AWS.Lambda.Events.ApiGateway.ProxyRequest.Identity
+ AWS.Lambda.Events.ApiGateway.ProxyRequest: instance GHC.Show.Show a => GHC.Show.Show (AWS.Lambda.Events.ApiGateway.ProxyRequest.ProxyRequest a)
+ AWS.Lambda.Events.ApiGateway.ProxyRequest: instance GHC.Show.Show a => GHC.Show.Show (AWS.Lambda.Events.ApiGateway.ProxyRequest.RequestContext a)
+ AWS.Lambda.Events.ApiGateway.ProxyResponse: instance Data.Aeson.Types.FromJSON.FromJSON AWS.Lambda.Events.ApiGateway.ProxyResponse.ProxyResponse
+ AWS.Lambda.Events.ApiGateway.ProxyResponse: instance GHC.Classes.Eq AWS.Lambda.Events.ApiGateway.ProxyResponse.ProxyBody
+ AWS.Lambda.Events.ApiGateway.ProxyResponse: instance GHC.Classes.Eq AWS.Lambda.Events.ApiGateway.ProxyResponse.ProxyResponse
+ AWS.Lambda.Events.ApiGateway.ProxyResponse: instance GHC.Generics.Generic AWS.Lambda.Events.ApiGateway.ProxyResponse.ProxyBody
+ AWS.Lambda.Events.ApiGateway.ProxyResponse: instance GHC.Generics.Generic AWS.Lambda.Events.ApiGateway.ProxyResponse.ProxyResponse

Files

+ CHANGELOG.md view
@@ -0,0 +1,74 @@+# Change log++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog][chg] and this project adheres to+[Haskell's Package Versioning Policy][pvp]++## `0.4.8` - 2021-05-07++  - Add `ToJSON` instances for `ProxyRequest` types, and test+    round-tripping as much as possible+  - Add `FromJSON` instance for `ProxyResponse`, and test+    round-tripping as much as possible++## `0.4.7` - 2021-04-12++  - Drop http-conduit as a dependency for potentially smaller binaries+  - Add more detailed error messages to context parsing failures+  - Increase compatibility by loosening envy version bounds+  - Add mRuntimeWithContext' and mRuntime+  - Deprecate withIoInterface, withFallibleInterface, withPureInterface, HasLambdaContext, runRaderTLambdaContext, mRuntimeWithContext++## `0.4.2` - 2020-07-23++  - Fix an issue where errors from the runtime were not retried++## `0.4.1` - 2020-01-22++  - Constrain the envy version for correctly building with Cabal+  - Fix documentation examples for Runtime and Combinators++## `0.4.0` - 2020-01-22++  - Add support for Value-based runtimes for handling JSON conversion errors explicitly++## `0.3.1` - 2020-07-23++  - Fix an issue where errors from the runtime were not retried++## `0.3.0` - 2019-02-26++  - Add support for triggered S3 events++## `0.2.1` - 2020-07-23++  - Fix an issue where errors from the runtime were not retried++## `0.2.0` - 2019-02-07++  - Expose `AWS.Lambda.Combinators` package for building other runtimes+  (e.g. to support Lambda Event triggers)++## `0.1.3` - 2020-07-23++  - Fix an issue where errors from the runtime were not retried++## `0.1.2` - 2019-01-10++  - Fix PVP bounds when building/uploading the package++## `0.1.1` - 2019-01-08++### Fixed++  - Incorrect error message when getting events from AWS++## `0.1.0` - 2018-12-20++### Added++  - Initial release!++[chg]: http://keepachangelog.com+[pvp]: http://pvp.haskell.org
hal.cabal view
@@ -1,13 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.33.0.+-- This file has been generated from package.yaml by hpack version 0.34.4. -- -- see: https://github.com/sol/hpack ----- hash: ac72c27a2329613830cbbdeaf5c590a17af49cb9f6987d444b538ded08a8d451+-- hash: e76a8db81a6c7a721b849d1efca110b833ece7559f999740e5ac5c2d6cdd36e1  name:           hal-version:        0.4.7+version:        0.4.8 synopsis:       A runtime environment for Haskell applications running on AWS Lambda. description:    This library uniquely supports different types of AWS Lambda Handlers for your                 needs/comfort with advanced Haskell. Instead of exposing a single function@@ -23,6 +23,7 @@ build-type:     Simple extra-source-files:     README.md+    CHANGELOG.md  source-repository head   type: git@@ -45,7 +46,25 @@       Paths_hal   hs-source-dirs:       src-  default-extensions: OverloadedStrings BangPatterns DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable DuplicateRecordFields EmptyCase GeneralizedNewtypeDeriving InstanceSigs LambdaCase MultiParamTypeClasses MultiWayIf OverloadedStrings PatternSynonyms ScopedTypeVariables+  default-extensions:+      OverloadedStrings+      BangPatterns+      DefaultSignatures+      DeriveFoldable+      DeriveFunctor+      DeriveGeneric+      DeriveLift+      DeriveTraversable+      DuplicateRecordFields+      EmptyCase+      GeneralizedNewtypeDeriving+      InstanceSigs+      LambdaCase+      MultiParamTypeClasses+      MultiWayIf+      OverloadedStrings+      PatternSynonyms+      ScopedTypeVariables   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fno-warn-partial-type-signatures -fno-warn-name-shadowing -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-incomplete-patterns   build-depends:       aeson@@ -58,6 +77,7 @@     , containers     , envy     , exceptions+    , hashable     , http-client     , http-types     , mtl@@ -70,18 +90,51 @@   type: exitcode-stdio-1.0   main-is: Spec.hs   other-modules:+      AWS.Lambda.Events.ApiGateway.ProxyRequest.Gen+      AWS.Lambda.Events.ApiGateway.ProxyRequest.Gen.Parameters+      AWS.Lambda.Events.ApiGateway.ProxyRequest.Gen.Resource+      AWS.Lambda.Events.ApiGateway.ProxyResponse.Gen+      Gen.Header       Paths_hal   hs-source-dirs:       test-  default-extensions: OverloadedStrings BangPatterns DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable DuplicateRecordFields EmptyCase GeneralizedNewtypeDeriving InstanceSigs LambdaCase MultiParamTypeClasses MultiWayIf OverloadedStrings PatternSynonyms ScopedTypeVariables+  default-extensions:+      OverloadedStrings+      BangPatterns+      DefaultSignatures+      DeriveFoldable+      DeriveFunctor+      DeriveGeneric+      DeriveLift+      DeriveTraversable+      DuplicateRecordFields+      EmptyCase+      GeneralizedNewtypeDeriving+      InstanceSigs+      LambdaCase+      MultiParamTypeClasses+      MultiWayIf+      OverloadedStrings+      PatternSynonyms+      ScopedTypeVariables   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fno-warn-partial-type-signatures -fno-warn-name-shadowing -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-incomplete-patterns -threaded -rtsopts -with-rtsopts=-N   build-depends:       aeson     , base >=4.7 && <5+    , base64-bytestring+    , bytestring+    , case-insensitive     , containers     , hal+    , hedgehog >=1.0.3 && <1.1     , hspec+    , hspec-hedgehog     , http-client-    , http-types+    , http-types >=0.12.2 && <0.13+    , scientific+    , text     , time+    , transformers+    , unordered-containers+    , vector   default-language: Haskell2010
src/AWS/Lambda/Events/ApiGateway/ProxyRequest.hs view
@@ -18,13 +18,16 @@     , StrictlyNoAuthorizer     ) where -import           Data.Aeson                  (FromJSON, Value, parseJSON,-                                              withObject, (.:), (.:?))-import           Data.ByteString.Base64.Lazy (decodeLenient)+import           Data.Aeson                  (FromJSON(..), ToJSON(..),+                                              Value(..), object, withObject,+                                              (.=), (.:), (.:?))+import           Data.ByteString.Base64.Lazy (decodeLenient, encode) import           Data.ByteString.Lazy        (ByteString)-import           Data.CaseInsensitive        (CI, mk)+import           Data.CaseInsensitive        (CI, FoldCase, mk, original) import           Data.Foldable               (fold)+import           Data.Hashable               (Hashable) import           Data.HashMap.Strict         (HashMap, foldrWithKey, insert)+import           Data.Maybe                  (catMaybes) import           Data.Text                   (Text) import qualified Data.Text.Lazy              as TL import qualified Data.Text.Lazy.Encoding     as TLE@@ -52,9 +55,10 @@     , apiKeyId                      :: Maybe Text     , userAgent                     :: Maybe Text     , user                          :: Maybe Text-    } deriving (Generic)+    } deriving (Eq, Generic, Show)  instance FromJSON Identity+instance ToJSON Identity  data RequestContext a = RequestContext     { path              :: Text@@ -70,7 +74,7 @@     , httpMethod        :: Text     , extendedRequestId :: Maybe Text     , apiId             :: Text-    }+    } deriving (Eq, Generic, Show)  instance FromJSON a => FromJSON (RequestContext a) where     parseJSON = withObject "ProxyRequest" $ \v ->@@ -87,6 +91,24 @@         v .:? "extendedRequestId" <*>         v .: "apiId" +-- | @since 0.4.8+instance ToJSON a => ToJSON (RequestContext a) where+    toJSON r = object $ catMaybes+        [ Just $ "path" .= path (r :: RequestContext a)+        , Just $ "accountId" .= accountId (r :: RequestContext a)+        , ("authorizer" .=) <$> authorizer r+        , Just $ "resourceId" .= resourceId r+        , Just $ "stage" .= stage r+        , ("domainPrefix" .=) <$> domainPrefix r+        , Just $ "requestId" .= requestId r+        , Just $ "identity" .= identity r+        , ("domainName" .=) <$> domainName r+        , Just $ "resourcePath" .= resourcePath r+        , Just $ "httpMethod" .= httpMethod (r :: RequestContext a)+        , ("extendedRequestId" .=) <$> extendedRequestId r+        , Just $ "apiId" .= apiId r+        ]+ -- TODO: Should also include websocket fields -- | This type is for representing events that come from API Gateway via the -- Lambda Proxy integration (forwarding HTTP data directly, rather than a@@ -141,17 +163,23 @@     , queryStringParameters           :: HashMap Text Text     , multiValueQueryStringParameters :: HashMap Text [Text]     , body                            :: ByteString-    } deriving (Generic)+    } deriving (Eq, Generic, Show) -toCIHashMap :: HashMap Text a -> HashMap (CI Text) a+toCIHashMap :: (Eq k, FoldCase k, Hashable k) => HashMap k a -> HashMap (CI k) a toCIHashMap = foldrWithKey (insert . mk) mempty +fromCIHashMap :: (Eq k, Hashable k) => HashMap (CI k) a -> HashMap k a+fromCIHashMap = foldrWithKey (insert . original) mempty+ toByteString :: Bool -> TL.Text -> ByteString toByteString isBase64Encoded =     if isBase64Encoded         then decodeLenient . TLE.encodeUtf8         else TLE.encodeUtf8 +toMaybe :: Bool -> a -> Maybe a+toMaybe b a = if b then Just a else Nothing+ -- | For ignoring API Gateway custom authorizer values type NoAuthorizer = Value @@ -172,3 +200,27 @@         (v .:? "queryStringParameters" <&> fold) <*>         (v .:? "multiValueQueryStringParameters" <&> fold) <*>         (toByteString <$> v .: "isBase64Encoded" <*> (v .:? "body" <&> fold))++-- | @since 0.4.8+instance ToJSON a => ToJSON (ProxyRequest a) where+    toJSON r = object $ catMaybes+        [ Just $ "path" .= path (r :: ProxyRequest a)+        , toMaybe (not . null $ headers r) $+              "headers" .= fromCIHashMap (headers r)+        , toMaybe (not . null $ multiValueHeaders r) $+              "multiValueHeaders" .= fromCIHashMap (multiValueHeaders r)+        , toMaybe (not . null $ pathParameters r) $+              "pathParameters" .= pathParameters r+        , toMaybe (not . null $ stageVariables r) $+              "stageVariables" .= stageVariables r+        , Just $ "requestContext" .= requestContext r+        , Just $ "resource" .= resource r+        , Just $ "httpMethod" .= httpMethod (r :: ProxyRequest a)+        , toMaybe (not . null $ queryStringParameters r) $+              "queryStringParameters" .= queryStringParameters r+        , toMaybe (not . null $ multiValueQueryStringParameters r) $+              "multiValueQueryStringParameters" .=+                  multiValueQueryStringParameters r+        , Just $ "isBase64Encoded" .= True+        , Just $ "body" .= TLE.decodeUtf8 (encode (body r))+        ]
src/AWS/Lambda/Events/ApiGateway/ProxyResponse.hs view
@@ -24,18 +24,23 @@     , module Network.HTTP.Types.Status     ) where -import           Data.Aeson                (ToJSON, encode, object, toJSON,-                                            (.=))+import           Data.Aeson                (FromJSON(..), ToJSON, encode,+                                            object, toJSON, withObject, (.:),+                                            (.:?), (.=)) import           Data.ByteString           (ByteString) import qualified Data.ByteString.Base64    as B64-import           Data.CaseInsensitive      (CI, mk, original)-import           Data.HashMap.Strict       (HashMap, foldrWithKey, insert,-                                            insertWith)+import           Data.CaseInsensitive      (CI, FoldCase, mk, original)+import qualified Data.CaseInsensitive      as CI+import           Data.Foldable             (fold)+import           Data.Hashable             (Hashable)+import           Data.HashMap.Strict       (HashMap)+import qualified Data.HashMap.Strict       as H import           Data.Semigroup            ((<>)) import qualified Data.Text                 as T import qualified Data.Text.Encoding        as TE import qualified Data.Text.Lazy            as TL import qualified Data.Text.Lazy.Encoding   as TLE+import           Network.HTTP.Types.Header (hContentType) import           Network.HTTP.Types.Status (Status (..), accepted202,                                             badGateway502, badRequest400,                                             conflict409, continue100,@@ -88,7 +93,15 @@                                             unprocessableEntity422,                                             unsupportedMediaType415,                                             upgradeRequired426, useProxy305)+import           GHC.Generics               (Generic (..)) +-- This function is available in Data.Functor as of base 4.11, but we define it+-- here for compatibility.+(<&>) :: Functor f => f a -> (a -> b) -> f b+(<&>) x f = f <$> x++infixl 1 <&>+ -- | Type that represents the body returned to an API Gateway when using HTTP -- Lambda Proxy integration.  It is highly recommended that you do not use this -- type directly, and instead use the smart constructors exposed such as@@ -98,7 +111,7 @@     { contentType     :: T.Text     , serialized      :: T.Text     , isBase64Encoded :: Bool-    } deriving (Show)+    } deriving (Eq, Generic, Show)  -- | A response returned to an API Gateway when using the HTTP Lambda Proxy -- integration.  ContentType will be set based on the ProxyBody (recommended)@@ -141,8 +154,11 @@     { status            :: Status     , multiValueHeaders :: HashMap (CI T.Text) [T.Text]     , body              :: ProxyBody-    } deriving (Show)+    } deriving (Eq, Generic, Show) +toCIHashMap :: (Eq k, FoldCase k, Hashable k) => HashMap k a -> HashMap (CI k) a+toCIHashMap = H.foldrWithKey (H.insert . mk) mempty+ -- | Smart constructor for creating a ProxyResponse from a status and a body response :: Status -> ProxyBody -> ProxyResponse response =@@ -154,13 +170,13 @@ -- previous headers or their values. addHeader :: T.Text -> T.Text -> ProxyResponse -> ProxyResponse addHeader header value (ProxyResponse s mvh b) =-  ProxyResponse s (insertWith (<>) (mk header) [value] mvh) b+  ProxyResponse s (H.insertWith (<>) (mk header) [value] mvh) b  -- | Set a header to the ProxyResponse.  If there were any previous values for -- this header they are __all replaced__ by this new value. setHeader :: T.Text -> T.Text -> ProxyResponse -> ProxyResponse setHeader header value (ProxyResponse s mvh b) =-  ProxyResponse s (insert (mk header) [value] mvh) b+  ProxyResponse s (H.insert (mk header) [value] mvh) b  -- | Smart constructor for creating a ProxyBody with an arbitrary ByteString of -- the chosen content type.  Use this smart constructor to avoid invalid JSON@@ -191,16 +207,13 @@         (TL.toStrict $ TLE.decodeUtf8 $ encode x)         False --- | Smart constructor for creating a simple body of a GIF (that has already--- been converted to a ByteString).- instance ToJSON ProxyResponse where     toJSON (ProxyResponse status mvh (ProxyBody contentType body isBase64Encoded)) =-        let unCI = foldrWithKey (insert . original) mempty+        let unCI = H.foldrWithKey (H.insert . original) mempty         in object                [ "statusCode" .= statusCode status                , "multiValueHeaders" .=-                     insertWith+                     H.insertWith                          (\_ old -> old)                          ("Content-Type" :: T.Text)                          [contentType]@@ -208,3 +221,20 @@                , "body" .= body                , "isBase64Encoded" .= isBase64Encoded                ]++-- | @since 0.4.8+instance FromJSON ProxyResponse where+    parseJSON = withObject "ProxyResponse" $ \v -> do+        headers <- v .:? "multiValueHeaders" <&> toCIHashMap . fold+        -- Move the "Content-Type" header into the ProxyBody. This is+        -- necessary to ensure round-tripping.+        let contentTypeHeader = CI.map TE.decodeUtf8 hContentType+            contentType = case H.lookup contentTypeHeader headers of+                Just (h:_) -> h+                _ -> "application/octet-stream"+            headers' = H.delete contentTypeHeader headers+        status <- v .: "statusCode" <&> toEnum+        proxyBody <- ProxyBody contentType+            <$> v .: "body"+            <*> v .: "isBase64Encoded"+        pure $ ProxyResponse status headers' proxyBody
+ test/AWS/Lambda/Events/ApiGateway/ProxyRequest/Gen.hs view
@@ -0,0 +1,113 @@+module AWS.Lambda.Events.ApiGateway.ProxyRequest.Gen where++import qualified AWS.Lambda.Events.ApiGateway.ProxyRequest.Gen.Resource as Resource+import qualified AWS.Lambda.Events.ApiGateway.ProxyRequest.Gen.Parameters as Parameters+import           AWS.Lambda.Events.ApiGateway.ProxyRequest+import           Control.Applicative                        (liftA2)+import           Data.Aeson                                 (Value(..))+import qualified Data.ByteString.Lazy                       as BL+import qualified Data.HashMap.Strict                        as H+import           Data.Scientific                            (fromFloatDigits)+import qualified Data.Vector                                as V+import qualified Gen.Header                                 as Header+import           Hedgehog                                   (Gen)+import qualified Hedgehog.Gen                               as Gen+import qualified Hedgehog.Range                             as Range+import           Network.HTTP.Types                         (renderStdMethod)+import           Data.Text                                  (Text)+import qualified Data.Text.Encoding                         as TE++proxyRequest :: Gen (ProxyRequest NoAuthorizer)+proxyRequest = do+  segments <- Resource.segments+  pathParameters <- Resource.vars segments+  let path = Resource.path pathParameters segments+      resource = Resource.resource segments+  multiValueHeaders <- Header.multiValueHeaders+  let headers = head <$> multiValueHeaders+  stageVariables <- fmap head <$> Parameters.multiParameters+  httpMethod <- method+  requestContext <- context path resource httpMethod+  multiValueQueryStringParameters <- Parameters.multiParameters+  let queryStringParameters = head <$> multiValueQueryStringParameters+  body <- BL.fromStrict <$> Gen.bytes (Range.exponential 1 1000)+  pure $ ProxyRequest+      { path = path+      , headers = headers+      , multiValueHeaders = multiValueHeaders+      , pathParameters = pathParameters+      , stageVariables = stageVariables+      , requestContext = requestContext+      , resource = resource+      , httpMethod = httpMethod+      , queryStringParameters = queryStringParameters+      , multiValueQueryStringParameters = multiValueQueryStringParameters+      , body = body+      }++method :: Gen Text+method = TE.decodeUtf8 . renderStdMethod <$> Gen.enumBounded++context :: Text -> Text -> Text -> Gen (RequestContext NoAuthorizer)+context path resourcePath httpMethod = do+  accountId <- Gen.text (Range.singleton 12) Gen.digit+  authorizer <- Gen.maybe authJson+  resourceId <- text+  stage <- text+  domainPrefix <- Gen.maybe $ Gen.text (Range.linear 1 15) Gen.alphaNum+  requestId <- text+  identity <- ident+  domainName <- Gen.maybe text+  extendedRequestId <- Gen.maybe text+  apiId <- text+  pure $ RequestContext+      { path = path+      , accountId = accountId+      , authorizer = authorizer+      , resourceId = resourceId+      , stage = stage+      , domainPrefix = domainPrefix+      , requestId = requestId+      , identity = identity+      , domainName = domainName+      , resourcePath = resourcePath+      , httpMethod = httpMethod+      , extendedRequestId = extendedRequestId+      , apiId = apiId+      }++ident :: Gen Identity+ident = Identity+  <$> Gen.maybe text+  <*> Gen.maybe text+  <*> Gen.maybe text+  <*> Gen.maybe text+  <*> Gen.maybe text+  <*> text+  <*> Gen.maybe text+  <*> Gen.maybe text+  <*> Gen.maybe text+  <*> Gen.maybe text+  <*> Gen.maybe text+  <*> Gen.maybe text+  <*> Gen.maybe text++text :: Gen Text+text = Gen.text (Range.linear 1 15) Gen.unicode++-- | Generate JSON for an authorizer blob.+--+-- __NOTE:__ We do not generate @null@s, because they kill+-- round-tripping: '.:?' deserializes both @null@ and missing keys to+-- @Nothing@.+authJson :: Gen Value+authJson = Gen.recursive Gen.choice+    [string, number, Bool <$> Gen.bool]+    [object, array]+    where+        object = Object . H.fromList <$> Gen.list (Range.linear 1 50) entry+        entry = liftA2 (,) text authJson+        array = Array . V.fromList <$> Gen.list (Range.linear 1 50) authJson+        string = String <$> Gen.text (Range.exponential 1 200) Gen.unicode+        number = Number . fromFloatDigits <$>+            Gen.double (Range.exponentialFloatFrom 0 (-10**23) (10**23))
+ test/AWS/Lambda/Events/ApiGateway/ProxyRequest/Gen/Parameters.hs view
@@ -0,0 +1,39 @@+module AWS.Lambda.Events.ApiGateway.ProxyRequest.Gen.Parameters where++import           Control.Monad.Trans.State+import           Data.HashMap.Strict       (HashMap)+import qualified Data.HashMap.Strict       as H+import           Data.Semigroup            ((<>))+import           Data.Set                  (Set)+import qualified Data.Set                  as S+import           Data.Text                 (Text)+import           Hedgehog+import qualified Hedgehog.Gen              as Gen+import qualified Hedgehog.Range            as Range++multiParameters :: Gen (HashMap Text [Text])+multiParameters = H.fromListWith (<>) <$>+    evalStateT (Gen.list (Range.linear 1 10) parameter) S.empty++parameter :: StateT (Set Text) Gen (Text, [Text])+parameter = Gen.choice [fresh, clash] where+    fresh = do+        vars <- get+        newVar <- Gen.filterT (not . (`S.member` vars)) var+        put $ S.insert newVar vars+        v <- val+        pure (newVar, [v])+    clash = do+        vars <- get+        if null vars+        then fresh+        else do+            var <- Gen.element $ S.toList vars+            v <- val+            pure (var, [v])++var :: MonadGen g => g Text+var = Gen.text (Range.linear 1 10) Gen.alpha++val :: MonadGen g => g Text+val = Gen.text (Range.linear 1 15) Gen.unicode
+ test/AWS/Lambda/Events/ApiGateway/ProxyRequest/Gen/Resource.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE LambdaCase #-}++module AWS.Lambda.Events.ApiGateway.ProxyRequest.Gen.Resource where++import           Control.Monad             (foldM)+import           Control.Monad.Trans.State (StateT, evalStateT, get, put)+import           Data.Foldable             (toList)+import           Data.HashMap.Strict       (HashMap)+import qualified Data.HashMap.Strict       as H+import           Data.List.NonEmpty        (NonEmpty)+import qualified Data.List.NonEmpty        as NE+import           Data.Semigroup            ((<>))+import qualified Data.Set                  as S+import           Data.Text                 (Text)+import qualified Data.Text                 as T+import           Hedgehog+import qualified Hedgehog.Gen              as Gen+import qualified Hedgehog.Range            as Range++data ResourceSegment = Literal Text | Variable Text deriving (Eq, Show)++segments :: Gen (NonEmpty ResourceSegment)+segments = flip evalStateT S.empty+    . fmap NE.fromList+    . Gen.list (Range.linear 1 10)+    $ Gen.choice+        [ Literal <$> Gen.text (Range.linear 1 10) Gen.alpha+        , Variable <$> var+        ]++-- | Generate a variable mapping for a resource path.+vars :: Foldable t => t ResourceSegment -> Gen (HashMap Text Text)+vars = foldM addVar H.empty+    where+        addVar m = \case+            Literal{} -> pure m+            Variable v -> do+                value <- Gen.text (Range.linear 1 20) Gen.unicode+                pure $ H.insert v value m++-- | Instantiate vars in a resource path.+path :: HashMap Text Text -> NonEmpty ResourceSegment -> Text+path vars = ("/" <>) . T.intercalate "/" . toList . fmap toText+    where+        toText = \case+            Literal t -> t+            Variable v -> vars H.! v++-- | Render a resource path with @{var}@ to indicate variables+resource :: NonEmpty ResourceSegment -> Text+resource = ("/" <>) . T.intercalate "/" . toList . fmap toText+    where+        toText = \case+            Literal t -> t+            Variable v -> "{" <> v <> "}"++-- | Generate one variable name for a resource path, and don't reuse+-- variable names.+var :: StateT (S.Set Text) Gen Text+var = do+    vars <- get+    newVar <- Gen.filterT (not . (`S.member` vars)) $+        Gen.text (Range.linear 1 10) Gen.alpha+    put $ S.insert newVar vars+    pure newVar
+ test/AWS/Lambda/Events/ApiGateway/ProxyResponse/Gen.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE TupleSections #-}++module AWS.Lambda.Events.ApiGateway.ProxyResponse.Gen where++import           AWS.Lambda.Events.ApiGateway.ProxyResponse+import qualified Data.ByteString.Base64                     as B64+import           Data.CaseInsensitive                       (CI)+import qualified Data.CaseInsensitive                       as CI+import           Data.HashMap.Strict                        (HashMap)+import qualified Data.Set                                   as S+import           Data.Text                                  (Text)+import qualified Data.Text.Encoding                         as TE+import qualified Gen.Header                                 as Header+import           Hedgehog+import qualified Hedgehog.Gen                               as Gen+import qualified Hedgehog.Range                             as Range+import           Network.HTTP.Types                         (hContentType)++proxyResponse :: Gen ProxyResponse+proxyResponse = ProxyResponse+    <$> responseStatus+    <*> responseHeaders+    <*> responseBody++responseStatus :: Gen Status+responseStatus = Gen.element+    [ continue100+    , switchingProtocols101+    , ok200+    , created201+    , accepted202+    , nonAuthoritative203+    , noContent204+    , resetContent205+    , partialContent206+    , multipleChoices300+    , movedPermanently301+    , found302+    , seeOther303+    , notModified304+    , useProxy305+    , temporaryRedirect307+    , permanentRedirect308+    , badRequest400+    , unauthorized401+    , paymentRequired402+    , forbidden403+    , notFound404+    , methodNotAllowed405+    , notAcceptable406+    , proxyAuthenticationRequired407+    , requestTimeout408+    , conflict409+    , gone410+    , lengthRequired411+    , preconditionFailed412+    , requestEntityTooLarge413+    , requestURITooLong414+    , unsupportedMediaType415+    , requestedRangeNotSatisfiable416+    , expectationFailed417+    , imATeapot418+    , unprocessableEntity422+    , upgradeRequired426+    , preconditionRequired428+    , tooManyRequests429+    , requestHeaderFieldsTooLarge431+    , internalServerError500+    , notImplemented501+    , badGateway502+    , serviceUnavailable503+    , gatewayTimeout504+    , httpVersionNotSupported505+    , networkAuthenticationRequired511+    ]++-- | Do not generate @Content-Type@ headers, as they will break+-- round-tripping: the 'ToJSON' instance for 'ProxyResponse' drops the+-- 'contentType' of the 'ProxyBody' if such a header is present.+responseHeaders :: Gen (HashMap (CI Text) [Text])+responseHeaders = Header.multiValueHeadersExcept+    . S.singleton+    $ CI.map TE.decodeUtf8 hContentType++responseBody :: Gen ProxyBody+responseBody = do+  contentType <- Gen.text (Range.linear 1 20) Gen.latin1+  (serialized, isBase64Encoded) <- Gen.choice+      [ (, False) <$> Gen.text (Range.exponential 1 2000) Gen.unicode+      , (, True) . TE.decodeUtf8 . B64.encode <$>+            Gen.bytes (Range.exponential 1 2000)+      ]+  pure $ ProxyBody contentType serialized isBase64Encoded
+ test/Gen/Header.hs view
@@ -0,0 +1,123 @@+module Gen.Header where++import           Control.Monad.Trans.State+import           Data.CaseInsensitive      (CI)+import qualified Data.CaseInsensitive      as CI+import           Data.Char                 (toUpper, toLower)+import           Data.HashMap.Strict       (HashMap)+import qualified Data.HashMap.Strict       as H+import           Data.Semigroup            ((<>))+import           Data.Set                  (Set)+import qualified Data.Set                  as S+import           Data.Text                 (Text)+import qualified Data.Text                 as T+import qualified Data.Text.Encoding        as TE+import           Data.Traversable          (for)+import           Hedgehog+import qualified Hedgehog.Gen              as Gen+import qualified Hedgehog.Range            as Range+import           Network.HTTP.Types.Header++multiValueHeaders :: Gen (HashMap (CI Text) [Text])+multiValueHeaders = multiValueHeadersExcept S.empty++multiValueHeadersExcept :: Set (CI Text) -> Gen (HashMap (CI Text) [Text])+multiValueHeadersExcept exclusions = H.fromListWith (<>) <$>+    evalStateT+        (Gen.list (Range.linear 1 30) $ singleHeaderExcept exclusions)+        S.empty++singleHeaderExcept :: Set (CI Text) -> StateT (Set (CI Text)) Gen (CI Text, [Text])+singleHeaderExcept exclusions = Gen.choice [fresh, clash] where+    fresh = do+        headers <- get+        newHeader <- Gen.filterT+            (\h -> not $ S.member h headers || S.member h exclusions)+            header+        put $ S.insert newHeader headers+        headerVal <- val+        pure (newHeader, [headerVal])+    clash = do+        headers <- get+        if null headers+        then fresh+        else do+            header <- Gen.filterT+                (\h -> not $ S.member h exclusions)+                (Gen.element (S.toList headers))+                >>= unfoldCase+            headerVal <- val+            pure (header, [headerVal])++header :: MonadGen g => g (CI Text)+header = Gen.choice+    [ CI.mk <$> Gen.text (Range.linear 1 15) Gen.alpha+    , standardHeader+    ]++standardHeader :: MonadGen g => g (CI Text)+standardHeader = CI.map TE.decodeUtf8 <$> Gen.element+    [ hAccept+    , hAcceptCharset+    , hAcceptEncoding+    , hAcceptLanguage+    , hAcceptRanges+    , hAge+    , hAllow+    , hAuthorization+    , hCacheControl+    , hConnection+    , hContentEncoding+    , hContentLanguage+    , hContentLength+    , hContentLocation+    , hContentMD5+    , hContentRange+    , hContentType+    , hDate+    , hETag+    , hExpect+    , hExpires+    , hFrom+    , hHost+    , hIfMatch+    , hIfModifiedSince+    , hIfNoneMatch+    , hIfRange+    , hIfUnmodifiedSince+    , hLastModified+    , hLocation+    , hMaxForwards+    , hOrigin+    , hPragma+    , hPrefer+    , hPreferenceApplied+    , hProxyAuthenticate+    , hProxyAuthorization+    , hRange+    , hReferer+    , hRetryAfter+    , hServer+    , hTE+    , hTrailer+    , hTransferEncoding+    , hUpgrade+    , hUserAgent+    , hVary+    , hVia+    , hWWWAuthenticate+    , hWarning+    , hContentDisposition+    , hMIMEVersion+    , hCookie+    , hSetCookie+    ]++-- | Create some permutation of the input text, that compares as equal+-- under 'CI'.+unfoldCase :: MonadGen g => CI Text -> g (CI Text)+unfoldCase t = CI.mk . T.pack <$> for (T.unpack $ CI.original t) flipCase+    where flipCase c = Gen.element [toUpper c, toLower c]++val :: MonadGen g => g Text+val = Gen.text (Range.linear 1 15) Gen.unicode
test/Spec.hs view
@@ -2,16 +2,24 @@                                                     ClientContext (..),                                                     CognitoIdentity (..),                                                     LambdaContext (..))+import qualified Gen.Header                        as Header+import qualified AWS.Lambda.Events.ApiGateway.ProxyRequest.Gen as ProxyRequest+import qualified AWS.Lambda.Events.ApiGateway.ProxyResponse.Gen as ProxyResponse import           AWS.Lambda.Internal               (StaticContext (..)) import           AWS.Lambda.RuntimeClient.Internal (eventResponseToNextData)-import           Data.Aeson                        (Value (Null))+import           Data.Aeson                        (Value (Null), decode, encode)+import qualified Data.CaseInsensitive              as CI import           Data.Map                          (singleton) import           Data.Semigroup                    ((<>)) import           Data.Time.Clock.POSIX             (posixSecondsToUTCTime)+import           Hedgehog+import qualified Hedgehog.Gen                      as Gen+import qualified Hedgehog.Range                    as Range import           Network.HTTP.Client.Internal      (Response (..)) import           Network.HTTP.Types                (Header) import           Test.Hspec                        (describe, it, shouldBe,-                                                    shouldStartWith)+                                                    shouldStartWith, specify)+import           Test.Hspec.Hedgehog               (hedgehog) import           Test.Hspec.Runner                 (hspec)  main :: IO ()@@ -198,6 +206,25 @@         context `shouldBe`           (Left              "Runtime Error: Unable to decode Context from event response.\nCould not parse deadline")++    describe "properties" $ do+        specify "Header.unfoldCase preserves equality" $+            hedgehog $ do+                -- CI only promises equality over the Latin-1 set+                s <- forAll $ CI.mk <$> Gen.text (Range.linear 1 100) Gen.latin1+                t <- forAll $ Header.unfoldCase s+                s === t++        specify "ProxyRequest tripping" $+            hedgehog $ do+                request <- forAll ProxyRequest.proxyRequest+                tripping request encode decode++        specify "ProxyResponse tripping" $+            hedgehog $ do+                request <- forAll ProxyResponse.proxyResponse+                tripping request encode decode+  minResponse :: [Header] -> a -> Response a minResponse headers body =