serverless-haskell 0.4.1 → 0.4.2
raw patch · 9 files changed
+536/−69 lines, 9 filesdep +aeson-extradep +case-insensitive
Dependencies added: aeson-extra, case-insensitive
Files
- README.md +2/−0
- serverless-haskell.cabal +10/−2
- src/AWSLambda/Events.hs +3/−1
- src/AWSLambda/Events/APIGateway.hs +182/−0
- src/AWSLambda/Events/SNSEvent.hs +32/−7
- src/Data/Aeson/Embedded.hs +19/−12
- src/Data/Aeson/TextValue.hs +32/−0
- test/AWSLambda/Events/APIGatewaySpec.hs +199/−0
- test/AWSLambda/Events/SNSEventSpec.hs +57/−47
README.md view
@@ -36,6 +36,8 @@ * Create `serverless.yml` with the following contents: ```yaml+ service: myservice+ provider: name: aws runtime: nodejs6.10
serverless-haskell.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 4c9e15e7f2028afe4a93b3dc54949d8501d4634b200ded5e15c1b7f2df5e358b+-- hash: 39c6aa6699d4c9e7504dab750ed591876715bfaa355c00c418ad2b04a388de41 name: serverless-haskell-version: 0.4.1+version: 0.4.2 synopsis: Deploying Haskell code onto AWS Lambda using Serverless description: Utilities to help process the events from AWS Lambda when deployed with the serverless-haskell plugin. category: AWS, Cloud, Network@@ -31,11 +31,13 @@ build-depends: aeson , aeson-casing+ , aeson-extra , amazonka-core , amazonka-kinesis , amazonka-s3 , base >=4.7 && <5 , bytestring+ , case-insensitive , lens , text , time@@ -44,6 +46,7 @@ exposed-modules: AWSLambda AWSLambda.Events+ AWSLambda.Events.APIGateway AWSLambda.Events.KinesisEvent AWSLambda.Events.Records AWSLambda.Events.S3Event@@ -52,6 +55,7 @@ AWSLambda.Orphans Data.Aeson.Alternative Data.Aeson.Embedded+ Data.Aeson.TextValue other-modules: Paths_serverless_haskell default-language: Haskell2010@@ -61,14 +65,17 @@ main-is: Spec.hs hs-source-dirs: test+ ghc-options: -Wall build-depends: aeson , aeson-casing+ , aeson-extra , amazonka-core , amazonka-kinesis , amazonka-s3 , base >=4.7 && <5 , bytestring+ , case-insensitive , hspec , hspec-discover , lens@@ -79,6 +86,7 @@ , unix , unordered-containers other-modules:+ AWSLambda.Events.APIGatewaySpec AWSLambda.Events.KinesisEventSpec AWSLambda.Events.S3EventSpec AWSLambda.Events.SNSEventSpec
src/AWSLambda/Events.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TemplateHaskell #-} module AWSLambda.Events where@@ -5,6 +6,7 @@ import Control.Applicative ((<|>)) import Control.Lens.TH import Data.Aeson (FromJSON (..), Value)+import Network.AWS.Data.Text (FromText) import AWSLambda.Events.KinesisEvent import AWSLambda.Events.S3Event@@ -82,7 +84,7 @@ -- | Attempt to parse the various event types. -- Any valid JSON that can't be parsed as a specific -- event type will result in a 'Custom' value.-instance FromJSON snsMessage =>+instance FromText snsMessage => FromJSON (LambdaEvent snsMessage) where parseJSON v = try S3 v <|> try KinesisStream v <|> try SNS v <|> pure (Custom v)
+ src/AWSLambda/Events/APIGateway.hs view
@@ -0,0 +1,182 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++{-|+Module: AWSLambda.Events.APIGateway+Description: Types for APIGateway Lambda requests and responses++Based on https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.APIGatewayEvents+-}+module AWSLambda.Events.APIGateway where++import Control.Lens+import Data.Aeson+import Data.Aeson.Casing (aesonDrop, camelCase)+import Data.Aeson.TH (deriveFromJSON)+-- import Data.CaseInsensitive (CI (..))+import Data.Aeson.Embedded+import Data.Aeson.TextValue+import Data.ByteString (ByteString)+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HashMap+import Data.Text (Text)+import GHC.Generics (Generic)+import Network.AWS.Data.Base64+import Network.AWS.Data.Text++import AWSLambda.Handler (lambdaMain)++type Method = Text+-- type HeaderName = CI Text+type HeaderName = Text --- XXX should be CI Text+type HeaderValue = Text+type QueryParamName = Text+type QueryParamValue = Text+type PathParamName = Text+type PathParamValue = Text+type StageVarName = Text+type StageVarValue = Text++data RequestIdentity = RequestIdentity+ { _riCognitoIdentityPoolId :: !(Maybe Text)+ , _riAccountId :: !(Maybe Text)+ , _riCognitoIdentityId :: !(Maybe Text)+ , _riCaller :: !(Maybe Text)+ , _riApiKey :: !(Maybe Text)+ , _riSourceIp :: !(Maybe Text)+ , _riCognitoAuthenticationType :: !(Maybe Text)+ , _riCognitoAuthenticationProvider :: !(Maybe Text)+ , _riUserArn :: !(Maybe Text)+ , _riUserAgent :: !(Maybe Text)+ , _riUser :: !(Maybe Text)+ } deriving (Eq, Show)++$(deriveFromJSON (aesonDrop 3 camelCase) ''RequestIdentity)+$(makeLenses ''RequestIdentity)++data ProxyRequestContext = ProxyRequestContext+ { _prcPath :: !(Maybe Text)+ , _prcAccountId :: !Text+ , _prcResourceId :: !Text+ , _prcStage :: !Text+ , _prcRequestId :: !Text+ , _prcIdentity :: !RequestIdentity+ , _prcResourcePath :: !Text+ , _prcHttpMethod :: !Text+ , _prcApiId :: !Text+ } deriving (Eq, Show)+$(deriveFromJSON (aesonDrop 4 camelCase) ''ProxyRequestContext)+$(makeLenses ''ProxyRequestContext)++data APIGatewayProxyRequest body = APIGatewayProxyRequest+ { _agprqResource :: !Text+ , _agprqPath :: !Text+ , _agprqHttpMethod :: !Method+ , _agprqHeaders :: !(HashMap HeaderName HeaderValue)+ , _agprqQueryStringParameters :: !(HashMap QueryParamName QueryParamValue)+ , _agprqPathParameters :: !(HashMap PathParamName PathParamValue)+ , _agprqStageVariables :: !(HashMap StageVarName StageVarValue)+ , _agprqRequestContext :: !ProxyRequestContext+ , _agprqBody :: !(Maybe (TextValue body))+ } deriving (Eq, Show, Generic)++instance FromText body => FromJSON (APIGatewayProxyRequest body) where+ parseJSON = withObject "APIGatewayProxyRequest" $ \o ->+ APIGatewayProxyRequest+ <$> o .: "resource"+ <*> o .: "path"+ <*> o .: "httpMethod"+ <*> o .:? "headers" .!= HashMap.empty+ <*> o .:? "queryStringParameters" .!= HashMap.empty+ <*> o .:? "pathParameters" .!= HashMap.empty+ <*> o .:? "stageVariables" .!= HashMap.empty+ <*> o .: "requestContext"+ <*> o .:? "body"+++$(makeLenses ''APIGatewayProxyRequest)++-- | Get the request body, if there is one+requestBody :: Getter (APIGatewayProxyRequest body) (Maybe body)+requestBody = agprqBody . mapping unTextValue++-- | Get the embedded request body, if there is one+requestBodyEmbedded :: Getter (APIGatewayProxyRequest (Embedded v)) (Maybe v)+requestBodyEmbedded = requestBody . mapping unEmbed++-- | Get the binary (decoded Base64) request body, if there is one+requestBodyBinary :: Getter (APIGatewayProxyRequest Base64) (Maybe ByteString)+requestBodyBinary = requestBody . mapping _Base64++data APIGatewayProxyResponse body = APIGatewayProxyResponse+ { _agprsStatusCode :: !Int+ , _agprsHeaders :: !(HashMap HeaderName HeaderValue)+ , _agprsBody :: !(Maybe (TextValue body))+ } deriving (Eq, Show, Generic)++instance ToText body => ToJSON (APIGatewayProxyResponse body) where+ toJSON = genericToJSON $ aesonDrop 6 camelCase++instance FromText body => FromJSON (APIGatewayProxyResponse body) where+ parseJSON = genericParseJSON $ aesonDrop 6 camelCase++$(makeLenses ''APIGatewayProxyResponse)++response :: Int -> APIGatewayProxyResponse body+response statusCode = APIGatewayProxyResponse statusCode HashMap.empty Nothing++responseOK :: APIGatewayProxyResponse body+responseOK = response 200++responseNotFound :: APIGatewayProxyResponse body+responseNotFound = response 404++responseBadRequest :: APIGatewayProxyResponse body+responseBadRequest = response 400++responseBody :: Setter' (APIGatewayProxyResponse body) (Maybe body)+responseBody = agprsBody . at () . mapping unTextValue++responseBodyEmbedded :: Setter' (APIGatewayProxyResponse (Embedded body)) (Maybe body)+responseBodyEmbedded = responseBody . mapping unEmbed++responseBodyBinary :: Setter' (APIGatewayProxyResponse Base64) (Maybe ByteString)+responseBodyBinary = responseBody . mapping _Base64++-- | Process incoming events from @serverless-haskell@ using a provided+-- function.+--+-- This is a specialisation of lambdaMain for API Gateway.+--+-- The handler receives the input event given to the AWS Lambda function, and+-- its return value is returned from the function.+--+-- This is intended to be used as @main@, for example:+--+-- > import AWSLambda.Events.APIGateway+-- > import Control.Lens+-- > import Data.Aeson+-- > import Data.Aeson.Embedded+-- >+-- > main = apiGatewayMain handler+-- >+-- > handler :: APIGatewayProxyRequest (Embedded Value) -> IO (APIGatewayProxyResponse (Embedded [Int]))+-- > handler request = do+-- > putStrLn "This should go to logs"+-- > print $ request ^. requestBody+-- > pure $ responseOK & responseBodyEmbedded ?~ [1, 2, 3]+--+-- The type parameters @reqBody@ and @resBody@ represent the types of request and response body, respectively.+-- The @FromText@ and @ToText@ contraints are required because these values come from string fields+-- in the request and response JSON objects.+-- To get direct access to the body string, use @Text@ as the parameter type.+-- To treat the body as a stringified embedded JSON value, use @Embedded a@, where @a@ has the+-- appropriate @FromJSON@ or @ToJSON@ instances.+-- To treat the body as base 64 encoded binary use @Base64@.+apiGatewayMain+ :: (FromText reqBody, ToText resBody)+ => (APIGatewayProxyRequest reqBody -> IO (APIGatewayProxyResponse resBody)) -- ^ Function to process the event+ -> IO ()+apiGatewayMain = lambdaMain
src/AWSLambda/Events/SNSEvent.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-}@@ -10,17 +11,24 @@ -} module AWSLambda.Events.SNSEvent where -import Control.Lens.TH+import Control.Lens+import Data.Aeson (FromJSON (..), genericParseJSON) import Data.Aeson.Casing (aesonDrop, pascalCase)+import Data.Aeson.Embedded+import Data.Aeson.TextValue import Data.Aeson.TH (deriveFromJSON)+import Data.ByteString (ByteString) import Data.HashMap.Strict (HashMap) import Data.Text (Text) import Data.Time.Clock (UTCTime)+import GHC.Generics (Generic)+import Network.AWS.Data.Base64+import Network.AWS.Data.Text (FromText) import AWSLambda.Events.Records data MessageAttribute = MessageAttribute- { _maType :: !Text+ { _maType :: !Text , _maValue :: !Text } deriving (Eq, Show) @@ -28,7 +36,7 @@ $(makeLenses ''MessageAttribute) data SNSMessage message = SNSMessage- { _smMessage :: !message+ { _smMessage :: !(TextValue message ) , _smMessageAttributes :: !(HashMap Text MessageAttribute) , _smMessageId :: !Text , _smSignature :: !Text@@ -39,9 +47,11 @@ , _smTopicArn :: !Text , _smType :: !Text , _smUnsubscribeUrl :: !Text- } deriving (Eq, Show)+ } deriving (Eq, Show, Generic) -$(deriveFromJSON (aesonDrop 3 pascalCase) ''SNSMessage)+instance FromText message => FromJSON (SNSMessage message) where+ parseJSON = genericParseJSON $ aesonDrop 3 pascalCase+ $(makeLenses ''SNSMessage) data SNSRecord message = SNSRecord@@ -49,9 +59,11 @@ , _srEventSubscriptionArn :: !Text , _srEventSource :: !Text , _srSns :: !(SNSMessage message)- } deriving (Eq, Show)+ } deriving (Eq, Show, Generic) -$(deriveFromJSON (aesonDrop 3 pascalCase) ''SNSRecord)+instance FromText message => FromJSON (SNSRecord message) where+ parseJSON = genericParseJSON $ aesonDrop 3 pascalCase+ $(makeLenses ''SNSRecord) -- | SNSEvent.@@ -60,4 +72,17 @@ -- To extract an embedded event object use the 'Embedded' type. -- E.g. @SNSEvent (Embedded S3Event)@ will treat the message -- as an embedded S3Event.+-- To extract embedded Base64 encoded binary use+-- @SNSEvent Base64@ type SNSEvent message = RecordsEvent (SNSRecord message)++-- | A Traversal to get messages from an SNSEvent+messages :: Traversal (SNSEvent message) (SNSEvent message') message message'+messages = reRecords . traverse . srSns . smMessage . unTextValue++-- | A Traversal to get embedded JSON values from an SNSEvent+embedded :: Traversal (SNSEvent (Embedded v)) (SNSEvent (Embedded v')) v v'+embedded = messages . unEmbed++binary :: Traversal' (SNSEvent Base64) ByteString+binary = messages . _Base64
src/Data/Aeson/Embedded.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-} {-| Module: Data.Aeson.Embedded@@ -10,22 +11,28 @@ import Control.Lens.TH import Data.Aeson-import Data.Text.Lazy (Text)-import Data.Text.Lazy.Encoding (decodeUtf8, encodeUtf8)+import Data.Aeson.Extra (encodeStrict)+import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Network.AWS.Data.Text (FromText (..), ToText (..), fromText,+ takeText) -- | Type for a JSON value embedded within a JSON string value newtype Embedded a = Embedded { _unEmbed :: a } deriving (Eq, Show) instance FromJSON a =>+ FromText (Embedded a) where+ parser =+ fmap Embedded . either fail pure . eitherDecodeStrict . encodeUtf8 =<< takeText++instance FromJSON a => FromJSON (Embedded a) where- parseJSON v =- fmap Embedded . either fail pure . eitherDecode . encodeUtf8 =<< parseJSON v+ parseJSON v = either fail pure . fromText =<< parseJSON v -instance ToJSON a => ToJSON (Embedded a) where- toJSON = toJSON . embed- toEncoding = toEncoding . embed+instance ToJSON a => ToText (Embedded a) where+ toText = decodeUtf8 . encodeStrict . _unEmbed -embed :: ToJSON a => Embedded a -> Text-embed = decodeUtf8 . encode . _unEmbed+instance ToJSON a => ToJSON (Embedded a) where+ toJSON = toJSON . toText+ toEncoding = toEncoding . toText $(makeLenses ''Embedded)
+ src/Data/Aeson/TextValue.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++{-|+Module: Data.Aeson.TextValue+Description: Type for things that can be embedded in a JSON string++Provides @FromJSON@ and @ToJSON@ instances for anything that+has @FromText@ and @ToText@ instances, e.g. @TextValue Text@,+@(FromJSON a, ToJSON a) => TextValue (Embedded a)@,+@TextValue Base64@+-}+module Data.Aeson.TextValue where++import Control.Lens.TH+import Data.Aeson+import Data.String+import Network.AWS.Data.Text (FromText (..), ToText (..), fromText)+++newtype TextValue a = TextValue { _unTextValue :: a } deriving (Eq, Show, IsString)++instance FromText a => FromJSON (TextValue a) where+ parseJSON = withText "TextValue" $ fmap TextValue . either fail pure . fromText++instance ToText a => ToJSON (TextValue a) where+ toJSON = toJSON . toText . _unTextValue+ toEncoding = toEncoding . toText . _unTextValue++$(makeLenses ''TextValue)
+ test/AWSLambda/Events/APIGatewaySpec.hs view
@@ -0,0 +1,199 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module AWSLambda.Events.APIGatewaySpec where++import AWSLambda.Events.APIGateway++import Control.Lens+import Data.Aeson+import Data.ByteString.Lazy (ByteString)+import qualified Data.HashMap.Strict as HashMap+import Data.Text (Text)++import Text.RawString.QQ++import Test.Hspec++spec :: Spec+spec = do+ describe "APIGatewayProxyRequest" $+ it "parses sample GET request" $+ decode sampleGetRequestJSON `shouldBe` Just sampleGetRequest+ describe "APIGatewayProxyResponse" $+ it "parses sample text event" $+ decode sampleOKResponseJSON `shouldBe` Just sampleOKResponse++sampleGetRequestJSON :: ByteString+sampleGetRequestJSON = [r|+{+ "path": "/test/hello",+ "headers": {+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",+ "Accept-Encoding": "gzip, deflate, lzma, sdch, br",+ "Accept-Language": "en-US,en;q=0.8",+ "CloudFront-Forwarded-Proto": "https",+ "CloudFront-Is-Desktop-Viewer": "true",+ "CloudFront-Is-Mobile-Viewer": "false",+ "CloudFront-Is-SmartTV-Viewer": "false",+ "CloudFront-Is-Tablet-Viewer": "false",+ "CloudFront-Viewer-Country": "US",+ "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com",+ "Upgrade-Insecure-Requests": "1",+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",+ "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)",+ "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==",+ "X-Forwarded-For": "192.168.100.1, 192.168.1.1",+ "X-Forwarded-Port": "443",+ "X-Forwarded-Proto": "https"+ },+ "pathParameters": {+ "proxy": "hello"+ },+ "requestContext": {+ "accountId": "123456789012",+ "resourceId": "us4z18",+ "stage": "test",+ "requestId": "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9",+ "identity": {+ "cognitoIdentityPoolId": "",+ "accountId": "",+ "cognitoIdentityId": "",+ "caller": "",+ "apiKey": "",+ "sourceIp": "192.168.100.1",+ "cognitoAuthenticationType": "",+ "cognitoAuthenticationProvider": "",+ "userArn": "",+ "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",+ "user": ""+ },+ "resourcePath": "/{proxy+}",+ "httpMethod": "GET",+ "apiId": "wt6mne2s9k"+ },+ "resource": "/{proxy+}",+ "httpMethod": "GET",+ "queryStringParameters": {+ "name": "me"+ },+ "stageVariables": {+ "stageVarName": "stageVarValue"+ }+}+|]++sampleGetRequest :: APIGatewayProxyRequest Text+sampleGetRequest =+ APIGatewayProxyRequest+ { _agprqResource = "/{proxy+}"+ , _agprqPath = "/test/hello"+ , _agprqHttpMethod = "GET"+ , _agprqHeaders =+ HashMap.fromList+ [ ("X-Forwarded-Proto", "https")+ , ("CloudFront-Is-Desktop-Viewer", "true")+ , ( "Accept"+ , "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")+ , ( "X-Amz-Cf-Id"+ , "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==")+ , ("Accept-Encoding", "gzip, deflate, lzma, sdch, br")+ , ("CloudFront-Forwarded-Proto", "https")+ , ("Accept-Language", "en-US,en;q=0.8")+ , ("CloudFront-Is-Tablet-Viewer", "false")+ , ("Upgrade-Insecure-Requests", "1")+ , ("CloudFront-Viewer-Country", "US")+ , ( "User-Agent"+ , "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48")+ , ("CloudFront-Is-Mobile-Viewer", "false")+ , ("Host", "wt6mne2s9k.execute-api.us-west-2.amazonaws.com")+ , ("X-Forwarded-Port", "443")+ , ("CloudFront-Is-SmartTV-Viewer", "false")+ , ( "Via"+ , "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)")+ , ("X-Forwarded-For", "192.168.100.1, 192.168.1.1")+ ]+ , _agprqQueryStringParameters = HashMap.fromList [("name", "me")]+ , _agprqPathParameters = HashMap.fromList [("proxy", "hello")]+ , _agprqStageVariables = HashMap.fromList [("stageVarName", "stageVarValue")]+ , _agprqRequestContext =+ ProxyRequestContext+ { _prcPath = Nothing+ , _prcAccountId = "123456789012"+ , _prcResourceId = "us4z18"+ , _prcStage = "test"+ , _prcRequestId = "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9"+ , _prcIdentity =+ RequestIdentity+ { _riCognitoIdentityPoolId = Just ""+ , _riAccountId = Just ""+ , _riCognitoIdentityId = Just ""+ , _riCaller = Just ""+ , _riApiKey = Just ""+ , _riSourceIp = Just "192.168.100.1"+ , _riCognitoAuthenticationType = Just ""+ , _riCognitoAuthenticationProvider = Just ""+ , _riUserArn = Just ""+ , _riUserAgent = Just+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48"+ , _riUser = Just ""+ }+ , _prcResourcePath = "/{proxy+}"+ , _prcHttpMethod = "GET"+ , _prcApiId = "wt6mne2s9k"+ }+ , _agprqBody = Nothing+ }++sampleOKResponseJSON :: ByteString+sampleOKResponseJSON = [r|+{+ "statusCode": 200,+ "headers": {+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",+ "Accept-Encoding": "gzip, deflate, lzma, sdch, br",+ "Accept-Language": "en-US,en;q=0.8",+ "CloudFront-Forwarded-Proto": "https",+ "CloudFront-Is-Desktop-Viewer": "true",+ "CloudFront-Is-Mobile-Viewer": "false",+ "CloudFront-Is-SmartTV-Viewer": "false",+ "CloudFront-Is-Tablet-Viewer": "false",+ "CloudFront-Viewer-Country": "US",+ "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com",+ "Upgrade-Insecure-Requests": "1",+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",+ "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)",+ "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==",+ "X-Forwarded-For": "192.168.100.1, 192.168.1.1",+ "X-Forwarded-Port": "443",+ "X-Forwarded-Proto": "https"+ },+ "body": "Hello World"+}+|]++sampleOKResponse :: APIGatewayProxyResponse Text+sampleOKResponse =+ responseOK+ & responseBody ?~ "Hello World"+ & agprsHeaders .~ HashMap.fromList+ [ ("X-Forwarded-Proto", "https")+ , ("CloudFront-Is-Desktop-Viewer", "true")+ , ( "Accept"+ , "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")+ , ("X-Amz-Cf-Id", "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==")+ , ("Accept-Encoding", "gzip, deflate, lzma, sdch, br")+ , ("CloudFront-Forwarded-Proto", "https")+ , ("Accept-Language", "en-US,en;q=0.8")+ , ("CloudFront-Is-Tablet-Viewer", "false")+ , ("Upgrade-Insecure-Requests", "1")+ , ("CloudFront-Viewer-Country", "US")+ , ( "User-Agent"+ , "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48")+ , ("CloudFront-Is-Mobile-Viewer", "false")+ , ("Host", "wt6mne2s9k.execute-api.us-west-2.amazonaws.com")+ , ("X-Forwarded-Port", "443")+ , ("CloudFront-Is-SmartTV-Viewer", "false")+ , ("Via", "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)")+ , ("X-Forwarded-For", "192.168.100.1, 192.168.1.1")+ ]
test/AWSLambda/Events/SNSEventSpec.hs view
@@ -9,13 +9,13 @@ import Data.Aeson import Data.Aeson.Embedded+import Data.Aeson.TextValue import Data.ByteString.Lazy (ByteString) import qualified Data.HashMap.Strict as HashMap import Data.Text (Text) import Data.Time.Calendar import Data.Time.Clock import Network.AWS.S3 as S3-import Network.AWS.Types as AWS import Text.RawString.QQ @@ -137,55 +137,62 @@ , _srSns = SNSMessage { _smMessage =- Embedded- { _unEmbed =- RecordsEvent- { _reRecords =- [ S3EventNotification- { _senAwsRegion = Sydney- , _senEventName = S3ObjectCreatedPut- , _senEventSource = "aws:s3"- , _senEventTime = UTCTime (fromGregorian 2017 3 6) (picosecondsToDiffTime 10579713000000000)- , _senEventVersion = "2.0"- , _senRequestParameters =- RequestParametersEntity- { _rpeSourceIPAddress = "192.168.0.1"- }- , _senResponseElements =- ResponseElementsEntity- { _reeXAmzId2 =- "xsdSDF/pgAl401Fz3UIATJ5/didfljDSFDSFsdfkjsdfl8JdsfLSDF89ldsf7SDF898jsdfljiA="- , _reeXAmzRequestId = "324098EDFLK0894F"- }- , _senS3 =- S3Entity- { _seBucket =- S3BucketEntity- { _sbeArn = "arn:aws:s3:::some-bucket"- , _sbeName = BucketName "some-bucket"- , _sbeOwnerIdentity =- UserIdentityEntity- { _uiePrincipalId = "A3O1SDFLKJIJXU"+ TextValue+ { _unTextValue =+ Embedded+ { _unEmbed =+ RecordsEvent+ { _reRecords =+ [ S3EventNotification+ { _senAwsRegion = Sydney+ , _senEventName = S3ObjectCreatedPut+ , _senEventSource = "aws:s3"+ , _senEventTime =+ UTCTime+ (fromGregorian 2017 3 6)+ (picosecondsToDiffTime 10579713000000000)+ , _senEventVersion = "2.0"+ , _senRequestParameters =+ RequestParametersEntity+ { _rpeSourceIPAddress = "192.168.0.1"+ }+ , _senResponseElements =+ ResponseElementsEntity+ { _reeXAmzId2 =+ "xsdSDF/pgAl401Fz3UIATJ5/didfljDSFDSFsdfkjsdfl8JdsfLSDF89ldsf7SDF898jsdfljiA="+ , _reeXAmzRequestId = "324098EDFLK0894F"+ }+ , _senS3 =+ S3Entity+ { _seBucket =+ S3BucketEntity+ { _sbeArn = "arn:aws:s3:::some-bucket"+ , _sbeName = BucketName "some-bucket"+ , _sbeOwnerIdentity =+ UserIdentityEntity+ { _uiePrincipalId = "A3O1SDFLKJIJXU"+ } }+ , _seConfigurationId = "SomeS3Event:Created"+ , _seObject =+ S3ObjectEntity+ { _soeETag =+ Just (ETag "6b1f72b9e81e4d6fcd3e0c808e8477f8")+ , _soeKey = ObjectKey "path/to/some/object"+ , _soeSize = Just 53598442+ , _soeSequencer = "0058BCCFD25C798E7B"+ , _soeVersionId = Nothing+ }+ , _seS3SchemaVersion = "1.0" }- , _seConfigurationId = "SomeS3Event:Created"- , _seObject =- S3ObjectEntity- { _soeETag = Just (ETag "6b1f72b9e81e4d6fcd3e0c808e8477f8")- , _soeKey = ObjectKey "path/to/some/object"- , _soeSize = Just 53598442- , _soeSequencer = "0058BCCFD25C798E7B"- , _soeVersionId = Nothing+ , _senUserIdentity =+ UserIdentityEntity+ { _uiePrincipalId =+ "AWS:DFLKSDFLKJ987SDFLJJDJ:some-principal-id" }- , _seS3SchemaVersion = "1.0" }- , _senUserIdentity =- UserIdentityEntity- { _uiePrincipalId =- "AWS:DFLKSDFLKJ987SDFLJJDJ:some-principal-id"- }- }- ]+ ]+ } } } , _smMessageAttributes = HashMap.fromList []@@ -196,7 +203,10 @@ , _smSigningCertUrl = "https://sns.ap-southeast-2.amazonaws.com/SimpleNotificationService-b95095beb82e8f6a046b3aafc7f4149a.pem" , _smSubject = "Amazon S3 Notification"- , _smTimestamp = UTCTime (fromGregorian 2017 3 6) (picosecondsToDiffTime 10579834000000000)+ , _smTimestamp =+ UTCTime+ (fromGregorian 2017 3 6)+ (picosecondsToDiffTime 10579834000000000) , _smTopicArn = "arn:aws:sns:ap-southeast-2:012345678901:SomeSNSEvent" , _smType = "Notification" , _smUnsubscribeUrl =