packages feed

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 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 #-}