aws-larpi-0.1.0.0: src/AWS/LARPI.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
{-# LANGUAGE RankNTypes, ExistentialQuantification #-}
-- | The AWS.LARPI library defines the interface and the associated data types
-- for running a Haskell program directly on AWS Lambda, and handles most of
-- associated overheads, leaving the user free to focus on the functionality
-- itself.
module AWS.LARPI (
runLambdaInterface,
LambdaInterface(..), LambdaInvocation(..), LambdaError(..)
) where
import System.Environment
import Network.HTTP.Req
import Text.URI
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy as BL
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Maybe
import Data.Aeson
import GHC.Generics
import Control.Monad (when)
-- CONSTANTS
version :: Text
version = "2018-06-01"
invPath :: Url 'Http -> Url 'Http
invPath api = api /~ version /: "runtime" /: "invocation"
errHeader :: Option 'Http
errHeader = header "Lambda-Runtime-Function-Error-Type" "Unhandled"
data Config = Conf { c_base :: Url 'Http
, c_opts :: Option 'Http }
invocationResponse :: Config -> Text -> ByteString -> IO ()
invocationResponse Conf{..} req_id resp =
runReq defaultHttpConfig $ do
req POST
(invPath c_base /: req_id /: "response" )
(ReqBodyBs resp)
ignoreResponse
c_opts
return ()
invocationError :: Config -> Text -> LambdaError -> IO ()
invocationError Conf{..} req_id lerr =
runReq defaultHttpConfig $ do
res <- req POST
(invPath c_base /: req_id /: "error" )
(ReqBodyJson lerr)
ignoreResponse
(c_opts <> errHeader)
return ()
initializationError :: Config -> LambdaError -> IO ()
initializationError Conf{..} lerr =
runReq defaultHttpConfig $ do
res <- req POST
(c_base /~ version /: "runtime" /: "init" /: "error" )
(ReqBodyJson lerr)
ignoreResponse
(c_opts <> errHeader)
return ()
nextInvocation :: Config -> IO LambdaInvocation
nextInvocation Conf{..} = runReq defaultHttpConfig $ do
res <- req GET
(invPath c_base /: "next")
NoReqBody
bsResponse
(c_opts <> responseTimeout maxBound)
let rH = responseHeader res
return $ LambdaInvocation {
li_body = responseBody res
, li_aws_request_id = fromJust $
T.decodeUtf8 <$> (rH "Lambda-Runtime-Aws-Request-Id")
, li_deadline_ms = rH "Lambda-Runtime-Deadline-Ms"
, li_invoked_function_arn = rH "Lambda-Runtime-Invoked-Function-Arn"
, li_trace_id = rH "Lambda-Runtime-Trace-Id"
, li_client_context = rH "Lambda-Runtime-Client-Context"
, li_cognito_identity = rH "Lambda-Runtime-Cognito-Identity" }
--- EXPORTS
-- | A Lambda error consists of a message and an error type, both of which
-- the user can define.
data LambdaError =
LambdaErr { errorMessage :: String
, errorType :: String }
deriving (Show, Generic, ToJSON)
-- | A Lambda invocaton contains the body of the request and a few headers
-- defined by AWS. Only the AWS-Request-Id header is guaranteed to be defined.
-- The headers are the following:
--
-- * li_aws_request_id – The request ID, which identifies the request that triggered
-- the function invocation.
--
-- For example, 8476a536-e9f4-11e8-9739-2dfe598c3fcd.
--
-- * li_deadline_ms – The date that the function times out in Unix time milliseconds.
--
-- For example, 1542409706888.
--
-- * li_invoked_function_arn – The ARN of the Lambda function, version, or alias
-- that's specified in the invocation.
--
-- For example, arn:aws:lambda:us-east-2:123456789012:function:custom-runtime.
--
-- * li_trace_id – The AWS X-Ray tracing
-- header.
--
-- For example, Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1.
--
-- * li_client_context – For invocations from the AWS Mobile SDK, data about the
-- client application and device.
--
-- * li_cognito_identity – For invocations from the AWS Mobile SDK, data about the
-- Amazon Cognito identity provider.
data LambdaInvocation =
LambdaInvocation {
li_body :: ByteString
, li_aws_request_id :: Text
, li_deadline_ms :: Maybe ByteString
, li_invoked_function_arn :: Maybe ByteString
, li_trace_id :: Maybe ByteString
, li_client_context :: Maybe ByteString
, li_cognito_identity :: Maybe ByteString}
deriving (Show, Eq)
-- | A Lambda interface consists of two functions:
--
-- The li_handler is the function that is run on every Lambda Invocation
-- the results will be dumped as JSON, as this is what most AWS APIs
-- expect. The first argument is the result of the li_init function, such
-- as a database connection or an IORef or similar. Existentially
-- quantified in the same manner as the ST monad to avoid the state
-- escaping.
--
-- The li_init function is called at the start of the program to initialize
-- the Lambda function, and some optional state. The state is then passed
-- to the handler at each invocation, e.g. a database connection or similar.
data LambdaInterface a = forall s.
LambdaInterface {
li_handler :: ToJSON a => s
-> LambdaInvocation
-> IO (Either LambdaError a)
, li_init :: IO (Either LambdaError s)
}
-- | The runLambdaInteface should be provided with the definition of the
-- interface to run, and handles the interaction with the AWS API, such as the
-- waiting for next request, responding to the right request, setting the
-- X-Ray trace id and initializing the function at the start.
runLambdaInterface :: ToJSON a => LambdaInterface a -> IO ()
runLambdaInterface (LambdaInterface {..}) =
do Just api <- fmap T.pack <$> lookupEnv "AWS_LAMBDA_RUNTIME_API"
Just (c_base, c_opts) <- useHttpURI <$> mkURI ("http://" <> api)
let conf = Conf{..}
i_res <- li_init
case i_res of
Left err -> initializationError conf err
Right init_state -> loop conf init_state li_handler
where loop conf init_state handler = do
li@LambdaInvocation{..} <- nextInvocation conf
when (isJust li_trace_id) $
setEnv "_X_AMZN_TRACE_ID" $ B.unpack $ fromJust li_trace_id
res <- handler init_state li
case res of
Left lerr -> invocationError conf li_aws_request_id lerr
Right resp -> invocationResponse conf li_aws_request_id
(BL.toStrict $ encode resp)
loop conf init_state handler