serverless-haskell 0.9.4 → 0.10.0
raw patch · 7 files changed
+315/−18 lines, 7 files
Files
- serverless-haskell.cabal +5/−2
- src/AWSLambda/Events.hs +7/−5
- src/AWSLambda/Events/MessageAttribute.hs +20/−0
- src/AWSLambda/Events/SNSEvent.hs +25/−11
- src/AWSLambda/Events/SQSEvent.hs +56/−0
- test/AWSLambda/Events/SNSEventSpec.hs +1/−0
- test/AWSLambda/Events/SQSEventSpec.hs +201/−0
serverless-haskell.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 1e05bad89b0aca82487e2433016e2d39a21f182bf384caab21fb8e56b3a5f2f6+-- hash: a98a36c8e69422845bcf7370c281cb9b0bbaac07f75b4e660c170bc782ced20c name: serverless-haskell-version: 0.9.4+version: 0.10.0 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@@ -30,9 +30,11 @@ AWSLambda.Events AWSLambda.Events.APIGateway AWSLambda.Events.KinesisEvent+ AWSLambda.Events.MessageAttribute AWSLambda.Events.Records AWSLambda.Events.S3Event AWSLambda.Events.SNSEvent+ AWSLambda.Events.SQSEvent AWSLambda.Handler AWSLambda.Orphans Data.Aeson.Alternative@@ -72,6 +74,7 @@ AWSLambda.Events.KinesisEventSpec AWSLambda.Events.S3EventSpec AWSLambda.Events.SNSEventSpec+ AWSLambda.Events.SQSEventSpec Data.Aeson.AlternativeSpec Data.Aeson.EmbeddedSpec Data.Aeson.TestUtil
src/AWSLambda/Events.hs view
@@ -11,6 +11,7 @@ import AWSLambda.Events.KinesisEvent import AWSLambda.Events.S3Event import AWSLambda.Events.SNSEvent+import AWSLambda.Events.SQSEvent -- | Not yet implemented data DynamoDBEvent@@ -60,11 +61,12 @@ -- | Sum type for all possible Lambda events. -- Parameterised on the type of SNS Events to be handled. -- See @SNSEvent@ for details.-data LambdaEvent snsMessage+data LambdaEvent message = S3 !S3Event | DynamoDB !DynamoDBEvent | KinesisStream !KinesisEvent- | SNS !(SNSEvent snsMessage)+ | SNS !(SNSEvent message)+ | SQS !(SQSEvent message) | SES !SESEvent | Cognito !CognitoEvent | CloudFormation !CloudFormationEvent@@ -84,10 +86,10 @@ -- | 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 FromText snsMessage =>- FromJSON (LambdaEvent snsMessage) where+instance FromText message =>+ FromJSON (LambdaEvent message) where parseJSON v =- try S3 v <|> try KinesisStream v <|> try SNS v <|> pure (Custom v)+ try S3 v <|> try KinesisStream v <|> try SNS v <|> try SQS v <|> pure (Custom v) where try f = fmap f . parseJSON
+ src/AWSLambda/Events/MessageAttribute.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE TemplateHaskell #-}++{-|+Module: AWSLambda.Events.MessageAttribute+Description: Types for SQS and SNS message attributes+-}+module AWSLambda.Events.MessageAttribute where++import Control.Lens (makeLenses)+import Data.Aeson.Casing (aesonPrefix, pascalCase)+import Data.Aeson.TH (deriveFromJSON)+import Data.Text (Text)++data MessageAttribute = MessageAttribute+ { _maType :: !Text+ , _maValue :: !Text+ } deriving (Eq, Show)++$(deriveFromJSON (aesonPrefix pascalCase) ''MessageAttribute)+$(makeLenses ''MessageAttribute)
src/AWSLambda/Events/SNSEvent.hs view
@@ -11,12 +11,13 @@ -} module AWSLambda.Events.SNSEvent where +import Control.Applicative ((<|>)) import Control.Lens-import Data.Aeson (FromJSON (..), genericParseJSON)+import Data.Aeson (FromJSON (..), genericParseJSON,+ withObject, (.:), (.:?), (.!=)) 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)@@ -25,16 +26,9 @@ import Network.AWS.Data.Base64 import Network.AWS.Data.Text (FromText) +import AWSLambda.Events.MessageAttribute import AWSLambda.Events.Records -data MessageAttribute = MessageAttribute- { _maType :: !Text- , _maValue :: !Text- } deriving (Eq, Show)--$(deriveFromJSON (aesonDrop 3 pascalCase) ''MessageAttribute)-$(makeLenses ''MessageAttribute)- data SNSMessage message = SNSMessage { _smMessage :: !(TextValue message ) , _smMessageAttributes :: !(HashMap Text MessageAttribute)@@ -49,8 +43,28 @@ , _smUnsubscribeUrl :: !Text } deriving (Eq, Show, Generic) +-- When a lambda is triggered directly off of an SNS topic,+-- the SNS message contains message attributes and the URI+-- fields are cased as `SigningCertUrl` and `UnsubscribeUrl`.+-- When an SNS message is embedded in an SQS event,+-- the SNS message changes in two ways; `MessageAttributes`+-- is not present and the casing for the URI fields becomes+-- `SigningCertURL` and `UnsubscribeURL`.+-- For these reasons we must hand-roll the `FromJSON` instance. instance FromText message => FromJSON (SNSMessage message) where- parseJSON = genericParseJSON $ aesonDrop 3 pascalCase+ parseJSON = withObject "SNSMessage'" $ \o ->+ SNSMessage+ <$> o .: "Message"+ <*> o .:? "MessageAttributes" .!= mempty+ <*> o .: "MessageId"+ <*> o .: "Signature"+ <*> o .: "SignatureVersion"+ <*> do o .: "SigningCertUrl" <|> o .: "SigningCertURL"+ <*> o .: "Subject"+ <*> o .: "Timestamp"+ <*> o .: "TopicArn"+ <*> o .: "Type"+ <*> do o .: "UnsubscribeUrl" <|> o .: "UnsubscribeURL" $(makeLenses ''SNSMessage)
+ src/AWSLambda/Events/SQSEvent.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++{-|+Module: AWSLambda.Events.SQSEvent+Description: Types for SQS Lambda events+-}+module AWSLambda.Events.SQSEvent where++import Control.Lens+import Data.Aeson (FromJSON (..), genericParseJSON)+import Data.Aeson.Casing (aesonPrefix, camelCase)+import Data.Aeson.Embedded+import Data.Aeson.TextValue+import Data.ByteString (ByteString)+import Data.HashMap.Strict (HashMap)+import Data.Text (Text)+import GHC.Generics (Generic)+import Network.AWS.Data.Base64+import Network.AWS.Data.Text (FromText)+import qualified Network.AWS.Types as AWS++import AWSLambda.Events.MessageAttribute+import AWSLambda.Events.Records++data SQSMessage body = SQSMessage+ { _sqsmMessageId :: !Text+ , _sqsmReceiptHandle :: !Text+ , _sqsmBody :: !(TextValue body)+ , _sqsmAttributes :: !(HashMap Text Text)+ , _sqsmMessageAttributes :: !(HashMap Text MessageAttribute)+ , _sqsmMd5OfBody :: !Text+ , _sqsmEventSource :: !Text+ , _sqsmEventSourceARN :: !Text+ , _sqsmAwsRegion :: !AWS.Region+ } deriving (Show, Eq, Generic)++instance FromText message => FromJSON (SQSMessage message) where+ parseJSON = genericParseJSON $ aesonPrefix camelCase++$(makeLenses ''SQSMessage)++type SQSEvent body = RecordsEvent (SQSMessage body)++-- | A Traversal to get messages from an SQSEvent+sqsMessages :: Traversal (SQSEvent message) (SQSEvent message') message message'+sqsMessages = reRecords . traverse . sqsmBody . unTextValue++-- | A Traversal to get embedded JSON values from an SQSEvent+sqsEmbedded :: Traversal (SQSEvent (Embedded v)) (SQSEvent (Embedded v')) v v'+sqsEmbedded = sqsMessages . unEmbed++sqsBinary :: Traversal' (SQSEvent Base64) ByteString+sqsBinary = sqsMessages . _Base64
test/AWSLambda/Events/SNSEventSpec.hs view
@@ -3,6 +3,7 @@ module AWSLambda.Events.SNSEventSpec where +import AWSLambda.Events.MessageAttribute import AWSLambda.Events.Records import AWSLambda.Events.S3Event import AWSLambda.Events.SNSEvent
+ test/AWSLambda/Events/SQSEventSpec.hs view
@@ -0,0 +1,201 @@+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module AWSLambda.Events.SQSEventSpec where++import AWSLambda.Events.MessageAttribute+import AWSLambda.Events.Records+import AWSLambda.Events.S3Event+import AWSLambda.Events.SNSEvent+import AWSLambda.Events.SQSEvent++import Data.Aeson+import Data.Aeson.Embedded+import Data.Aeson.TextValue+import Data.ByteString.Lazy (ByteString)+import Data.Text (Text)+import Data.Time.Calendar+import Data.Time.Clock+import Network.AWS.S3 as S3++import Text.RawString.QQ++import Test.Hspec++spec :: Spec+spec = describe "Handler" $ do+ it "parses sample text event" $+ decode sampleSQSJSON `shouldBe` Just sampleSQSEvent+ it "parses sample embedded S3 -> SNS -> SQS event" $+ eitherDecode sampleS3SNSSQSJSON `shouldBe` Right sampleS3SNSSQSEvent++sampleSQSJSON :: ByteString+sampleSQSJSON = [r|+{+ "Records": [+ {+ "messageId": "b792b6ba-b444-48c5-9cd8-29b8ad373eae",+ "receiptHandle": "ReceiptHandle",+ "body": "Hello from SQS!",+ "attributes": {+ "ApproximateReceiveCount": "1",+ "SentTimestamp": "1572575717896",+ "SenderId": "AIDAIY4XCTD3OFZN5ED42",+ "ApproximateFirstReceiveTimestamp": "1572575717898"+ },+ "messageAttributes": {+ "Test": {+ "Type": "String",+ "Value": "TestString"+ },+ "TestBinary": {+ "Type": "Binary",+ "Value": "TestBinary"+ }+ },+ "md5OfBody": "aca137746cb7d6a8e3eda3d1ce09b0c5",+ "eventSource": "aws:sqs",+ "eventSourceARN": "arn:aws:sqs:ap-southeast-2:1234567890:my-queue",+ "awsRegion": "ap-southeast-2"+ }+ ]+}+|]++sampleSQSEvent :: SQSEvent Text+sampleSQSEvent =+ RecordsEvent+ [ SQSMessage+ { _sqsmMessageId = "b792b6ba-b444-48c5-9cd8-29b8ad373eae"+ , _sqsmReceiptHandle = "ReceiptHandle"+ , _sqsmBody = "Hello from SQS!"+ , _sqsmAttributes =+ [ ("ApproximateReceiveCount", "1")+ , ("SentTimestamp", "1572575717896")+ , ("SenderId", "AIDAIY4XCTD3OFZN5ED42")+ , ("ApproximateFirstReceiveTimestamp", "1572575717898")+ ]+ , _sqsmMessageAttributes =+ [ ( "Test"+ , MessageAttribute+ { _maType = "String"+ , _maValue = "TestString"+ })+ , ( "TestBinary"+ , MessageAttribute+ { _maType = "Binary"+ , _maValue = "TestBinary"+ })+ ]+ , _sqsmMd5OfBody = "aca137746cb7d6a8e3eda3d1ce09b0c5"+ , _sqsmEventSource = "aws:sqs"+ , _sqsmEventSourceARN = "arn:aws:sqs:ap-southeast-2:1234567890:my-queue"+ , _sqsmAwsRegion = Sydney+ }+ ]++sampleS3SNSSQSJSON :: ByteString+sampleS3SNSSQSJSON = [r|+{+ "Records": [+ {+ "messageId": "b792b6ba-b444-48c5-9cd8-29b8ad373eae",+ "receiptHandle": "ReceiptHandle",+ "body": "{\n \"Type\" : \"Notification\",\n \"MessageId\" : \"MessageId\",\n \"TopicArn\" : \"arn:aws:sns:ap-southeast-2:11111111111111:my-topic\",\n \"Subject\" : \"Amazon S3 Notification\",\n \"Message\" : \"{\\\"Records\\\":[{\\\"eventVersion\\\":\\\"2.1\\\",\\\"eventSource\\\":\\\"aws:s3\\\",\\\"awsRegion\\\":\\\"ap-southeast-2\\\",\\\"eventTime\\\":\\\"2019-11-01T00:00:00.00Z\\\",\\\"eventName\\\":\\\"ObjectCreated:Put\\\",\\\"userIdentity\\\":{\\\"principalId\\\":\\\"AWS:AHJD568HF4356HJJ:bob\\\"},\\\"requestParameters\\\":{\\\"sourceIPAddress\\\":\\\"787.39.11.220\\\"},\\\"responseElements\\\":{\\\"x-amz-request-id\\\":\\\"GDJS6765sJSHSS\\\",\\\"x-amz-id-2\\\":\\\"ID2\\\"},\\\"s3\\\":{\\\"s3SchemaVersion\\\":\\\"1.0\\\",\\\"configurationId\\\":\\\"ConfigurationId\\\",\\\"bucket\\\":{\\\"name\\\":\\\"my-bucket\\\",\\\"ownerIdentity\\\":{\\\"principalId\\\":\\\"ASKD794UDYDH\\\"},\\\"arn\\\":\\\"arn:aws:s3:::my-bucket\\\"},\\\"object\\\":{\\\"key\\\":\\\"my-key\\\",\\\"size\\\":13315,\\\"eTag\\\":\\\"1231234fabf233124124\\\",\\\"versionId\\\":\\\"735hjf893ufb8fhuf\\\",\\\"sequencer\\\":\\\"HUJKFDHJD8656567HGGSGJKD\\\"}}}]}\",\n \"Timestamp\" : \"2019-11-01T00:00:00Z\",\n \"SignatureVersion\" : \"1\",\n \"Signature\" : \"Signature\",\n \"SigningCertURL\" : \"https://sns.ap-southeast-2.amazonaws.com/SimpleNotificationService-my-cert.pem\",\n \"UnsubscribeURL\" : \"https://sns.ap-southeast-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-southeast-2:11111111111111:my-topic:unsub\"\n}",+ "attributes": {+ "ApproximateReceiveCount": "1",+ "SentTimestamp": "1572575717896",+ "SenderId": "AIDAIY4XCTD3OFZN5ED42",+ "ApproximateFirstReceiveTimestamp": "1572575717898"+ },+ "messageAttributes": {+ "Test": {+ "Type": "String",+ "Value": "TestString"+ },+ "TestBinary": {+ "Type": "Binary",+ "Value": "TestBinary"+ }+ },+ "md5OfBody": "aca137746cb7d6a8e3eda3d1ce09b0c5",+ "eventSource": "aws:sqs",+ "eventSourceARN": "arn:aws:sqs:ap-southeast-2:1234567890:my-queue",+ "awsRegion": "ap-southeast-2"+ }+ ]+}+|]++sampleS3SNSSQSEvent :: SQSEvent (Embedded (SNSMessage (Embedded S3Event)))+sampleS3SNSSQSEvent =+ RecordsEvent+ [ SQSMessage+ { _sqsmMessageId = "b792b6ba-b444-48c5-9cd8-29b8ad373eae"+ , _sqsmReceiptHandle = "ReceiptHandle"+ , _sqsmBody =+ TextValue $ Embedded $ SNSMessage+ { _smMessage = TextValue $ Embedded $ RecordsEvent+ [ S3EventNotification+ { _senAwsRegion = Sydney+ , _senEventName = S3ObjectCreatedPut+ , _senEventSource = "aws:s3"+ , _senEventTime = UTCTime (fromGregorian 2019 11 1) 0+ , _senEventVersion = "2.1"+ , _senRequestParameters = RequestParametersEntity "787.39.11.220"+ , _senResponseElements = ResponseElementsEntity+ { _reeXAmzId2 = "ID2"+ , _reeXAmzRequestId = "GDJS6765sJSHSS" }+ , _senS3 = S3Entity+ { _seBucket = S3BucketEntity+ { _sbeArn = "arn:aws:s3:::my-bucket"+ , _sbeName = BucketName "my-bucket"+ , _sbeOwnerIdentity = UserIdentityEntity "ASKD794UDYDH" }+ , _seConfigurationId = "ConfigurationId"+ , _seObject = S3ObjectEntity+ { _soeETag = Just (ETag "1231234fabf233124124")+ , _soeKey = ObjectKey "my-key"+ , _soeSize = Just 13315+ , _soeSequencer = "HUJKFDHJD8656567HGGSGJKD"+ , _soeVersionId = Just "735hjf893ufb8fhuf" }+ , _seS3SchemaVersion = "1.0"+ }+ , _senUserIdentity = UserIdentityEntity "AWS:AHJD568HF4356HJJ:bob"+ }+ ]+ , _smMessageAttributes = mempty+ , _smMessageId = "MessageId"+ , _smSignature = "Signature"+ , _smSignatureVersion = "1"+ , _smSigningCertUrl = "https://sns.ap-southeast-2.amazonaws.com/SimpleNotificationService-my-cert.pem"+ , _smSubject = "Amazon S3 Notification"+ , _smTimestamp = UTCTime (fromGregorian 2019 11 1) 0+ , _smTopicArn = "arn:aws:sns:ap-southeast-2:11111111111111:my-topic"+ , _smType = "Notification"+ , _smUnsubscribeUrl = "https://sns.ap-southeast-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-southeast-2:11111111111111:my-topic:unsub"+ }+ , _sqsmAttributes =+ [ ("ApproximateReceiveCount", "1")+ , ("SentTimestamp", "1572575717896")+ , ("SenderId", "AIDAIY4XCTD3OFZN5ED42")+ , ("ApproximateFirstReceiveTimestamp", "1572575717898")+ ]+ , _sqsmMessageAttributes =+ [ ( "Test"+ , MessageAttribute+ { _maType = "String"+ , _maValue = "TestString"+ })+ , ( "TestBinary"+ , MessageAttribute+ { _maType = "Binary"+ , _maValue = "TestBinary"+ })+ ]+ , _sqsmMd5OfBody = "aca137746cb7d6a8e3eda3d1ce09b0c5"+ , _sqsmEventSource = "aws:sqs"+ , _sqsmEventSourceARN = "arn:aws:sqs:ap-southeast-2:1234567890:my-queue"+ , _sqsmAwsRegion = Sydney+ }+ ]