serverless-haskell 0.4.3 → 0.5.0
raw patch · 5 files changed
+124/−55 lines, 5 filesdep +http-typesdep +iproute
Dependencies added: http-types, iproute
Files
- README.md +13/−4
- serverless-haskell.cabal +31/−28
- src/AWSLambda.hs +1/−1
- src/AWSLambda/Events/APIGateway.hs +71/−16
- test/AWSLambda/Events/APIGatewaySpec.hs +8/−6
README.md view
@@ -40,13 +40,17 @@ provider: name: aws- runtime: nodejs6.10+ runtime: nodejs8.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+ # name as defined in the Cabal file. The handler field may be prefixed+ # with a path of the form `dir1/.../dirn`, relative to `serverless.yml`,+ # which points to the location where the Haskell package `mypackage` is+ # defined. This prefix is not needed when the Stack project is defined at+ # the same level as `serverless.yml`. plugins: - serverless-haskell@@ -96,13 +100,18 @@ ### Integration tests -Integration tests are not run automatically due to the need for an AWS account.-To run them manually:+Integration test verifies that the project can build and deploy a complete+function to AWS, and it runs with expected functionality. +Integration test is only automatically run up to deployment due to the need for+an AWS account. To run manually:+ * Ensure you have the required dependencies: `curl`, [jq], [NPM], `pwgen` and [Stack]. * Get an AWS account and add the access credentials into your shell environment. * Run `./integration-test/run.sh`. The exit code indicates success.+* To verify just the packaging, without deployment, run+ `./integration-test/run.sh --dry-run`. ## Releasing
serverless-haskell.cabal view
@@ -1,11 +1,11 @@--- This file has been generated from package.yaml by hpack version 0.20.0.+-- This file has been generated from package.yaml by hpack version 0.28.2. -- -- see: https://github.com/sol/hpack ----- hash: 298ad7fe6395331a112788dc36344fd07fa6c61411aa488fd4f0b128edec9710+-- hash: 6aead2a5ac837e6007c05071a0b459bd55ce9a015ff3fc03b6d176d288dd453a name: serverless-haskell-version: 0.4.3+version: 0.5.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@@ -16,7 +16,6 @@ license-file: LICENSE build-type: Simple cabal-version: >= 1.10- extra-source-files: README.md @@ -25,6 +24,21 @@ location: https://github.com/seek-oss/serverless-haskell library+ exposed-modules:+ AWSLambda+ AWSLambda.Events+ AWSLambda.Events.APIGateway+ AWSLambda.Events.KinesisEvent+ AWSLambda.Events.Records+ AWSLambda.Events.S3Event+ AWSLambda.Events.SNSEvent+ AWSLambda.Handler+ AWSLambda.Orphans+ Data.Aeson.Alternative+ Data.Aeson.Embedded+ Data.Aeson.TextValue+ other-modules:+ Paths_serverless_haskell hs-source-dirs: src ghc-options: -Wall@@ -38,31 +52,27 @@ , base >=4.7 && <5 , bytestring , case-insensitive+ , http-types+ , iproute , lens , text , time , unix , unordered-containers- exposed-modules:- AWSLambda- AWSLambda.Events- AWSLambda.Events.APIGateway- AWSLambda.Events.KinesisEvent- AWSLambda.Events.Records- AWSLambda.Events.S3Event- AWSLambda.Events.SNSEvent- AWSLambda.Handler- AWSLambda.Orphans- Data.Aeson.Alternative- Data.Aeson.Embedded- Data.Aeson.TextValue- other-modules:- Paths_serverless_haskell default-language: Haskell2010 test-suite tests type: exitcode-stdio-1.0 main-is: Spec.hs+ other-modules:+ AWSLambda.Events.APIGatewaySpec+ AWSLambda.Events.KinesisEventSpec+ AWSLambda.Events.S3EventSpec+ AWSLambda.Events.SNSEventSpec+ Data.Aeson.AlternativeSpec+ Data.Aeson.EmbeddedSpec+ Data.Aeson.TestUtil+ Paths_serverless_haskell hs-source-dirs: test ghc-options: -Wall@@ -78,6 +88,8 @@ , case-insensitive , hspec , hspec-discover+ , http-types+ , iproute , lens , raw-strings-qq , serverless-haskell@@ -85,13 +97,4 @@ , time , unix , unordered-containers- other-modules:- AWSLambda.Events.APIGatewaySpec- AWSLambda.Events.KinesisEventSpec- AWSLambda.Events.S3EventSpec- AWSLambda.Events.SNSEventSpec- Data.Aeson.AlternativeSpec- Data.Aeson.EmbeddedSpec- Data.Aeson.TestUtil- Paths_serverless_haskell default-language: Haskell2010
src/AWSLambda.hs view
@@ -21,7 +21,7 @@ > > provider: > name: aws- > runtime: nodejs6.10+ > runtime: nodejs8.10 > > functions: > myfunc:
src/AWSLambda/Events/APIGateway.hs view
@@ -11,20 +11,27 @@ -} module AWSLambda.Events.APIGateway where -import Control.Lens+import Control.Lens hiding ((.=)) 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.Aeson.Types (Parser) import Data.ByteString (ByteString)+import qualified Data.CaseInsensitive as CI import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap+import Data.IP import Data.Text (Text)+import qualified Data.Text as Text+import Data.Text.Encoding (decodeUtf8, encodeUtf8) import GHC.Generics (Generic) import Network.AWS.Data.Base64 import Network.AWS.Data.Text+import qualified Network.HTTP.Types as HTTP+import Text.Read import AWSLambda.Handler (lambdaMain) @@ -45,7 +52,7 @@ , _riCognitoIdentityId :: !(Maybe Text) , _riCaller :: !(Maybe Text) , _riApiKey :: !(Maybe Text)- , _riSourceIp :: !(Maybe Text)+ , _riSourceIp :: !(Maybe IP) , _riCognitoAuthenticationType :: !(Maybe Text) , _riCognitoAuthenticationProvider :: !(Maybe Text) , _riUserArn :: !(Maybe Text)@@ -53,7 +60,25 @@ , _riUser :: !(Maybe Text) } deriving (Eq, Show) -$(deriveFromJSON (aesonDrop 3 camelCase) ''RequestIdentity)+readParse :: Read a => String -> Text -> Parser a+readParse msg str =+ case readMaybe (Text.unpack str) of+ Just result -> pure result+ Nothing -> fail $ "Failed to parse an " ++ msg++instance FromJSON RequestIdentity where+ parseJSON =+ withObject "RequestIdentity" $ \o ->+ RequestIdentity <$> o .:? "cognitoIdentityPoolId" <*> o .:? "accountId" <*>+ o .:? "cognitoIdentityId" <*>+ o .:? "caller" <*>+ o .:? "apiKey" <*>+ (o .:? "sourceIp" >>= traverse (readParse "IP address")) <*>+ o .:? "cognitoAuthenticationType" <*>+ o .:? "cognitoAuthenticationProvider" <*>+ o .:? "userArn" <*>+ o .:? "userAgent" <*>+ o .:? "user" $(makeLenses ''RequestIdentity) data ProxyRequestContext = ProxyRequestContext@@ -66,16 +91,17 @@ , _prcResourcePath :: !Text , _prcHttpMethod :: !Text , _prcApiId :: !Text+ , _prcProtocol :: !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)+ , _agprqPath :: !ByteString+ , _agprqHttpMethod :: !HTTP.Method+ , _agprqHeaders :: !HTTP.RequestHeaders+ , _agprqQueryStringParameters :: !HTTP.Query , _agprqPathParameters :: !(HashMap PathParamName PathParamValue) , _agprqStageVariables :: !(HashMap StageVarName StageVarValue) , _agprqRequestContext :: !ProxyRequestContext@@ -86,15 +112,25 @@ parseJSON = withObject "APIGatewayProxyRequest" $ \o -> APIGatewayProxyRequest <$> o .: "resource"- <*> o .: "path"- <*> o .: "httpMethod"- <*> o .:? "headers" .!= HashMap.empty- <*> o .:? "queryStringParameters" .!= HashMap.empty+ <*> (encodeUtf8 <$> o .: "path")+ <*> (encodeUtf8 <$> o .: "httpMethod")+ <*> (fmap fromAWSHeaders <$> o .:? "headers") .!= mempty+ <*> (fmap fromAWSQuery <$> o .:? "queryStringParameters") .!= mempty <*> o .:? "pathParameters" .!= HashMap.empty <*> o .:? "stageVariables" .!= HashMap.empty <*> o .: "requestContext" <*> o .:? "body"-+ where+ -- Explicit type signatures so that we don't accidentally tell Aeson+ -- to try to parse the wrong sort of structure+ fromAWSHeaders :: HashMap HeaderName HeaderValue -> HTTP.RequestHeaders+ fromAWSHeaders = fmap toHeader . HashMap.toList+ where+ toHeader = bimap (CI.mk . encodeUtf8) encodeUtf8+ fromAWSQuery :: HashMap QueryParamName QueryParamValue -> HTTP.Query+ fromAWSQuery = fmap toQueryItem . HashMap.toList+ where+ toQueryItem = bimap encodeUtf8 (\x -> if Text.null x then Nothing else Just . encodeUtf8 $ x) $(makeLenses ''APIGatewayProxyRequest) @@ -112,20 +148,39 @@ data APIGatewayProxyResponse body = APIGatewayProxyResponse { _agprsStatusCode :: !Int- , _agprsHeaders :: !(HashMap HeaderName HeaderValue)+ , _agprsHeaders :: !HTTP.ResponseHeaders , _agprsBody :: !(Maybe (TextValue body)) } deriving (Eq, Show, Generic) instance ToText body => ToJSON (APIGatewayProxyResponse body) where- toJSON = genericToJSON $ aesonDrop 6 camelCase+ toJSON APIGatewayProxyResponse {..} =+ object+ [ "statusCode" .= _agprsStatusCode+ , "headers" .= toAWSHeaders _agprsHeaders+ , "body" .= _agprsBody+ ]+ where+ toAWSHeaders :: HTTP.ResponseHeaders -> HashMap HeaderName HeaderValue+ toAWSHeaders = HashMap.fromList . fmap (bimap (decodeUtf8 . CI.original) decodeUtf8) instance FromText body => FromJSON (APIGatewayProxyResponse body) where- parseJSON = genericParseJSON $ aesonDrop 6 camelCase+ parseJSON =+ withObject "APIGatewayProxyResponse" $ \o ->+ APIGatewayProxyResponse <$> o .: "statusCode" <*>+ (fromAWSHeaders <$> o .: "headers") <*>+ o .:? "body"+ -- Explicit type signatures so that we don't accidentally tell Aeson+ -- to try to parse the wrong sort of structure+ where+ fromAWSHeaders :: HashMap HeaderName HeaderValue -> HTTP.RequestHeaders+ fromAWSHeaders = fmap toHeader . HashMap.toList+ where+ toHeader = bimap (CI.mk . encodeUtf8) encodeUtf8 $(makeLenses ''APIGatewayProxyResponse) response :: Int -> APIGatewayProxyResponse body-response statusCode = APIGatewayProxyResponse statusCode HashMap.empty Nothing+response statusCode = APIGatewayProxyResponse statusCode mempty Nothing responseOK :: APIGatewayProxyResponse body responseOK = response 200
test/AWSLambda/Events/APIGatewaySpec.hs view
@@ -9,6 +9,7 @@ import Data.Aeson import Data.ByteString.Lazy (ByteString) import qualified Data.HashMap.Strict as HashMap+import Data.IP import Data.Text (Text) import Text.RawString.QQ@@ -19,10 +20,10 @@ spec = do describe "APIGatewayProxyRequest" $ it "parses sample GET request" $- decode sampleGetRequestJSON `shouldBe` Just sampleGetRequest+ eitherDecode sampleGetRequestJSON `shouldBe` Right sampleGetRequest describe "APIGatewayProxyResponse" $ it "parses sample text event" $- decode sampleOKResponseJSON `shouldBe` Just sampleOKResponse+ eitherDecode sampleOKResponseJSON `shouldBe` Right sampleOKResponse sampleGetRequestJSON :: ByteString sampleGetRequestJSON = [r|@@ -55,6 +56,7 @@ "resourceId": "us4z18", "stage": "test", "requestId": "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9",+ "protocol": "HTTP/1.1", "identity": { "cognitoIdentityPoolId": "", "accountId": "",@@ -90,7 +92,6 @@ , _agprqPath = "/test/hello" , _agprqHttpMethod = "GET" , _agprqHeaders =- HashMap.fromList [ ("X-Forwarded-Proto", "https") , ("CloudFront-Is-Desktop-Viewer", "true") , ( "Accept"@@ -113,7 +114,7 @@ , "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)") , ("X-Forwarded-For", "192.168.100.1, 192.168.1.1") ]- , _agprqQueryStringParameters = HashMap.fromList [("name", "me")]+ , _agprqQueryStringParameters = [("name", Just "me")] , _agprqPathParameters = HashMap.fromList [("proxy", "hello")] , _agprqStageVariables = HashMap.fromList [("stageVarName", "stageVarValue")] , _agprqRequestContext =@@ -130,7 +131,7 @@ , _riCognitoIdentityId = Just "" , _riCaller = Just "" , _riApiKey = Just ""- , _riSourceIp = Just "192.168.100.1"+ , _riSourceIp = Just $ IPv4 $ toIPv4 [192, 168, 100, 1] , _riCognitoAuthenticationType = Just "" , _riCognitoAuthenticationProvider = Just "" , _riUserArn = Just ""@@ -141,6 +142,7 @@ , _prcResourcePath = "/{proxy+}" , _prcHttpMethod = "GET" , _prcApiId = "wt6mne2s9k"+ , _prcProtocol = "HTTP/1.1" } , _agprqBody = Nothing }@@ -176,7 +178,7 @@ sampleOKResponse = responseOK & responseBody ?~ "Hello World"- & agprsHeaders .~ HashMap.fromList+ & agprsHeaders .~ [ ("X-Forwarded-Proto", "https") , ("CloudFront-Is-Desktop-Viewer", "true") , ( "Accept"