serverless-haskell (empty) → 0.0.0
raw patch · 18 files changed
+1074/−0 lines, 18 filesdep +aesondep +aeson-casingdep +aeson-qqsetup-changed
Dependencies added: aeson, aeson-casing, aeson-qq, amazonka-core, amazonka-kinesis, amazonka-s3, base, bytestring, hspec, hspec-discover, lens, serverless-haskell, text, time, unix, unordered-containers
Files
- LICENSE +21/−0
- README.md +55/−0
- Setup.hs +2/−0
- serverless-haskell.cabal +86/−0
- src/AWSLambda.hs +56/−0
- src/AWSLambda/Events.hs +67/−0
- src/AWSLambda/Events/KinesisEvent.hs +54/−0
- src/AWSLambda/Events/Records.hs +14/−0
- src/AWSLambda/Events/S3Event.hs +109/−0
- src/AWSLambda/Events/SNSEvent.hs +57/−0
- src/AWSLambda/Handler.hs +103/−0
- src/AWSLambda/Orphans.hs +27/−0
- src/Data/Aeson/Alternative.hs +29/−0
- test/AWSLambda/Events/KinesisEventSpec.hs +69/−0
- test/AWSLambda/Events/S3EventSpec.hs +207/−0
- test/AWSLambda/Events/SNSEventSpec.hs +94/−0
- test/Data/Aeson/AlternativeSpec.hs +23/−0
- test/Spec.hs +1/−0
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2017 SEEK++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,55 @@+# Serverless Haskell++Deploying Haskell code onto [AWS Lambda] using [Serverless].++## Usage++* Initialise a Serverless project in the same directory as your Stack-enabled+ package.++* Install `serverless-haskell` plugin (_Warning_: not uploaded to NPM registry+ yet, install manually by cloning this repository and specifying its+ `serverless-plugin` directory to `npm install`).++* Add the following to `serverless.yml`:++ ```yaml+ provider:+ name: aws+ runtime: nodejs6.10++ functions:+ myfunc:+ handler: mypackage.myfunc+ # Here, mypackage is the Haskell package name and myfunc is the executable+ # name as defined in the Cabal file++ plugins:+ - serverless-haskell+ ```++* Write your `main` function:++ ```haskell+ import qualified Data.Aeson as Aeson++ import AWSLambda++ main = lambdaMain handler++ handler :: Aeson.Value -> IO [Int]+ handler evt = do+ putStrLn "This should go to logs"+ print evt+ pure [1, 2, 3]+ ```++* Use `sls deploy` to deploy the executable to AWS Lambda. **Note**: `sls deploy+ function` is not supported.++See+[AWSLambda](https://github.com/seek-oss/serverless-haskell/blob/master/src/AWSLambda.hs)+for the documentation.++[AWS Lambda]: https://aws.amazon.com/lambda/+[Serverless]: https://serverless.com/framework/
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ serverless-haskell.cabal view
@@ -0,0 +1,86 @@+-- This file has been generated from package.yaml by hpack version 0.21.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: ada4461bd3cea5ba304057b51d6d137378385405f12344e8d4c77aebd7cb26f9++name: serverless-haskell+version: 0.0.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+homepage: https://github.com/seek-oss/serverless-haskell#readme+bug-reports: https://github.com/seek-oss/serverless-haskell/issues+maintainer: akotlyarov@seek.com.au+license: MIT+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10++extra-source-files:+ README.md++source-repository head+ type: git+ location: https://github.com/seek-oss/serverless-haskell++library+ exposed-modules:+ AWSLambda+ AWSLambda.Events+ AWSLambda.Events.KinesisEvent+ AWSLambda.Events.Records+ AWSLambda.Events.S3Event+ AWSLambda.Events.SNSEvent+ AWSLambda.Handler+ AWSLambda.Orphans+ Data.Aeson.Alternative+ other-modules:+ Paths_serverless_haskell+ hs-source-dirs:+ src+ ghc-options: -Wall+ build-depends:+ aeson+ , aeson-casing+ , amazonka-core+ , amazonka-kinesis+ , amazonka-s3+ , base >=4.7 && <5+ , bytestring+ , lens+ , text+ , time+ , unix+ , unordered-containers+ default-language: Haskell2010++test-suite tests+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ AWSLambda.Events.KinesisEventSpec+ AWSLambda.Events.S3EventSpec+ AWSLambda.Events.SNSEventSpec+ Data.Aeson.AlternativeSpec+ Paths_serverless_haskell+ hs-source-dirs:+ test+ build-depends:+ aeson+ , aeson-casing+ , aeson-qq+ , amazonka-core+ , amazonka-kinesis+ , amazonka-s3+ , base >=4.7 && <5+ , bytestring+ , hspec+ , hspec-discover+ , lens+ , serverless-haskell+ , text+ , time+ , unix+ , unordered-containers+ default-language: Haskell2010
+ src/AWSLambda.hs view
@@ -0,0 +1,56 @@+{-|+Module : AWSLambda.Handler+Stability : experimental+Portability : POSIX++Tools for running Haskell on AWS Lambda.++= Usage++To deploy a Haskell function on AWS Lambda:++* Initialise a Serverless project in the same directory as your Stack-enabled+ package.++* Install @serverless-haskell@ plugin (/Warning/: not uploaded to NPM registry+ yet, install manually by cloning this repository and specifying its+ @serverless-plugin@ directory to @npm install@).++* Add the following to @serverless.yml@:++ > provider:+ > name: aws+ > runtime: nodejs6.10+ >+ > functions:+ > myfunc:+ > handler: mypackage.myfunc+ > # Here, mypackage is the Haskell package name and myfunc is the executable+ > # name as defined in the Cabal file+ >+ > plugins:+ > - serverless-haskell++* Write your @main@ function using 'AWSLambda.lambdaMain'.++* Use @sls deploy@ to deploy the executable to AWS Lambda. __Note__: @sls deploy+ function@ is not supported.++= Additional features++To add flags to @stack build@, add the following key to @serverless.yml@:++> custom:+> haskell:+> stackBuildArgs:+> - --pedantic+> - --allow-different-user+-}+module AWSLambda+ ( Handler.lambdaMain+ , module AWSLambda.Events+ ) where++import qualified AWSLambda.Handler as Handler++import AWSLambda.Events
+ src/AWSLambda/Events.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE TemplateHaskell #-}++module AWSLambda.Events where++import Control.Applicative ((<|>))+import Control.Lens.TH+import Data.Aeson (FromJSON (..), Value)++import AWSLambda.Events.KinesisEvent+import AWSLambda.Events.S3Event+import AWSLambda.Events.SNSEvent++data DynamoDBEvent++data SESEvent++data CognitoEvent++data CloudFormationEvent++data CloudWatchLogsEvent++data CloudWatchEventsEvent++data CodeCommitEvent++data ConfigEvent++data AlexaEvent++data LexEvent++data APIGatewayEvent++data IoTButtonEvent++data CloudFrontEvent++data FirehoseEvent++data InvokeEvent++data LambdaEvent+ = S3 !S3Event+ | DynamoDB !DynamoDBEvent+ | KinesisStream !KinesisEvent+ | SNS !SNSEvent+ | SES !SESEvent+ | Cognito !CognitoEvent+ | CloudFormation !CloudFormationEvent+ | CloudWatchLogs !CloudWatchLogsEvent+ | CloudWatchEvents !CloudWatchEventsEvent+ | CodeCommit !CodeCommitEvent+ | Config !ConfigEvent+ | Alexa !AlexaEvent+ | Lex !LexEvent+ | APIGateway !APIGatewayEvent+ | IoTButton !IoTButtonEvent+ | CloudFront !CloudFrontEvent+ | Firehose !FirehoseEvent+ | Invoke !InvokeEvent+ | Custom !Value++instance FromJSON LambdaEvent where+ parseJSON v = S3 <$> parseJSON v <|> pure (Custom v)++$(makePrisms ''LambdaEvent)
+ src/AWSLambda/Events/KinesisEvent.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++{-|+Module: AWSLambda.Events.KinesisEvent+Description: Types for Kinesis Lambda events++Based on https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.KinesisEvents+-}+module AWSLambda.Events.KinesisEvent where++import Control.Lens.TH+import Data.Aeson (FromJSON (..), withObject, (.:))+import Data.Aeson.Casing (aesonDrop, camelCase)+import Data.Aeson.TH (deriveFromJSON)+import Data.Text (Text)+import Network.AWS.Data.Base64 (Base64 (..))+import qualified Network.AWS.Kinesis.Types as Kinesis+import qualified Network.AWS.Types as AWS++import AWSLambda.Events.Records++data KinesisRecord = KinesisRecord+ { _krRecord :: !Kinesis.Record+ , _krKinesisSchemaVersion :: !Text+ } deriving (Eq, Show)++instance FromJSON KinesisRecord where+ parseJSON =+ withObject "KinesisRecord" $+ \o -> do+ _krKinesisSchemaVersion <- o .: "kinesisSchemaVersion"+ dataBase64 <- o .: "data"+ _krRecord <-+ Kinesis.record <$> (o .: "sequenceNumber") <*> pure (unBase64 dataBase64) <*>+ (o .: "partitionKey")+ return KinesisRecord {..}+$(makeLenses ''KinesisRecord)++data KinesisEventRecord = KinesisEventRecord+ { _kerKinesis :: !KinesisRecord+ , _kerEventSource :: !Text+ , _kerEventID :: !Text+ , _kerInvokeIdentityArn :: !Text+ , _kerEventVersion :: !Text+ , _kerEventName :: !Text+ , _kerEventSourceARN :: !Text+ , _kerAwsRegion :: !AWS.Region+ } deriving (Eq, Show)+$(deriveFromJSON (aesonDrop 4 camelCase) ''KinesisEventRecord)+$(makeLenses ''KinesisEventRecord)++type KinesisEvent = RecordsEvent KinesisEventRecord
+ src/AWSLambda/Events/Records.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module AWSLambda.Events.Records where++import Control.Lens.TH (makeLenses)+import Data.Aeson (FromJSON (..), withObject, (.:))++newtype RecordsEvent a = RecordsEvent { _reRecords :: [a] } deriving (Eq, Show)++instance FromJSON a => FromJSON (RecordsEvent a) where+ parseJSON = withObject "RecordsEvent" $ \o -> RecordsEvent <$> o .: "Records"++$(makeLenses ''RecordsEvent)
+ src/AWSLambda/Events/S3Event.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++{-|+Module: AWSLambda.Events.S3Event+Description: Types for S3 Lambda events++Based on https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.S3Events+-}+module AWSLambda.Events.S3Event where++import Control.Lens.TH+import Control.Monad (guard)+import Data.Aeson (FromJSON (..), withObject, (.:))+import Data.Aeson.Casing (aesonDrop, camelCase)+import Data.Aeson.TH (deriveFromJSON)+import Data.Text (Text)+import Data.Time.Clock (UTCTime)+import qualified Network.AWS.S3 as S3+import qualified Network.AWS.Types as AWS++import AWSLambda.Events.Records+import AWSLambda.Orphans ()++newtype UserIdentityEntity = UserIdentityEntity+ { _uiePrincipalId :: Text+ } deriving (Eq, Show)++$(deriveFromJSON (aesonDrop 4 camelCase) ''UserIdentityEntity)+$(makeLenses ''UserIdentityEntity)++data S3BucketEntity = S3BucketEntity+ { _sbeArn :: !Text+ , _sbeName :: !S3.BucketName+ , _sbeOwnerIdentity :: !UserIdentityEntity+ } deriving (Eq, Show)++$(deriveFromJSON (aesonDrop 4 camelCase) ''S3BucketEntity)+$(makeLenses ''S3BucketEntity)++data S3ObjectEntity = S3ObjectEntity+ { _soeETag :: !(Maybe S3.ETag)+ , _soeKey :: !S3.ObjectKey+ , _soeSize :: !(Maybe Integer)+ , _soeSequencer :: !Text+ , _soeVersionId :: !(Maybe Text)+ } deriving (Eq, Show)++$(deriveFromJSON (aesonDrop 4 camelCase) ''S3ObjectEntity)+$(makeLenses ''S3ObjectEntity)++newtype RequestParametersEntity = RequestParametersEntity+ { _rpeSourceIPAddress :: Text+ } deriving (Eq, Show)++$(deriveFromJSON (aesonDrop 4 camelCase) ''RequestParametersEntity)+$(makeLenses ''RequestParametersEntity)++data ResponseElementsEntity = ResponseElementsEntity+ { _reeXAmzId2 :: !Text+ , _reeXAmzRequestId :: !Text+ } deriving (Eq, Show)++instance FromJSON ResponseElementsEntity where+ parseJSON =+ withObject "ResponseElementsEntity" $+ \o ->+ ResponseElementsEntity <$> o .: "x-amz-id-2" <*> o .: "x-amz-request-id"+$(makeLenses ''ResponseElementsEntity)++data S3Entity = S3Entity+ { _seBucket :: !S3BucketEntity+ , _seConfigurationId :: !Text+ , _seObject :: !S3ObjectEntity+ , _seS3SchemaVersion :: !Text+ } deriving (Eq, Show)++$(deriveFromJSON (aesonDrop 3 camelCase) ''S3Entity)+$(makeLenses ''S3Entity)++data S3EventNotification = S3EventNotification+ { _senAwsRegion :: !AWS.Region+ , _senEventName :: !S3.Event+ , _senEventSource :: !Text+ , _senEventTime :: !UTCTime+ , _senEventVersion :: !Text+ , _senRequestParameters :: !RequestParametersEntity+ , _senResponseElements :: !ResponseElementsEntity+ , _senS3 :: !S3Entity+ , _senUserIdentity :: !UserIdentityEntity+ } deriving (Eq, Show)++instance FromJSON S3EventNotification where+ parseJSON = withObject "S3EventNotification" $ \o -> do+ _senEventSource <- o .: "eventSource"+ guard $ _senEventSource == "aws:s3"+ _senAwsRegion <- o .: "awsRegion"+ _senEventName <- o .: "eventName"+ _senEventTime <- o .: "eventTime"+ _senEventVersion <- o .: "eventVersion"+ _senRequestParameters <- o .: "requestParameters"+ _senResponseElements <- o .: "responseElements"+ _senS3 <- o .: "s3"+ _senUserIdentity <- o .: "userIdentity"+ return S3EventNotification {..}+$(makeLenses ''S3EventNotification)++type S3Event = RecordsEvent S3EventNotification
+ src/AWSLambda/Events/SNSEvent.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++{-|+Module: AWSLambda.Events.SNSEvent+Description: Types for SNS Lambda events++Based on https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.SNSEvents+-}+module AWSLambda.Events.SNSEvent where++import Control.Lens.TH+import Data.Aeson.Casing (aesonDrop, pascalCase)+import Data.Aeson.TH (deriveFromJSON)+import Data.HashMap.Strict (HashMap)+import Data.Text (Text)+import Data.Time.Clock (UTCTime)++import AWSLambda.Events.Records++data MessageAttribute = MessageAttribute+ { _maType :: !Text+ , _maValue :: !Text+ } deriving (Eq, Show)++$(deriveFromJSON (aesonDrop 3 pascalCase) ''MessageAttribute)+$(makeLenses ''MessageAttribute)++data SNSMessage = SNSMessage+ { _smMessage :: !Text+ , _smMessageAttributes :: !(HashMap Text MessageAttribute)+ , _smMessageId :: !Text+ , _smSignature :: !Text+ , _smSignatureVersion :: !Text+ , _smSigningCertUrl :: !Text+ , _smSubject :: !Text+ , _smTimestamp :: !UTCTime+ , _smTopicArn :: !Text+ , _smType :: !Text+ , _smUnsubscribeUrl :: !Text+ } deriving (Eq, Show)++$(deriveFromJSON (aesonDrop 3 pascalCase) ''SNSMessage)+$(makeLenses ''SNSMessage)++data SNSRecord = SNSRecord+ { _srEventVersion :: !Text+ , _srEventSubscriptionArn :: !Text+ , _srEventSource :: !Text+ , _srSns :: !SNSMessage+ } deriving (Eq, Show)++$(deriveFromJSON (aesonDrop 3 pascalCase) ''SNSRecord)+$(makeLenses ''SNSRecord)++type SNSEvent = RecordsEvent SNSRecord
+ src/AWSLambda/Handler.hs view
@@ -0,0 +1,103 @@+{-|+Module : AWSLambda.Handler+Stability : experimental+Portability : POSIX++Entry point for AWS Lambda handlers deployed with @serverless-haskell@ plugin.+-}+module AWSLambda.Handler+ ( lambdaMain+ ) where++import Control.Exception (bracket)++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Text as Aeson++import qualified Data.ByteString as ByteString++import qualified Data.Text.Lazy.IO as Text++import GHC.IO.Handle (Handle, hClose)++import System.Environment (lookupEnv)+import System.IO (stdout)+import System.Posix.IO (fdToHandle)+import System.Posix.Types (Fd(..))++-- | Process incoming events from @serverless-haskell@ using a provided+-- function.+--+-- 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 qualified Data.Aeson as Aeson+-- >+-- > import AWSLambda+-- >+-- > main = lambdaMain handler+-- >+-- > handler :: Aeson.Value -> IO [Int]+-- > handler evt = do+-- > putStrLn "This should go to logs"+-- > print evt+-- > pure [1, 2, 3]+--+-- The handler function can receive arbitrary JSON values from custom+-- invocations, or one of the events from the "AWSLambda.Events" module, such as+-- 'AWSLambda.Events.S3Event':+--+-- > import AWSLambda.Events.S3Event+-- >+-- > handler :: S3Event -> IO ()+-- > handler evt = do+-- > print $ records evt+--+-- If the Lambda function needs to process several types of events, use+-- 'Data.Aeson.Alternative' to combine several handlers:+--+-- > import AWSLambda+-- > import AWSLambda.Events.S3Event+-- > import Data.Aeson+-- > import Data.Aeson.Alternative+-- >+-- > main = lambdaMain $ handlerS3 `alternative` handlerCustom+-- >+-- > handlerS3 :: S3Event -> IO ()+-- > handlerS3 = _+-- >+-- > handlerCustom :: Value -> IO ()+-- > handlerCustom = _+--+-- When run outside the AWS Lambda environment, the input is read as JSON from+-- the command line, and the result of the execution is printed, also as JSON,+-- to the standard output.+lambdaMain ::+ (Aeson.FromJSON event, Aeson.ToJSON res)+ => (event -> IO res) -- ^ Function to process the event+ -> IO ()+lambdaMain act =+ withResultChannel $ \resultChannel -> do+ input <- ByteString.getLine+ case Aeson.eitherDecodeStrict input of+ Left err -> error err+ Right event -> do+ result <- act event+ Text.hPutStrLn resultChannel $ Aeson.encodeToLazyText result+ pure ()++-- | Invoke an action with the handle to write the results to. On AWS Lambda,+-- use the channel opened by the JavaScript wrapper, otherwise use standard+-- output.+withResultChannel :: (Handle -> IO r) -> IO r+withResultChannel act = do+ lambdaFunctionName <- lookupEnv "AWS_LAMBDA_FUNCTION_NAME"+ case lambdaFunctionName of+ Just _ -> bracket (fdToHandle communicationFd) hClose act+ Nothing -> act stdout++-- | File descriptor opened by the JavaScript wrapper to listen for the results+communicationFd :: Fd+communicationFd = Fd 3
+ src/AWSLambda/Orphans.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+module AWSLambda.Orphans where++import Data.Aeson+import Data.Monoid ((<>))+import qualified Data.Text as Text+import Network.AWS.Data.Text (fromText)+import qualified Network.AWS.S3 as S3++deriving instance FromJSON S3.BucketName++deriving instance FromJSON S3.ObjectKey++instance FromJSON S3.ETag where+ parseJSON = withText "ETag" $ either fail return . fromText++instance FromJSON S3.Event where+ parseJSON = withText "Event" $ either fail return . fromText . addS3Prefix+ where+ s3Prefix = "s3:"+ addS3Prefix s =+ if s3Prefix `Text.isPrefixOf` s+ then s+ else s3Prefix <> s
+ src/Data/Aeson/Alternative.hs view
@@ -0,0 +1,29 @@+{-|+Module : Data.Aeson.Alternative+Stability : experimental++Utilities for decoding JSON into one of the possible types and handling the+resulting sum type.+-}+module Data.Aeson.Alternative+ ( AlternativeJSON+ , alternative+ ) where++import Control.Applicative++import Data.Aeson++-- | One of the two values that has been parsed from JSON+data AlternativeJSON a b+ = FirstJSON a+ | SecondJSON b+ deriving (Eq, Ord, Show)++instance (FromJSON a, FromJSON b) => FromJSON (AlternativeJSON a b) where+ parseJSON v = FirstJSON <$> parseJSON v <|> SecondJSON <$> parseJSON v++-- | Handle either of the two types that have been parsed from JSON+alternative :: (a -> r) -> (b -> r) -> AlternativeJSON a b -> r+alternative f _ (FirstJSON a) = f a+alternative _ g (SecondJSON b) = g b
+ test/AWSLambda/Events/KinesisEventSpec.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module AWSLambda.Events.KinesisEventSpec where++import AWSLambda.Events.KinesisEvent+import AWSLambda.Events.Records++import Data.Aeson+import Data.Aeson.QQ+import Data.ByteString (ByteString)+import qualified Network.AWS.Kinesis.Types as Kinesis+import Network.AWS.Types (Region (..))++import Test.Hspec++spec :: Spec+spec =+ describe "KinesisEvent" $+ it "parses sample event" $+ fromJSON sampleKinesisJSON `shouldBe` Success sampleKinesisEvent++sampleKinesisJSON :: Value+sampleKinesisJSON = [aesonQQ|+{+ "Records": [+ {+ "kinesis": {+ "partitionKey": "partitionKey-3",+ "kinesisSchemaVersion": "1.0",+ "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=",+ "sequenceNumber": "49545115243490985018280067714973144582180062593244200961"+ },+ "eventSource": "aws:kinesis",+ "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961",+ "invokeIdentityArn": "arn:aws:iam::account-id:role/testLEBRole",+ "eventVersion": "1.0",+ "eventName": "aws:kinesis:record",+ "eventSourceARN": "arn:aws:kinesis:us-west-2:35667example:stream/examplestream",+ "awsRegion": "us-west-2"+ }+ ]+}+|]++sampleKinesisEvent :: KinesisEvent+sampleKinesisEvent =+ RecordsEvent+ [ KinesisEventRecord+ { _kerKinesis =+ KinesisRecord+ { _krRecord =+ Kinesis.record+ "49545115243490985018280067714973144582180062593244200961"+ "Hello, this is a test 123."+ "partitionKey-3"+ , _krKinesisSchemaVersion = "1.0"+ }+ , _kerEventSource = "aws:kinesis"+ , _kerEventID =+ "shardId-000000000000:49545115243490985018280067714973144582180062593244200961"+ , _kerInvokeIdentityArn = "arn:aws:iam::account-id:role/testLEBRole"+ , _kerEventVersion = "1.0"+ , _kerEventName = "aws:kinesis:record"+ , _kerEventSourceARN =+ "arn:aws:kinesis:us-west-2:35667example:stream/examplestream"+ , _kerAwsRegion = Oregon+ }+ ]
+ test/AWSLambda/Events/S3EventSpec.hs view
@@ -0,0 +1,207 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module AWSLambda.Events.S3EventSpec where++import AWSLambda.Events.Records+import AWSLambda.Events.S3Event++import Data.Aeson+import Data.Aeson.QQ+import Data.ByteString (ByteString)+import qualified Data.HashMap.Strict as HashMap+import Data.Time.Calendar+import Data.Time.Clock+import Network.AWS.S3++import Test.Hspec++spec :: Spec+spec =+ describe "S3Event" $ do+ it "parses sample Put event" $+ fromJSON sampleS3PutJSON `shouldBe` Success sampleS3PutEvent+ it "parses sample Delete event" $+ fromJSON sampleS3DeleteJSON `shouldBe` Success sampleS3DeleteEvent++sampleS3PutJSON :: Value+sampleS3PutJSON = [aesonQQ|+{+ "Records": [+ {+ "eventVersion": "2.0",+ "eventTime": "1970-01-01T00:00:00.000Z",+ "requestParameters": {+ "sourceIPAddress": "127.0.0.1"+ },+ "s3": {+ "configurationId": "testConfigRule",+ "object": {+ "eTag": "0123456789abcdef0123456789abcdef",+ "sequencer": "0A1B2C3D4E5F678901",+ "key": "HappyFace.jpg",+ "size": 1024+ },+ "bucket": {+ "arn": "bucketarn",+ "name": "sourcebucket",+ "ownerIdentity": {+ "principalId": "EXAMPLE"+ }+ },+ "s3SchemaVersion": "1.0"+ },+ "responseElements": {+ "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH",+ "x-amz-request-id": "EXAMPLE123456789"+ },+ "awsRegion": "us-east-1",+ "eventName": "ObjectCreated:Put",+ "userIdentity": {+ "principalId": "EXAMPLE"+ },+ "eventSource": "aws:s3"+ }+ ]+}+|]++sampleS3PutEvent :: S3Event+sampleS3PutEvent =+ RecordsEvent+ { _reRecords =+ [ S3EventNotification+ { _senAwsRegion = NorthVirginia+ , _senEventName = S3ObjectCreatedPut+ , _senEventSource = "aws:s3"+ , _senEventTime = UTCTime (fromGregorian 1970 1 1) (secondsToDiffTime 0)+ , _senEventVersion = "2.0"+ , _senRequestParameters =+ RequestParametersEntity+ { _rpeSourceIPAddress = "127.0.0.1"+ }+ , _senResponseElements =+ ResponseElementsEntity+ { _reeXAmzId2 =+ "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH"+ , _reeXAmzRequestId = "EXAMPLE123456789"+ }+ , _senS3 =+ S3Entity+ { _seBucket =+ S3BucketEntity+ { _sbeArn = "bucketarn"+ , _sbeName = BucketName "sourcebucket"+ , _sbeOwnerIdentity =+ UserIdentityEntity+ { _uiePrincipalId = "EXAMPLE"+ }+ }+ , _seConfigurationId = "testConfigRule"+ , _seObject =+ S3ObjectEntity+ { _soeETag = Just (ETag "0123456789abcdef0123456789abcdef")+ , _soeKey = ObjectKey "HappyFace.jpg"+ , _soeSize = Just 1024+ , _soeSequencer = "0A1B2C3D4E5F678901"+ , _soeVersionId = Nothing+ }+ , _seS3SchemaVersion = "1.0"+ }+ , _senUserIdentity =+ UserIdentityEntity+ { _uiePrincipalId = "EXAMPLE"+ }+ }+ ]+ }++sampleS3DeleteJSON :: Value+sampleS3DeleteJSON = [aesonQQ|+ {+ "Records": [+ {+ "eventVersion": "2.0",+ "eventTime": "1970-01-01T00:00:00.000Z",+ "requestParameters": {+ "sourceIPAddress": "127.0.0.1"+ },+ "s3": {+ "configurationId": "testConfigRule",+ "object": {+ "sequencer": "0A1B2C3D4E5F678901",+ "key": "HappyFace.jpg"+ },+ "bucket": {+ "arn": "bucketarn",+ "name": "sourcebucket",+ "ownerIdentity": {+ "principalId": "EXAMPLE"+ }+ },+ "s3SchemaVersion": "1.0"+ },+ "responseElements": {+ "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH",+ "x-amz-request-id": "EXAMPLE123456789"+ },+ "awsRegion": "us-east-1",+ "eventName": "ObjectRemoved:Delete",+ "userIdentity": {+ "principalId": "EXAMPLE"+ },+ "eventSource": "aws:s3"+ }+ ]+}+|]++sampleS3DeleteEvent :: S3Event+sampleS3DeleteEvent =+ RecordsEvent+ { _reRecords =+ [ S3EventNotification+ { _senAwsRegion = NorthVirginia+ , _senEventName = S3ObjectRemovedDelete+ , _senEventSource = "aws:s3"+ , _senEventTime = UTCTime (fromGregorian 1970 1 1) (secondsToDiffTime 0)+ , _senEventVersion = "2.0"+ , _senRequestParameters =+ RequestParametersEntity+ { _rpeSourceIPAddress = "127.0.0.1"+ }+ , _senResponseElements =+ ResponseElementsEntity+ { _reeXAmzId2 =+ "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH"+ , _reeXAmzRequestId = "EXAMPLE123456789"+ }+ , _senS3 =+ S3Entity+ { _seBucket =+ S3BucketEntity+ { _sbeArn = "bucketarn"+ , _sbeName = BucketName "sourcebucket"+ , _sbeOwnerIdentity =+ UserIdentityEntity+ { _uiePrincipalId = "EXAMPLE"+ }+ }+ , _seConfigurationId = "testConfigRule"+ , _seObject =+ S3ObjectEntity+ { _soeETag = Nothing+ , _soeKey = ObjectKey "HappyFace.jpg"+ , _soeSize = Nothing+ , _soeSequencer = "0A1B2C3D4E5F678901"+ , _soeVersionId = Nothing+ }+ , _seS3SchemaVersion = "1.0"+ }+ , _senUserIdentity =+ UserIdentityEntity+ { _uiePrincipalId = "EXAMPLE"+ }+ }+ ]+ }
+ test/AWSLambda/Events/SNSEventSpec.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module AWSLambda.Events.SNSEventSpec where++import AWSLambda.Events.Records+import AWSLambda.Events.SNSEvent++import Data.Aeson+import Data.Aeson.QQ+import Data.ByteString (ByteString)+import qualified Data.HashMap.Strict as HashMap+import Data.Time.Clock+import Data.Time.Calendar+import Network.AWS.Types (Region (..))++import Test.Hspec++spec :: Spec+spec =+ describe "SNSEvent" $+ it "parses sample event" $+ fromJSON sampleSNSJSON `shouldBe` Success sampleSNSEvent++sampleSNSJSON :: Value+sampleSNSJSON = [aesonQQ|+{+ "Records": [+ {+ "EventVersion": "1.0",+ "EventSubscriptionArn": "eventsubscriptionarn",+ "EventSource": "aws:sns",+ "Sns": {+ "SignatureVersion": "1",+ "Timestamp": "1970-01-01T00:00:00.000Z",+ "Signature": "EXAMPLE",+ "SigningCertUrl": "EXAMPLE",+ "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",+ "Message": "Hello from SNS!",+ "MessageAttributes": {+ "Test": {+ "Type": "String",+ "Value": "TestString"+ },+ "TestBinary": {+ "Type": "Binary",+ "Value": "TestBinary"+ }+ },+ "Type": "Notification",+ "UnsubscribeUrl": "EXAMPLE",+ "TopicArn": "topicarn",+ "Subject": "TestInvoke"+ }+ }+ ]+}+|]++sampleSNSEvent :: SNSEvent+sampleSNSEvent =+ RecordsEvent+ [ SNSRecord+ { _srEventVersion = "1.0"+ , _srEventSubscriptionArn = "eventsubscriptionarn"+ , _srEventSource = "aws:sns"+ , _srSns =+ SNSMessage+ { _smMessage = "Hello from SNS!"+ , _smMessageAttributes =+ HashMap.fromList+ [ ( "Test"+ , MessageAttribute+ { _maType = "String"+ , _maValue = "TestString"+ })+ , ( "TestBinary"+ , MessageAttribute+ { _maType = "Binary"+ , _maValue = "TestBinary"+ })+ ]+ , _smMessageId = "95df01b4-ee98-5cb9-9903-4c221d41eb5e"+ , _smSignature = "EXAMPLE"+ , _smSignatureVersion = "1"+ , _smSigningCertUrl = "EXAMPLE"+ , _smSubject = "TestInvoke"+ , _smTimestamp = UTCTime (fromGregorian 1970 1 1) (secondsToDiffTime 0)+ , _smTopicArn = "topicarn"+ , _smType = "Notification"+ , _smUnsubscribeUrl = "EXAMPLE"+ }+ }+ ]
+ test/Data/Aeson/AlternativeSpec.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.Aeson.AlternativeSpec where++import Data.Aeson+import Data.Aeson.Alternative++import Test.Hspec++spec :: Spec+spec =+ describe "AlternativeJSON" $ do+ let handler1 :: [Int] -> Int+ handler1 = sum+ let handler2 :: Bool -> Int+ handler2 = fromEnum+ let handler = handler1 `alternative` handler2+ it "parses the first alternative" $+ (handler <$> decode "[1, 2, 3]") `shouldBe` (Just 6)+ it "parses the second alternative" $+ (handler <$> decode "true") `shouldBe` (Just 1)+ it "fails to parse anything else" $+ (handler <$> decode "{}") `shouldBe` Nothing
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}