diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..13a6e46 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,36 @@ +name: CI + +on: push + +jobs: + ci: + runs-on: ${{ matrix.os }} + env: + STACK_YAML: stack-${{ matrix.ghc }}.yaml + strategy: + fail-fast: false + matrix: + ghc: ['8.8', '8.10'] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-haskell@v1 + with: + enable-stack: true + ghc-version: ${{ matrix.ghc }} + stack-version: '2.5.1' + - name: Setup stack + run: | + stack config set install-ghc false + stack config set system-ghc true + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.stack + key: a-${{ matrix.os }}-${{ hashFiles(env.STACK_YAML, format('{0}.lock', env.STACK_YAML)) }} + - name: Install dependencies + run: stack build --test --only-dependencies + - name: Build + run: stack build --fast --test --no-run-tests + - name: Run tests + run: stack build --fast --test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48b31df --- /dev/null +++ b/.gitignore @@ -0,0 +1,207 @@ + +# Created by https://www.gitignore.io/api/macOS,vim,Intellij+iml,emacs,haskell,cabal,stack +# Edit at https://www.gitignore.io/?templates=macOS,vim,Intellij+iml,emacs,haskell,cabal,stack + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + + +### Haskell ### +dist +dist-* +cabal-dev +*.o +*.hi +*.chi +*.chs.h +*.dyn_o +*.dyn_hi +.hpc +.hsenv +.cabal-sandbox/ +cabal.sandbox.config +*.prof +*.aux +*.hp +*.eventlog +.stack-work/ +cabal.project.local +cabal.project.local~ +.HTF/ +.ghc.environment.* + +### Intellij+iml ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Nix +result/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+iml Patch ### +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +#!! ERROR: stack is undefined. Use list command to see defined gitignore types !!# + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +# End of https://www.gitignore.io/api/macOS,vim,Intellij+iml,emacs,haskell,cabal,stack diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e245658 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Change log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog][chg] and this project adheres to +[Haskell's Package Versioning Policy][pvp] + + +## `0.1.0` - 2021-01-19 + +### Added + + - Initial release! + +[chg]: http://keepachangelog.com +[pvp]: http://pvp.haskell.org diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..731b0f9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +Copyright Nike, Inc. (c) 2018, Pact Inc 2021 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index afff93a..110511a 100644 --- a/README.md +++ b/README.md @@ -1 +1,173 @@ -# lambda-client +# lambda-runtime + +An opinionated runtime environment for [Haskell] applications running on [AWS Lambda]. This library is a fork +and rewrite of the [nike/hal] library. It differs from the [nike/hal] library in the following ways: + +- It only exposes a single function that constructs a lambda +- It has a strong emphasis on error catching and handling, exposing the ability to catch `LambdaClientErrors` to user applications for post processing and reporting. +- It aims for small dependence footprint in comparison to lambda-client through getting rid of conduit dependencies +- It doesn't have an internal retry mechanism in case of failed HTTP connections as found in [nike/hal]. + +## Table of Contents + + - [Supported Platforms / GHC Versions](#supported-platforms-ghc-versions) + - [Quick Start](#quick-start) + - [Local Testing](#local-testing) + +## Supported Platforms / GHC Versions + +We currently support this library under the same environment that [AWS Lambda +supports][lambda-env]. + +Our [CI] currently targets the latest two [GHC] versions under [stackage] (e.g. `8.10.x`, `8.8.x`). + +If you haven't already, adding `docker: { enable: true }` to your `stack.yaml` +file will ensure that you're building a binary that can run in +[AWS Lambda][lambda-env]. + +## Quick Start + +This quick start assumes you have the following tools installed: + + - [Stack][stack.yaml] + - [Docker] + - [aws-cli] + +Add `lambda-client` to your [stack.yaml]'s [`extra-deps`] and enable +[Docker] integration so that your binary is automatically compiled in a +compatible environment for AWS. Also add `lambda-client` to your project's +dependency list (either `project-name.cabal` or `package.yaml`) + +```yaml +#... +packages: + - '.' + - lambda-client-0.0.1 +# ... +docker: + enable: true +# ... +``` + +Then, define your types and handler: + +```haskell +module Main where + +import AWS.Lambda.Runtime +import Data.Aeson (FromJSON, parseJSON) +import Data.Aeson.Types (parseMaybe) +import Data.Text (Text) +import GHC.Generics (Generic) + +newtype Named = Named { name :: Text } + deriving anyclass FromJSON + deriving stock Generic + +myHandler :: LambdaResponse -> IO (Either Text Text) +myHandler LambdaResponse{lambdaContext = LambdaContext{..}, ..} = + case parseMaybe parseJSON event of + Nothing -> return $ Left "My name is lambdaClient, what's yours?" + Just (Named name) -> + return . Right $ name <> " from " <> functionName static <> "!" + +main :: IO () +main = runtime myHandler + +``` + +Your binary should be called `bootstrap` in order for the custom runtime +to execute properly: + +```yaml +# Example snippet of package.yaml +# ... +executables: + bootstrap: + source-dirs: src + main: Main.hs # e.g. {project root}/src/Main.hs +# ... +``` + +Don't forget to define your [CloudFormation] stack: + +```yaml +# file: template.yaml +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Description: Test for the Haskell Runtime. +Resources: + HelloWorldApp: + Type: 'AWS::Serverless::Function' + Properties: + Handler: NOT_USED + Runtime: provided + # CodeUri is a relative path from the directory that this CloudFormation + # file is defined. + CodeUri: .stack-work/docker/_home/.local/bin/ + Description: My Haskell runtime. + MemorySize: 128 + Timeout: 3 +``` + +Finally, build, upload and test your lambda! + +```bash +# Build the binary, make sure your executable is named `bootstrap` +stack build --copy-bins + +# Create your function package +aws cloudformation package \ + --template-file template.yaml + --s3-bucket your-existing-bucket > \ + deployment_stack.yaml + +# Deploy your function +aws cloudformation deploy \ + --stack-name "hello-world-haskell" \ + --region us-west-2 \ + --capabilities CAPABILITY_IAM \ + --template-file deployment_stack.yaml + +# Take it for a spin! +aws lambda invoke \ + --function-name your-function-name \ + --region us-west-2 + --payload '{"input": "foo"}' + output.txt +``` + +## Local Testing + +### Dependencies + + - [Stack][stack.yaml] + - [Docker] + - [aws-sam-cli] (>v0.8.0) + +### Build + +```bash +docker pull fpco/stack-build:lts-{version} # First build only, find the latest version in stack.yaml +stack build --copy-bins +``` + +### Execute + +```bash +echo '{ "accountId": "byebye" }' | sam local invoke --region us-east-1 +``` + +[AWS Lambda]: https://docs.aws.amazon.com/lambda/latest/dg/welcome.html +[Haskell]: https://www.haskell.org/ +[stack.yaml]: https://docs.haskellstack.org/ +[`extra-deps`]: https://docs.haskellstack.org/en/stable/yaml_configuration/#yaml-configuration +[Docker]: https://www.docker.com/why-docker +[aws-cli]: https://aws.amazon.com/cli/ +[CloudFormation]: https://aws.amazon.com/cloudformation/ +[aws-sam-cli]: https://github.com/awslabs/aws-sam-cli +[lambda-env]: https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html +[ci]: https://travis-ci.org/Nike-Inc/lambda-client +[stackage]: https://www.stackage.org/ +[GHC]: https://www.haskell.org/ghc/download.html +[nike/hal]: https://github.com/Nike-inc/hal diff --git a/example/Main.hs b/example/Main.hs new file mode 100644 index 0000000..82d4f6d --- /dev/null +++ b/example/Main.hs @@ -0,0 +1,21 @@ +module Main where + +import AWS.Lambda.Runtime +import Data.Aeson (FromJSON, parseJSON) +import Data.Aeson.Types (parseMaybe) +import Data.Text (Text) +import GHC.Generics (Generic) + +newtype Named = Named { name :: Text } + deriving anyclass FromJSON + deriving stock Generic + +myHandler :: LambdaResponse -> IO (Either Text Text) +myHandler LambdaResponse{lambdaContext = LambdaContext{..}, ..} = + case parseMaybe parseJSON event of + Nothing -> return $ Left "My name is lambdaClient, what's yours?" + Just (Named name) -> + return . Right $ name <> " from " <> functionName static <> "!" + +main :: IO () +main = runtime myHandler diff --git a/example/template.yaml b/example/template.yaml new file mode 100644 index 0000000..bb8313b --- /dev/null +++ b/example/template.yaml @@ -0,0 +1,13 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Description: Test for the Haskell Runtime. +Resources: + helloworld: + Type: 'AWS::Serverless::Function' + Properties: + Handler: NOT_USED + Runtime: provided + CodeUri: .stack-work/docker/_home/.local/bin/ + Description: Test for the Haskell Runtime. + MemorySize: 128 + Timeout: 3 diff --git a/lambda-client.cabal b/lambda-client.cabal new file mode 100644 index 0000000..2c34e4b --- /dev/null +++ b/lambda-client.cabal @@ -0,0 +1,112 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.33.0. +-- +-- see: https://github.com/sol/hpack +-- +-- hash: 439029ad0e4b6f079c73faa4a09da5efd75851ae6b0d11380aaf86a5857cb93f + +name: lambda-client +version: 0.0.1 +synopsis: A runtime environment for Haskell applications running on AWS Lambda. +description: A runtime environment for [Haskell] applications running on [AWS Lambda]. This library is a fork + and rewrite of the [nike/hal] library. It differs from the [nike/hal] library in the following ways: + . + - It only exposes a single function that constructs a lambda + - It has a strong emphasis on error catching and handling, exposing lambda client errors to user applications + for post processing and reporting. + - It aims for small dependence footprint in comparison to lambda-client by getting rid of conduit dependencies + - It doesn't have an internal retry mechanism in case of failed HTTP connections as found in [nike/hal] +category: Web,AWS +homepage: https://github.com/pact/lambda-runtime#readme +bug-reports: https://github.com/pact/lambda-runtime/issues +author: Nike, Inc, Pact Inc +maintainer: epicallan.al@gmail.com, markus@pact.tax +copyright: 2018 Nike, Inc, 2021 Pact Inc +license: BSD3 +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + +source-repository head + type: git + location: https://github.com/pact/lambda-runtime + +flag development + description: Run GHC with development flags + manual: True + default: False + +library + exposed-modules: + AWS.Lambda.Client + AWS.Lambda.Context + AWS.Lambda.Runtime + AWS.Prelude + other-modules: + Paths_lambda_client + hs-source-dirs: + src + default-extensions: OverloadedStrings DeriveFoldable DeriveFunctor DeriveAnyClass DeriveGeneric DerivingStrategies DeriveTraversable EmptyCase FlexibleContexts GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses RecordWildCards ScopedTypeVariables TypeApplications ViewPatterns + ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fno-warn-partial-type-signatures -fno-warn-name-shadowing -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-incomplete-patterns + build-depends: + aeson + , base >=4.7 && <5 + , bytestring + , containers + , conversions + , envy <2 + , exceptions + , http-client + , http-conduit + , http-types + , mtl + , text + , time + if flag(development) + ghc-options: -Werror -fplugin=SourceConstraints + build-depends: + source-constraints >=0.0.2 && <0.1 + else + ghc-options: -Wwarn + default-language: Haskell2010 + +executable example + main-is: Main.hs + other-modules: + Paths_lambda_client + hs-source-dirs: + example + default-extensions: OverloadedStrings DeriveFoldable DeriveFunctor DeriveAnyClass DeriveGeneric DerivingStrategies DeriveTraversable EmptyCase FlexibleContexts GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses RecordWildCards ScopedTypeVariables TypeApplications ViewPatterns + ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fno-warn-partial-type-signatures -fno-warn-name-shadowing -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-incomplete-patterns + build-depends: + aeson + , base >=4.7 && <5 + , lambda-client + , text + if flag(development) + ghc-options: -Werror -fplugin=SourceConstraints + build-depends: + source-constraints >=0.0.2 && <0.1 + else + ghc-options: -Wwarn + default-language: Haskell2010 + +test-suite test + type: exitcode-stdio-1.0 + main-is: test/Test.hs + other-modules: + Paths_lambda_client + default-extensions: OverloadedStrings DeriveFoldable DeriveFunctor DeriveAnyClass DeriveGeneric DerivingStrategies DeriveTraversable EmptyCase FlexibleContexts GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses RecordWildCards ScopedTypeVariables TypeApplications ViewPatterns + ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fno-warn-partial-type-signatures -fno-warn-name-shadowing -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-incomplete-patterns -rtsopts -threaded -with-rtsopts=-N + build-depends: + base >=4.7 && <5 + , devtools >=0.1.0 && <0.2 + if flag(development) + ghc-options: -Werror -fplugin=SourceConstraints + build-depends: + source-constraints >=0.0.2 && <0.1 + else + ghc-options: -Wwarn + default-language: Haskell2010 diff --git a/package.yaml b/package.yaml new file mode 100644 index 0000000..3fb9001 --- /dev/null +++ b/package.yaml @@ -0,0 +1,107 @@ +name: lambda-client +synopsis: A runtime environment for Haskell applications running on AWS Lambda. +description: | + A runtime environment for [Haskell] applications running on [AWS Lambda]. This library is a fork + and rewrite of the [nike/hal] library. It differs from the [nike/hal] library in the following ways: + + - It only exposes a single function that constructs a lambda + - It has a strong emphasis on error catching and handling, exposing lambda client errors to user applications + for post processing and reporting. + - It aims for small dependence footprint in comparison to lambda-client by getting rid of conduit dependencies + - It doesn't have an internal retry mechanism in case of failed HTTP connections as found in [nike/hal] +version: 0.0.1 +github: pact/lambda-runtime +license: BSD3 +author: Nike, Inc, Pact Inc +maintainer: epicallan.al@gmail.com, markus@pact.tax +copyright: 2018 Nike, Inc, 2021 Pact Inc +category: Web,AWS +extra-source-files: +- README.md + +dependencies: + - base >= 4.7 && < 5 + +default-extensions: + - OverloadedStrings + - DeriveFoldable + - DeriveFunctor + - DeriveAnyClass + - DeriveGeneric + - DerivingStrategies + - DeriveTraversable + - EmptyCase + - FlexibleContexts + - GeneralizedNewtypeDeriving + - LambdaCase + - MultiParamTypeClasses + - RecordWildCards + - ScopedTypeVariables + - TypeApplications + - ViewPatterns + +ghc-options: + - -Wall + - -Wcompat + - -Wincomplete-record-updates + - -Wincomplete-uni-patterns + - -Wredundant-constraints + - -fno-warn-partial-type-signatures + - -fno-warn-name-shadowing + - -fwarn-tabs + - -fwarn-unused-imports + - -fwarn-missing-signatures + - -fwarn-incomplete-patterns + +flags: + development: + description: Run GHC with development flags + default: false + manual: true + +when: + - condition: flag(development) + then: + ghc-options: + - -Werror + - -fplugin=SourceConstraints + dependencies: + - source-constraints ^>= 0.0.2 + else: + ghc-options: -Wwarn + +library: + source-dirs: src + dependencies: + - aeson + - bytestring + - conversions + - envy < 2 + - http-client + - http-conduit + - http-types + - exceptions + - mtl + - containers + - time + - text + +tests: + test: + main: test/Test.hs + ghc-options: + - -rtsopts + - -threaded + - -with-rtsopts=-N + dependencies: + - devtools ^>= 0.1.0 + +executables: + example: + source-dirs: example + main: Main.hs + dependencies: + - base >= 4.7 && < 5 + - text + - aeson + - lambda-client diff --git a/src/AWS/Lambda/Client.hs b/src/AWS/Lambda/Client.hs new file mode 100644 index 0000000..077d95c --- /dev/null +++ b/src/AWS/Lambda/Client.hs @@ -0,0 +1,225 @@ +module AWS.Lambda.Client + ( HTTPConfig + , LambdaClient + , LambdaClientError (..) + , LambdaResponse (..) + , getHttpConfig + , getNextLambdaResponse + , sendEventError + , sendEventSuccess + , sendInitError + ) +where + +import AWS.Lambda.Context +import AWS.Prelude +import Control.Monad ((<=<)) +import Control.Monad.Except (ExceptT(..), liftEither, throwError, withExceptT) +import Data.Time.Clock.POSIX (posixSecondsToUTCTime) +import System.Environment (lookupEnv) +import System.Envy (decodeEnv) + +import qualified Data.Aeson as JSON +import qualified Data.ByteString as BS +import qualified Network.HTTP.Client as HTTP +import qualified Network.HTTP.Types as HTTP + +data HTTPConfig = HTTPConfig + { request :: HTTP.Request + , manager :: HTTP.Manager + } + +data LambdaResponse = LambdaResponse + { event :: JSON.Value + , lambdaContext :: LambdaContext + } + deriving stock Show + +data LambdaClientError + = LambdaContextDecodeError Text + | MissingLambdaRunTimeApi + | InvalidLambdaRunTimeApi Text + | InvalidStaticContext Text + | ConnectionError HTTP.HttpException + | ResponseBodyDecodeFailure Text + | UnExpectedRunTimeError HTTP.Status Text + | PayLoadTooLarge Text + deriving stock Show + +instance Exception LambdaClientError + +type LambdaClient = ExceptT LambdaClientError IO + +getHttpConfig :: LambdaClient HTTPConfig +getHttpConfig = do + awsLambdaRuntimeApi <- ExceptT $ maybeToEither MissingLambdaRunTimeApi + <$> lookupEnv "AWS_LAMBDA_RUNTIME_API" + + req <- withExceptT (InvalidLambdaRunTimeApi . showc @Text) + . ExceptT + . try @_ @HTTP.HttpException + $ HTTP.parseRequest ("http://" <> awsLambdaRuntimeApi) + + man <- liftIO $ HTTP.newManager + -- In the off chance that they set a proxy value, we don't want to + -- use it. There's also no reason to spend time reading env vars. + $ HTTP.managerSetProxy HTTP.noProxy + $ HTTP.defaultManagerSettings + -- This is the most important setting, we must not timeout requests + { HTTP.managerResponseTimeout = HTTP.responseTimeoutNone + -- We only ever need a single connection, because we'll never make + -- concurrent requests and never talk to more than one host. + , HTTP.managerConnCount = 1 + , HTTP.managerIdleConnectionCount = 1 + } + return $ HTTPConfig req man + +getNextLambdaResponse :: HTTPConfig -> LambdaClient LambdaResponse +getNextLambdaResponse httpConfig = do + nextRes <- getNextEvent httpConfig + + staticContext <- ExceptT $ pure . first (InvalidStaticContext . convert) + =<< decodeEnv @StaticContext + + let mGetHeader headerName errorMsg = liftEither $ getHeader nextRes headerName errorMsg + + awsRequestId <- RequestId <$> mGetHeader "Lambda-Runtime-Aws-HTTP.Request-Id" "Missing request Id" + xRayTraceId <- mGetHeader "Lambda-Runtime-Trace-Id" "Missing trace Id" + invokedFunctionArn <- mGetHeader "Lambda-Runtime-Invoked-Function-Arn" "Missing runtime Function" + deadline <- liftEither . toMillSeconds + =<< mGetHeader "Lambda-Runtime-Deadline-Ms" "Missing lambda deadline" + + let clientContext = decodeOptional @ClientContext nextRes "Lambda-Runtime-Client-Context" + let identity = decodeOptional @CognitoIdentity nextRes "Lambda-Runtime-Cognito-Identity" + + let dynamicContext = DynamicContext {..} + + return LambdaResponse + { event = HTTP.responseBody nextRes + , lambdaContext = LambdaContext staticContext dynamicContext + } + where + toMillSeconds :: Text -> Either LambdaClientError UTCTime + toMillSeconds ms = maybeToEither + (LambdaContextDecodeError "Failed to decode lambdaRuntime milliseconds as utcTime") $ do + milliseconds <- readMaybe @Double $ convert ms + return $ posixSecondsToUTCTime $ realToFrac $ milliseconds / 1000 + + readMaybe :: Read a => String -> Maybe a + readMaybe s = case reads s of + [(x,"")] -> Just x + _ -> Nothing + + getHeader + :: HTTP.Response JSON.Value + -> HTTP.HeaderName + -> Text + -> Either LambdaClientError Text + getHeader response header errorMsg + = maybeToEither (LambdaContextDecodeError errorMsg) + $ getResponseHeader header response + + decodeOptional + :: forall a . JSON.FromJSON a + => HTTP.Response JSON.Value + -> HTTP.HeaderName + -> Maybe a + decodeOptional response = JSON.decodeStrict <=< flip getRawResponseHeader response + +getNextEvent :: HTTPConfig -> LambdaClient (HTTP.Response JSON.Value) +getNextEvent HTTPConfig{..} = do + response <- performHttpRequest $ HTTPConfig { request = toNextEventRequest request, .. } + + let statusCode = HTTP.responseStatus response + + unless (HTTP.statusIsSuccessful statusCode) . throwError + $ UnExpectedRunTimeError statusCode "Could not retrieve next event" + + return response + where + toNextEventRequest :: HTTP.Request -> HTTP.Request + toNextEventRequest req = req + { HTTP.path = "2018-06-01/runtime/invocation/next" + } + +sendEventSuccess + :: (MonadCatch m, MonadIO m) + => HTTPConfig + -> RequestId + -> JSON.Value + -> m () +sendEventSuccess HTTPConfig{..} requestId json = do + response <- catchConnectionError + . flip HTTP.httpNoBody manager + $ toEventSuccessRequest request + + let statusCode = HTTP.responseStatus response + + when (statusCode == HTTP.status413) . throwM . PayLoadTooLarge $ showc json + + unless (HTTP.statusIsSuccessful statusCode) . + throwM $ UnExpectedRunTimeError statusCode "Could not post handler result" + where + toEventSuccessRequest :: HTTP.Request -> HTTP.Request + toEventSuccessRequest req = req + { HTTP.requestBody = HTTP.RequestBodyLBS (JSON.encode json) + , HTTP.method = "POST" + , HTTP.path = "2018-06-01/runtime/invocation/" <> convert requestId <> "/response" + } + +sendEventError :: MonadIO m => HTTPConfig -> RequestId -> Text -> m () +sendEventError HTTPConfig{..} requestId error + = void + . liftIO + . flip HTTP.httpNoBody manager + $ toEventErrorRequest request + where + toEventErrorRequest :: HTTP.Request -> HTTP.Request + toEventErrorRequest req = (toBaseErrorRequest error req) + { HTTP.path = "2018-06-01/runtime/invocation/" <> convert requestId <> "/error" + } + +sendInitError :: MonadIO m => HTTPConfig -> Text -> m () +sendInitError HTTPConfig{..} error + = void + . liftIO + . flip HTTP.httpNoBody manager + $ toInitErrorRequest request + where + toInitErrorRequest :: HTTP.Request -> HTTP.Request + toInitErrorRequest req = (toBaseErrorRequest error req) + { HTTP.path = "2018-06-01/runtime/init/error" + } + +performHttpRequest :: HTTPConfig -> LambdaClient (HTTP.Response JSON.Value) +performHttpRequest HTTPConfig{..} = do + response <- catchConnectionError $ HTTP.httpLbs request manager + + body <- liftEither + . first (ResponseBodyDecodeFailure . convert) + . JSON.eitherDecode + $ HTTP.responseBody response + + pure $ fmap (const body) response + +catchConnectionError :: (MonadCatch m, MonadIO m) => IO a -> m a +catchConnectionError action = + catch (liftIO action) + $ \e -> throwM . ConnectionError $ (e :: HTTP.HttpException) + +toBaseErrorRequest :: Text -> HTTP.Request -> HTTP.Request +toBaseErrorRequest error req = req + { HTTP.requestBody = HTTP.RequestBodyLBS (JSON.encode error) + , HTTP.method = "POST" + , HTTP.requestHeaders = [("Content-Type", "application/vnd.aws.lambda.error+json") ] + } + +getResponseHeader :: HTTP.HeaderName -> HTTP.Response a -> Maybe Text +getResponseHeader name = fmap decodeUtf8 . getRawResponseHeader name + +getRawResponseHeader :: HTTP.HeaderName -> HTTP.Response a -> Maybe BS.ByteString +getRawResponseHeader name + = safeHead + . map snd + . filter ((name ==) . fst) + . HTTP.responseHeaders diff --git a/src/AWS/Lambda/Context.hs b/src/AWS/Lambda/Context.hs new file mode 100644 index 0000000..7d31df8 --- /dev/null +++ b/src/AWS/Lambda/Context.hs @@ -0,0 +1,86 @@ +{-# LANGUAGE DerivingVia #-} +module AWS.Lambda.Context where + +import AWS.Prelude +import Data.Aeson (FromJSON, ToJSON) +import Data.Word (Word16) + +import qualified Data.ByteString as BS +import qualified Data.Time as Time +import qualified Data.Time.Clock.POSIX as Time +import qualified System.Envy as Envy + +data ClientApplication = ClientApplication + { appTitle :: Text + , appVersionName :: Text + , appVersionCode :: Text + , appPackageName :: Text + } + deriving anyclass (ToJSON, FromJSON) + deriving stock (Show, Generic) + +data ClientContext = ClientContext + { client :: ClientApplication + , custom :: Map Text Text + , environment :: Map Text Text + } + deriving anyclass (ToJSON, FromJSON) + deriving stock (Show, Generic) + +data CognitoIdentity = CognitoIdentity + { identityId :: Text + , identityPoolId :: Text + } + deriving anyclass (ToJSON, FromJSON) + deriving stock (Show, Generic) + +data StaticContext = StaticContext + { functionName :: Text + , functionVersion :: Text + , functionMemorySize :: Word16 + , logGroupName :: Text + , logStreamName :: Text + } + deriving anyclass (ToJSON, FromJSON) + deriving stock (Show, Generic) + +instance Envy.DefConfig StaticContext where + defConfig = StaticContext mempty mempty 0 mempty mempty + +instance Envy.FromEnv StaticContext where + fromEnv = Envy.gFromEnvCustom Envy.Option + { dropPrefixCount = 0 + , customPrefix = "AWS_LAMBDA" + } + +newtype RequestId = RequestId Text + deriving (Semigroup, Monoid, ToJSON, FromJSON) via Text + deriving stock Show + +instance Conversion BS.ByteString RequestId where + convert (RequestId requestId) = encodeUtf8 requestId + +data DynamicContext = DynamicContext + { awsRequestId :: RequestId + , invokedFunctionArn :: Text + , xRayTraceId :: Text + , deadline :: Time.UTCTime + , clientContext :: Maybe ClientContext + , identity :: Maybe CognitoIdentity + } + deriving anyclass (ToJSON, FromJSON) + deriving stock (Show, Generic) + + +instance Envy.DefConfig DynamicContext where + defConfig = DynamicContext mempty mempty mempty (Time.posixSecondsToUTCTime 0) empty empty + +data LambdaContext = LambdaContext + { static :: StaticContext + , dynamic :: DynamicContext + } + deriving anyclass (ToJSON, FromJSON) + deriving stock (Show, Generic) + +instance Envy.DefConfig LambdaContext where + defConfig = LambdaContext Envy.defConfig Envy.defConfig diff --git a/src/AWS/Lambda/Runtime.hs b/src/AWS/Lambda/Runtime.hs new file mode 100644 index 0000000..e9f3316 --- /dev/null +++ b/src/AWS/Lambda/Runtime.hs @@ -0,0 +1,51 @@ +module AWS.Lambda.Runtime + ( module Exports + , LambdaClientError (..) + , LambdaResponse (..) + , runtime + ) +where + +import AWS.Lambda.Client +import AWS.Lambda.Context as Exports +import AWS.Prelude +import Control.Monad.Except (runExceptT) +import Data.Aeson (ToJSON(..)) + +type LambdaFunction m result = LambdaResponse -> m (Either Text result) + +runtime + :: forall m result . (MonadCatch m, MonadIO m, ToJSON result) + => LambdaFunction m result + -> m () +runtime = forever . runtimeLoop + +runtimeLoop + :: forall result m . (ToJSON result, MonadIO m, MonadCatch m) + => LambdaFunction m result + -> m () +runtimeLoop fn = + either throwM runFunction =<< liftIO (runExceptT getHttpConfig) + where + runFunction :: HTTPConfig -> m () + runFunction httpConfig = do + lambdaResponse <- processNextLambdaAction httpConfig + $ getNextLambdaResponse httpConfig + result <- fn lambdaResponse + + let requestId = getRequestId lambdaResponse + + either + (sendEventError httpConfig requestId) + (sendEventSuccess httpConfig requestId . toJSON) result + + processNextLambdaAction :: HTTPConfig -> LambdaClient a -> m a + processNextLambdaAction httpConfig action = + liftIO (runExceptT action) >>= \case + Right result -> pure result + Left error -> do + sendInitError httpConfig $ showc error + throwM error + + getRequestId :: LambdaResponse -> RequestId + getRequestId = awsRequestId . dynamic . lambdaContext diff --git a/src/AWS/Prelude.hs b/src/AWS/Prelude.hs new file mode 100644 index 0000000..f5efc05 --- /dev/null +++ b/src/AWS/Prelude.hs @@ -0,0 +1,32 @@ +module AWS.Prelude + ( module Exports + , maybeToEither + , safeHead + , showc + ) where + +import Control.Applicative as Exports (empty, pure) +import Control.Exception as Exports (Exception, SomeException) +import Control.Monad as Exports (forever, unless, void, when) +import Control.Monad.Catch as Exports (MonadCatch, catch, throwM, try) +import Control.Monad.IO.Class as Exports (MonadIO, liftIO) +import Data.Bifunctor as Exports +import Data.Conversions as Exports +import Data.Map as Exports (Map) +import Data.Text as Exports (Text) +import Data.Text.Encoding as Exports (decodeUtf8, encodeUtf8) +import Data.Time as Exports (UTCTime) +import GHC.Generics as Exports (Generic) + +showc :: forall b a . (Show a, Conversion b String) => a -> b +showc = convert . show + +safeHead :: [a] -> Maybe a +safeHead = \case + (x:_) -> pure x + _ -> empty + +maybeToEither :: b -> Maybe a -> Either b a +maybeToEither b ma = case ma of + Nothing -> Left b + Just a -> Right a diff --git a/stack-8.10.yaml b/stack-8.10.yaml new file mode 100644 index 0000000..43391f4 --- /dev/null +++ b/stack-8.10.yaml @@ -0,0 +1,82 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# https://docs.haskellstack.org/en/stable/yaml_configuration/ + +# Resolver to choose a 'specific' stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# resolver: lts-3.5 +# resolver: nightly-2015-09-21 +# resolver: ghc-7.10.2 +# resolver: ghcjs-0.1.0_ghc-7.10.2 +# +# The location of a snapshot can be provided as a file or url. Stack assumes +# a snapshot provided as a file might change, whereas a url resource does not. +# +# resolver: ./custom-snapshot.yaml +# resolver: https://example.com/snapshots/2018-01-01.yaml +resolver: nightly-2020-11-19 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# - location: +# git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# subdirs: +# - auto-update +# - wai +packages: +- . +# Dependency packages to be pulled from upstream that are not in the resolver +# using the same syntax as the packages field. +# (e.g., acme-missiles-0.3) +# Override default flag values for local packages and extra-deps +# flags: {} + +# Extra package databases containing global packages +# extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=1.7" +# +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor + +# docker: +# enable: true + +# https://docs.haskellstack.org/en/stable/yaml_configuration/#pvp-bounds +pvp-bounds: lower-revision + +extra-deps: +- envy-1.5.1.0@sha256:a00910ebf461ec36ff6b7b01711bffe09cee062b7515c4580c466002590283a2,1683 +- conversions-0.0.4@sha256:2e795093fe1318d5b37d52283ca3709573d28f4be7978c51284c27b645d3548d,3406 +- source-constraints-0.0.2@sha256:5746921c48cde021cd9120fc917dd21768fa6f3716f9d09fc847cac3c6f5d4e4,3103 +- devtools-0.1.0@sha256:c5898fe567d610492cc140495f0828c55e64e7c1b28dc2b1b9a84cc5f6dc7c39,3148 +- mprelude-0.2.1@sha256:c7cfd52b86c4a5469cffd84e84e96157c99ef05173b14cf309498b291c867a8f,2968 +- tasty-1.3.1@sha256:01e35c97f7ee5ccbc28f21debea02a38cd010d53b4c3087f5677c5d06617a507,2520 +- tasty-mgolden-0.0.1@sha256:07fadb592cde353c386c5f38cff75539a5675e9350776ed6f4f0a70247427a78,4214 +- unliftio-core-0.2.0.1@sha256:9b3e44ea9aacacbfc35b3b54015af450091916ac3618a41868ebf6546977659a,1082 +flags: + lambda-client: + development: true diff --git a/stack-8.10.yaml.lock b/stack-8.10.yaml.lock new file mode 100644 index 0000000..9d08948 --- /dev/null +++ b/stack-8.10.yaml.lock @@ -0,0 +1,68 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/lock_files + +packages: +- completed: + hackage: envy-1.5.1.0@sha256:a00910ebf461ec36ff6b7b01711bffe09cee062b7515c4580c466002590283a2,1683 + pantry-tree: + size: 368 + sha256: 85ed121176a3b9cff9221af50077c45181ab2fe0e4ffe5273aaf57daef197b0a + original: + hackage: envy-1.5.1.0@sha256:a00910ebf461ec36ff6b7b01711bffe09cee062b7515c4580c466002590283a2,1683 +- completed: + hackage: conversions-0.0.4@sha256:2e795093fe1318d5b37d52283ca3709573d28f4be7978c51284c27b645d3548d,3406 + pantry-tree: + size: 250 + sha256: dcd44c2b005235991df8b9f5fce5931f79599c05d59da2caf943c6aa41f09088 + original: + hackage: conversions-0.0.4@sha256:2e795093fe1318d5b37d52283ca3709573d28f4be7978c51284c27b645d3548d,3406 +- completed: + hackage: source-constraints-0.0.2@sha256:5746921c48cde021cd9120fc917dd21768fa6f3716f9d09fc847cac3c6f5d4e4,3103 + pantry-tree: + size: 768 + sha256: 028d0fa64e36ef0df8629d4d36339458351d831a43d8d2dd8009600df1203377 + original: + hackage: source-constraints-0.0.2@sha256:5746921c48cde021cd9120fc917dd21768fa6f3716f9d09fc847cac3c6f5d4e4,3103 +- completed: + hackage: devtools-0.1.0@sha256:c5898fe567d610492cc140495f0828c55e64e7c1b28dc2b1b9a84cc5f6dc7c39,3148 + pantry-tree: + size: 516 + sha256: 2de35331c693369cd31b739e4d619ddd487653f6107d36f7c025c15a4e117ff9 + original: + hackage: devtools-0.1.0@sha256:c5898fe567d610492cc140495f0828c55e64e7c1b28dc2b1b9a84cc5f6dc7c39,3148 +- completed: + hackage: mprelude-0.2.1@sha256:c7cfd52b86c4a5469cffd84e84e96157c99ef05173b14cf309498b291c867a8f,2968 + pantry-tree: + size: 265 + sha256: 391c3c95dc57ce7c59698a4c1896906b384d7001683dc7888e4714a5fa7e85da + original: + hackage: mprelude-0.2.1@sha256:c7cfd52b86c4a5469cffd84e84e96157c99ef05173b14cf309498b291c867a8f,2968 +- completed: + hackage: tasty-1.3.1@sha256:01e35c97f7ee5ccbc28f21debea02a38cd010d53b4c3087f5677c5d06617a507,2520 + pantry-tree: + size: 1804 + sha256: 6150ec5094fe321150b1263bc751c1a85b22be766053fb4648115d8c02b2064b + original: + hackage: tasty-1.3.1@sha256:01e35c97f7ee5ccbc28f21debea02a38cd010d53b4c3087f5677c5d06617a507,2520 +- completed: + hackage: tasty-mgolden-0.0.1@sha256:07fadb592cde353c386c5f38cff75539a5675e9350776ed6f4f0a70247427a78,4214 + pantry-tree: + size: 338 + sha256: 9cd24c3a02396dd93ed7d57df1d9668a2377d128e413db5ff89d82ffdfc830fa + original: + hackage: tasty-mgolden-0.0.1@sha256:07fadb592cde353c386c5f38cff75539a5675e9350776ed6f4f0a70247427a78,4214 +- completed: + hackage: unliftio-core-0.2.0.1@sha256:9b3e44ea9aacacbfc35b3b54015af450091916ac3618a41868ebf6546977659a,1082 + pantry-tree: + size: 328 + sha256: e81c5a1e82ec2cd68cbbbec9cd60567363abe02257fa1370a906f6754b6818b8 + original: + hackage: unliftio-core-0.2.0.1@sha256:9b3e44ea9aacacbfc35b3b54015af450091916ac3618a41868ebf6546977659a,1082 +snapshots: +- completed: + size: 553330 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2020/11/19.yaml + sha256: 7ae41d7d9026078cbc28fa535936498eaf604f93b04f9c9c61224312c7f47758 + original: nightly-2020-11-19 diff --git a/stack-8.8.yaml b/stack-8.8.yaml new file mode 100644 index 0000000..16736c1 --- /dev/null +++ b/stack-8.8.yaml @@ -0,0 +1,82 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# https://docs.haskellstack.org/en/stable/yaml_configuration/ + +# Resolver to choose a 'specific' stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# resolver: lts-3.5 +# resolver: nightly-2015-09-21 +# resolver: ghc-7.10.2 +# resolver: ghcjs-0.1.0_ghc-7.10.2 +# +# The location of a snapshot can be provided as a file or url. Stack assumes +# a snapshot provided as a file might change, whereas a url resource does not. +# +# resolver: ./custom-snapshot.yaml +# resolver: https://example.com/snapshots/2018-01-01.yaml +resolver: lts-16.22 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# - location: +# git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# subdirs: +# - auto-update +# - wai +packages: +- . +# Dependency packages to be pulled from upstream that are not in the resolver +# using the same syntax as the packages field. +# (e.g., acme-missiles-0.3) +# Override default flag values for local packages and extra-deps +# flags: {} + +# Extra package databases containing global packages +# extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=1.7" +# +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor + +# docker: +# enable: true + +# https://docs.haskellstack.org/en/stable/yaml_configuration/#pvp-bounds +pvp-bounds: lower-revision + +extra-deps: +- envy-1.5.1.0@sha256:a00910ebf461ec36ff6b7b01711bffe09cee062b7515c4580c466002590283a2,1683 +- conversions-0.0.4@sha256:2e795093fe1318d5b37d52283ca3709573d28f4be7978c51284c27b645d3548d,3406 +- devtools-0.1.0@sha256:c5898fe567d610492cc140495f0828c55e64e7c1b28dc2b1b9a84cc5f6dc7c39,3148 +- mprelude-0.2.1@sha256:c7cfd52b86c4a5469cffd84e84e96157c99ef05173b14cf309498b291c867a8f,2968 +- source-constraints-0.0.2@sha256:5746921c48cde021cd9120fc917dd21768fa6f3716f9d09fc847cac3c6f5d4e4,3103 +- tasty-1.3.1@sha256:01e35c97f7ee5ccbc28f21debea02a38cd010d53b4c3087f5677c5d06617a507,2520 +- tasty-mgolden-0.0.1@sha256:07fadb592cde353c386c5f38cff75539a5675e9350776ed6f4f0a70247427a78,4214 +- unliftio-core-0.2.0.1@sha256:9b3e44ea9aacacbfc35b3b54015af450091916ac3618a41868ebf6546977659a,1082 +flags: + lambda-client: + development: true diff --git a/stack-8.8.yaml.lock b/stack-8.8.yaml.lock new file mode 100644 index 0000000..be02f61 --- /dev/null +++ b/stack-8.8.yaml.lock @@ -0,0 +1,68 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/lock_files + +packages: +- completed: + hackage: envy-1.5.1.0@sha256:a00910ebf461ec36ff6b7b01711bffe09cee062b7515c4580c466002590283a2,1683 + pantry-tree: + size: 368 + sha256: 85ed121176a3b9cff9221af50077c45181ab2fe0e4ffe5273aaf57daef197b0a + original: + hackage: envy-1.5.1.0@sha256:a00910ebf461ec36ff6b7b01711bffe09cee062b7515c4580c466002590283a2,1683 +- completed: + hackage: conversions-0.0.4@sha256:2e795093fe1318d5b37d52283ca3709573d28f4be7978c51284c27b645d3548d,3406 + pantry-tree: + size: 250 + sha256: dcd44c2b005235991df8b9f5fce5931f79599c05d59da2caf943c6aa41f09088 + original: + hackage: conversions-0.0.4@sha256:2e795093fe1318d5b37d52283ca3709573d28f4be7978c51284c27b645d3548d,3406 +- completed: + hackage: devtools-0.1.0@sha256:c5898fe567d610492cc140495f0828c55e64e7c1b28dc2b1b9a84cc5f6dc7c39,3148 + pantry-tree: + size: 516 + sha256: 2de35331c693369cd31b739e4d619ddd487653f6107d36f7c025c15a4e117ff9 + original: + hackage: devtools-0.1.0@sha256:c5898fe567d610492cc140495f0828c55e64e7c1b28dc2b1b9a84cc5f6dc7c39,3148 +- completed: + hackage: mprelude-0.2.1@sha256:c7cfd52b86c4a5469cffd84e84e96157c99ef05173b14cf309498b291c867a8f,2968 + pantry-tree: + size: 265 + sha256: 391c3c95dc57ce7c59698a4c1896906b384d7001683dc7888e4714a5fa7e85da + original: + hackage: mprelude-0.2.1@sha256:c7cfd52b86c4a5469cffd84e84e96157c99ef05173b14cf309498b291c867a8f,2968 +- completed: + hackage: source-constraints-0.0.2@sha256:5746921c48cde021cd9120fc917dd21768fa6f3716f9d09fc847cac3c6f5d4e4,3103 + pantry-tree: + size: 768 + sha256: 028d0fa64e36ef0df8629d4d36339458351d831a43d8d2dd8009600df1203377 + original: + hackage: source-constraints-0.0.2@sha256:5746921c48cde021cd9120fc917dd21768fa6f3716f9d09fc847cac3c6f5d4e4,3103 +- completed: + hackage: tasty-1.3.1@sha256:01e35c97f7ee5ccbc28f21debea02a38cd010d53b4c3087f5677c5d06617a507,2520 + pantry-tree: + size: 1804 + sha256: 6150ec5094fe321150b1263bc751c1a85b22be766053fb4648115d8c02b2064b + original: + hackage: tasty-1.3.1@sha256:01e35c97f7ee5ccbc28f21debea02a38cd010d53b4c3087f5677c5d06617a507,2520 +- completed: + hackage: tasty-mgolden-0.0.1@sha256:07fadb592cde353c386c5f38cff75539a5675e9350776ed6f4f0a70247427a78,4214 + pantry-tree: + size: 338 + sha256: 9cd24c3a02396dd93ed7d57df1d9668a2377d128e413db5ff89d82ffdfc830fa + original: + hackage: tasty-mgolden-0.0.1@sha256:07fadb592cde353c386c5f38cff75539a5675e9350776ed6f4f0a70247427a78,4214 +- completed: + hackage: unliftio-core-0.2.0.1@sha256:9b3e44ea9aacacbfc35b3b54015af450091916ac3618a41868ebf6546977659a,1082 + pantry-tree: + size: 328 + sha256: e81c5a1e82ec2cd68cbbbec9cd60567363abe02257fa1370a906f6754b6818b8 + original: + hackage: unliftio-core-0.2.0.1@sha256:9b3e44ea9aacacbfc35b3b54015af450091916ac3618a41868ebf6546977659a,1082 +snapshots: +- completed: + size: 532414 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/22.yaml + sha256: e483fb88549fc0f454c190979bf35ac91c7aceff2c0e71e7d8edd11842d772d8 + original: lts-16.22 diff --git a/test/Test.hs b/test/Test.hs new file mode 100644 index 0000000..e0aaf64 --- /dev/null +++ b/test/Test.hs @@ -0,0 +1,9 @@ +module Main + ( main + ) +where + +import qualified Devtools + +main :: IO () +main = Devtools.main Devtools.defaultConfig diff --git a/test/stack-8.10-dependencies.txt b/test/stack-8.10-dependencies.txt new file mode 100644 index 0000000..e8c10fe --- /dev/null +++ b/test/stack-8.10-dependencies.txt @@ -0,0 +1,134 @@ +Cabal 3.2.0.0 +Diff 0.4.0 +aeson 1.5.4.1 +ansi-terminal 0.10.3 +ansi-wl-pprint 0.6.9 +array 0.5.4.0 +asn1-encoding 0.9.6 +asn1-parse 0.9.5 +asn1-types 0.3.4 +assoc 1.0.2 +async 2.2.2 +attoparsec 0.13.2.4 +base 4.14.1.0 +base-compat 0.11.2 +base-compat-batteries 0.11.2 +base-orphans 0.8.3 +basement 0.0.11 +bifunctors 5.5.8 +binary 0.8.8.0 +blaze-builder 0.4.1.0 +bytestring 0.10.10.0 +cabal-doctest 1.0.8 +call-stack 0.2.0 +case-insensitive 1.2.1.0 +cereal 0.5.8.1 +clock 0.8 +cmdargs 0.10.20 +colour 2.3.5 +comonad 5.0.6 +conduit 1.3.3 +conduit-extra 1.3.5 +connection 0.3.1 +containers 0.6.2.1 +control-bool 0.2.1 +conversions 0.0.4 +cookie 0.4.5 +cpphs 1.20.9.1 +cryptonite 0.27 +data-default 0.7.1.1 +data-default-class 0.1.2.0 +data-default-instances-containers 0.0.1 +data-default-instances-dlist 0.0.1 +data-default-instances-old-locale 0.0.1 +data-fix 0.3.0 +deepseq 1.4.4.0 +devtools 0.1.0 +directory 1.3.6.0 +distributive 0.6.2 +dlist 0.8.0.8 +envy 1.5.1.0 +exceptions 0.10.4 +extra 1.7.8 +file-embed 0.0.13.0 +filepath 1.4.2.1 +filepattern 0.1.2 +ghc 8.10.2 +ghc-boot 8.10.2 +ghc-boot-th 8.10.2 +ghc-heap 8.10.2 +ghc-lib-parser-ex 8.10.0.16 +ghc-prim 0.6.1 +ghci 8.10.2 +hashable 1.3.0.0 +hlint 3.2.2 +hourglass 0.2.12 +hpc 0.6.1.0 +hscolour 1.24.4 +http-client 0.6.4.1 +http-client-tls 0.3.5.3 +http-conduit 2.3.7.3 +http-types 0.12.3 +integer-gmp 1.0.3.0 +integer-logarithms 1.0.3.1 +lambda-client 0.0.1 +libyaml 0.1.2 +memory 0.15.0 +mime-types 0.1.0.9 +mono-traversable 1.0.15.1 +mprelude 0.2.1 +mtl 2.2.2 +network 3.1.1.1 +network-uri 2.6.3.0 +old-locale 1.0.0.7 +optparse-applicative 0.15.1.0 +parsec 3.1.14.0 +pem 0.2.4 +polyparse 1.13 +pretty 1.1.3.6 +primitive 0.7.1.0 +process 1.6.9.0 +random 1.1 +refact 0.3.0.2 +resourcet 1.2.4.2 +rts 1.0 +scientific 0.3.6.2 +socks 0.6.1 +source-constraints 0.0.2 +split 0.2.3.4 +stm 2.5.0.0 +streaming-commons 0.2.2.1 +strict 0.4 +syb 0.7.1 +tagged 0.8.6 +tasty 1.3.1 +tasty-expected-failure 0.11.1.2 +tasty-hunit 0.10.0.2 +tasty-mgolden 0.0.1 +template-haskell 2.16.0.0 +terminfo 0.4.1.4 +text 1.2.3.2 +th-abstraction 0.4.0.0 +these 1.1.1.1 +time 1.9.3 +time-compat 1.9.4 +tls 1.5.4 +transformers 0.5.6.2 +transformers-compat 0.6.6 +typed-process 0.2.6.0 +unbounded-delays 0.1.1.0 +uniplate 1.6.13 +unix 2.7.2.2 +unliftio-core 0.2.0.1 +unordered-containers 0.2.13.0 +utf8-string 1.0.1.1 +uuid-types 1.0.3 +vector 0.12.1.2 +vector-algorithms 0.8.0.3 +wcwidth 0.0.2 +x509 1.7.5 +x509-store 1.6.7 +x509-system 1.6.6 +x509-validation 1.6.11 +yaml 0.11.5.0 +zlib 0.6.2.2 diff --git a/test/stack-8.8-dependencies.txt b/test/stack-8.8-dependencies.txt new file mode 100644 index 0000000..e1e7e0c --- /dev/null +++ b/test/stack-8.8-dependencies.txt @@ -0,0 +1,128 @@ +Diff 0.4.0 +aeson 1.4.7.1 +alex 3.2.5 +ansi-terminal 0.10.3 +ansi-wl-pprint 0.6.9 +array 0.5.4.0 +asn1-encoding 0.9.6 +asn1-parse 0.9.5 +asn1-types 0.3.4 +async 2.2.2 +attoparsec 0.13.2.4 +base 4.13.0.0 +base-compat 0.11.2 +base-compat-batteries 0.11.2 +base-orphans 0.8.3 +basement 0.0.11 +binary 0.8.7.0 +blaze-builder 0.4.1.0 +bytestring 0.10.10.1 +call-stack 0.2.0 +case-insensitive 1.2.1.0 +cereal 0.5.8.1 +clock 0.8 +cmdargs 0.10.20 +colour 2.3.5 +conduit 1.3.3 +conduit-extra 1.3.5 +connection 0.3.1 +containers 0.6.2.1 +control-bool 0.2.1 +conversions 0.0.4 +cookie 0.4.5 +cpphs 1.20.9.1 +cryptonite 0.26 +data-default 0.7.1.1 +data-default-class 0.1.2.0 +data-default-instances-containers 0.0.1 +data-default-instances-dlist 0.0.1 +data-default-instances-old-locale 0.0.1 +deepseq 1.4.4.0 +devtools 0.1.0 +directory 1.3.6.0 +dlist 0.8.0.8 +envy 1.5.1.0 +exceptions 0.10.4 +extra 1.7.8 +file-embed 0.0.11.2 +filepath 1.4.2.1 +filepattern 0.1.2 +ghc 8.8.4 +ghc-boot 8.8.4 +ghc-boot-th 8.8.4 +ghc-heap 8.8.4 +ghc-lib-parser 8.10.2.20200916 +ghc-lib-parser-ex 8.10.0.16 +ghc-prim 0.5.3 +ghci 8.8.4 +happy 1.19.12 +hashable 1.3.0.0 +hlint 3.1.6 +hourglass 0.2.12 +hpc 0.6.0.3 +hscolour 1.24.4 +http-client 0.6.4.1 +http-client-tls 0.3.5.3 +http-conduit 2.3.7.3 +http-types 0.12.3 +integer-gmp 1.0.2.0 +integer-logarithms 1.0.3.1 +lambda-client 0.0.1 +libyaml 0.1.2 +memory 0.15.0 +mime-types 0.1.0.9 +mono-traversable 1.0.15.1 +mprelude 0.2.1 +mtl 2.2.2 +network 3.1.1.1 +network-uri 2.6.3.0 +old-locale 1.0.0.7 +optparse-applicative 0.15.1.0 +parsec 3.1.14.0 +pem 0.2.4 +polyparse 1.13 +pretty 1.1.3.6 +primitive 0.7.0.1 +process 1.6.9.0 +random 1.1 +refact 0.3.0.2 +resourcet 1.2.4.2 +rts 1.0 +scientific 0.3.6.2 +socks 0.6.1 +source-constraints 0.0.2 +split 0.2.3.4 +stm 2.5.0.0 +streaming-commons 0.2.2.1 +syb 0.7.1 +tagged 0.8.6 +tasty 1.3.1 +tasty-expected-failure 0.11.1.2 +tasty-hunit 0.10.0.2 +tasty-mgolden 0.0.1 +template-haskell 2.15.0.0 +terminfo 0.4.1.4 +text 1.2.4.0 +th-abstraction 0.3.2.0 +time 1.9.3 +time-compat 1.9.4 +tls 1.5.4 +transformers 0.5.6.2 +transformers-compat 0.6.6 +typed-process 0.2.6.0 +unbounded-delays 0.1.1.0 +uniplate 1.6.13 +unix 2.7.2.2 +unliftio-core 0.2.0.1 +unordered-containers 0.2.10.0 +utf8-string 1.0.1.1 +uuid-types 1.0.3 +vector 0.12.1.2 +vector-algorithms 0.8.0.3 +wcwidth 0.0.2 +x509 1.7.5 +x509-store 1.6.7 +x509-system 1.6.6 +x509-validation 1.6.11 +yaml 0.11.5.0 +zlib 0.6.2.2