-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #52 from Nike-Inc/feature/support-api-gateway-prox…
…y-events Feature/support api gateway proxy events
- Loading branch information
Showing
3 changed files
with
381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
{-| | ||
Module : AWS.Lambda.Events.ApiGateway.ProxyRequest | ||
Description : Data types that represent typical lambda responses | ||
Copyright : (c) Nike, Inc., 2019 | ||
License : BSD3 | ||
Maintainer : [email protected], [email protected] | ||
Stability : stable | ||
This module exposes types used to model incoming __proxy__ requests from AWS | ||
API Gateway. These types are a light pass over the incoming JSON | ||
representation. | ||
-} | ||
module AWS.Lambda.Events.ApiGateway.ProxyRequest | ||
( ProxyRequest(..) | ||
, RequestContext(..) | ||
, Identity(..) | ||
, NoAuthorizer | ||
, StrictlyNoAuthorizer | ||
) where | ||
|
||
import Data.Aeson (FromJSON, Value, parseJSON, | ||
withObject, (.:), (.:?)) | ||
import Data.ByteString.Base64.Lazy (decodeLenient) | ||
import Data.ByteString.Lazy (ByteString) | ||
import Data.CaseInsensitive (CI, mk) | ||
import Data.Foldable (fold) | ||
import Data.Functor ((<&>)) | ||
import Data.HashMap.Strict (HashMap, foldrWithKey, insert) | ||
import Data.Text (Text) | ||
import qualified Data.Text.Lazy as TL | ||
import qualified Data.Text.Lazy.Encoding as TLE | ||
import Data.Void (Void) | ||
import GHC.Generics (Generic (..)) | ||
|
||
data Identity = Identity | ||
{ cognitoIdentityPoolId :: Maybe Text | ||
, accountId :: Maybe Text | ||
, cognitoIdentityId :: Maybe Text | ||
, caller :: Maybe Text | ||
, apiKey :: Maybe Text | ||
, sourceIp :: Text | ||
, accessKey :: Maybe Text | ||
, cognitoAuthenticationType :: Maybe Text | ||
, cognitoAuthenticationProvider :: Maybe Text | ||
, userArn :: Maybe Text | ||
, apiKeyId :: Maybe Text | ||
, userAgent :: Maybe Text | ||
, user :: Maybe Text | ||
} deriving (Generic) | ||
|
||
instance FromJSON Identity | ||
|
||
data RequestContext a = RequestContext | ||
{ path :: Text | ||
, accountId :: Text | ||
, authorizer :: Maybe a | ||
, resourceId :: Text | ||
, stage :: Text | ||
, domainPrefix :: Maybe Text | ||
, requestId :: Text | ||
, identity :: Identity | ||
, domainName :: Maybe Text | ||
, resourcePath :: Text | ||
, httpMethod :: Text | ||
, extendedRequestId :: Maybe Text | ||
, apiId :: Text | ||
} | ||
|
||
instance FromJSON a => FromJSON (RequestContext a) where | ||
parseJSON = withObject "ProxyRequest" $ \v -> | ||
RequestContext <$> v .: "path" <*> v .: "accountId" <*> | ||
v .:? "authorizer" <*> | ||
v .: "resourceId" <*> | ||
v .: "stage" <*> | ||
v .:? "domainPrefix" <*> | ||
v .: "requestId" <*> | ||
v .: "identity" <*> | ||
v .:? "domainName" <*> | ||
v .: "resourcePath" <*> | ||
v .: "httpMethod" <*> | ||
v .:? "extendedRequestId" <*> | ||
v .: "apiId" | ||
|
||
-- TODO: Should also include websocket fields | ||
-- | This type is for representing events that come from API Gateway via the | ||
-- Lambda Proxy integration (forwarding HTTP data directly, rather than a | ||
-- custom integration). It will automatically decode the event that comes in. | ||
-- | ||
-- The 'ProxyRequest' notably has one parameter for the type of information | ||
-- returned by the API Gateway's custom authorizer (if applicable). This type | ||
-- must also implement FromJSON so that it can be decoded. If you do not | ||
-- expect this data to be populated we recommended using the 'NoAuthorizer' | ||
-- type exported from this module (which is just an alias for 'Value'). If | ||
-- there _must not_ be authorizer populated (this is unlikely) then use the | ||
-- 'StrictlyNoAuthorizer' type. | ||
-- | ||
-- @ | ||
-- {-\# LANGUAGE NamedFieldPuns \#-} | ||
-- {-\# LANGUAGE DuplicateRecordFields \#-} | ||
-- | ||
-- module Main where | ||
-- | ||
-- import AWS.Lambda.Runtime (pureRuntime) | ||
-- import AWS.Lambda.Events.ApiGateway.ProxyRequest (ProxyRequest(..), NoAuthorizer) | ||
-- import AWS.Lambda.Events.ApiGateway.ProxyResponse (ProxyResponse(..), textPlain, forbidden403, ok200) | ||
-- | ||
-- myHandler :: ProxyRequest NoAuthorizer -> ProxyResponse | ||
-- myHandler ProxyRequest { httpMethod = \"GET\", path = "/say_hello" } = | ||
-- ProxyResponse | ||
-- { status = ok200 | ||
-- , body = textPlain \"Hello\" | ||
-- , headers = mempty | ||
-- , multiValueHeaders = mempty | ||
-- } | ||
-- myHandler _ = | ||
-- ProxyResponse | ||
-- { status = forbidden403 | ||
-- , body = textPlain \"Forbidden\" | ||
-- , headers = mempty | ||
-- , multiValueHeaders = mempty | ||
-- } | ||
-- | ||
-- main :: IO () | ||
-- main = pureRuntime myHandler | ||
-- @ | ||
data ProxyRequest a = ProxyRequest | ||
{ path :: Text | ||
, headers :: HashMap (CI Text) Text | ||
, multiValueHeaders :: HashMap (CI Text) [Text] | ||
, pathParameters :: HashMap Text Text | ||
, stageVariables :: HashMap Text Text | ||
, requestContext :: RequestContext a | ||
, resource :: Text | ||
, httpMethod :: Text | ||
, queryStringParameters :: HashMap Text Text | ||
, multiValueQueryStringParameters :: HashMap Text [Text] | ||
, body :: ByteString | ||
} deriving (Generic) | ||
|
||
toCIHashMap :: HashMap Text a -> HashMap (CI Text) a | ||
toCIHashMap = foldrWithKey (insert . mk) mempty | ||
|
||
toByteString :: Bool -> TL.Text -> ByteString | ||
toByteString isBase64Encoded = | ||
if isBase64Encoded | ||
then decodeLenient . TLE.encodeUtf8 | ||
else TLE.encodeUtf8 | ||
|
||
-- | For ignoring API Gateway custom authorizer values | ||
type NoAuthorizer = Value | ||
|
||
-- | For ensuring that there were no API Gateway custom authorizer values (this | ||
-- is not likely to be useful, you probably want 'NoAuthorizer') | ||
type StrictlyNoAuthorizer = Void | ||
|
||
instance FromJSON a => FromJSON (ProxyRequest a) where | ||
parseJSON = withObject "ProxyRequest" $ \v -> | ||
ProxyRequest <$> v .: "path" <*> | ||
(v .:? "headers" <&> toCIHashMap . fold) <*> | ||
(v .:? "multiValueHeaders" <&> toCIHashMap . fold) <*> | ||
(v .:? "pathParameters" <&> fold) <*> | ||
(v .:? "stageVariables" <&> fold) <*> | ||
v .: "requestContext" <*> | ||
v .: "resource" <*> | ||
v .: "httpMethod" <*> | ||
(v .:? "queryStringParameters" <&> fold) <*> | ||
(v .:? "multiValueQueryStringParameters" <&> fold) <*> | ||
(toByteString <$> v .: "isBase64Encoded" <*> (v .:? "body" <&> fold)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
{-| | ||
Module : AWS.Lambda.Events.ApiGateway.ProxyResponse | ||
Description : Data types that represent typical lambda responses | ||
Copyright : (c) Nike, Inc., 2019 | ||
License : BSD3 | ||
Maintainer : [email protected], [email protected] | ||
Stability : stable | ||
This module enable exposes the required types for responding to API Gateway | ||
Proxy Events. Responses must return a status, body, and optionaly headers. | ||
Multiple smart contructors and helpers are provided to help encapsulated | ||
details like header case-insensitivity, multiple header copies, correct base64 | ||
encoding, and default content type. | ||
-} | ||
module AWS.Lambda.Events.ApiGateway.ProxyResponse | ||
( ProxyResponse(..) | ||
, response | ||
, addHeader | ||
, setHeader | ||
, ProxyBody(..) | ||
, textPlain | ||
, applicationJson | ||
, genericBinary | ||
, module Network.HTTP.Types.Status | ||
) where | ||
|
||
import Data.Aeson (ToJSON, encode, object, toJSON, | ||
(.=)) | ||
import Data.ByteString (ByteString) | ||
import qualified Data.ByteString.Base64 as B64 | ||
import Data.CaseInsensitive (CI, mk, original) | ||
import Data.HashMap.Strict (HashMap, foldrWithKey, insert, | ||
insertWith) | ||
import qualified Data.Text as T | ||
import qualified Data.Text.Encoding as TE | ||
import qualified Data.Text.Lazy as TL | ||
import qualified Data.Text.Lazy.Encoding as TLE | ||
import Network.HTTP.Types.Status (Status (..), accepted202, | ||
badGateway502, badRequest400, | ||
conflict409, continue100, | ||
created201, expectationFailed417, | ||
forbidden403, found302, | ||
gatewayTimeout504, gone410, | ||
httpVersionNotSupported505, | ||
imATeapot418, | ||
internalServerError500, | ||
lengthRequired411, | ||
methodNotAllowed405, | ||
movedPermanently301, | ||
multipleChoices300, | ||
networkAuthenticationRequired511, | ||
noContent204, nonAuthoritative203, | ||
notAcceptable406, notFound404, | ||
notImplemented501, notModified304, | ||
ok200, partialContent206, | ||
paymentRequired402, | ||
permanentRedirect308, | ||
preconditionFailed412, | ||
preconditionRequired428, | ||
proxyAuthenticationRequired407, | ||
requestEntityTooLarge413, | ||
requestHeaderFieldsTooLarge431, | ||
requestTimeout408, | ||
requestURITooLong414, | ||
requestedRangeNotSatisfiable416, | ||
resetContent205, seeOther303, | ||
serviceUnavailable503, status100, | ||
status101, status200, status201, | ||
status202, status203, status204, | ||
status205, status206, status300, | ||
status301, status302, status303, | ||
status304, status305, status307, | ||
status308, status400, status401, | ||
status402, status403, status404, | ||
status405, status406, status407, | ||
status408, status409, status410, | ||
status411, status412, status413, | ||
status414, status415, status416, | ||
status417, status418, status422, | ||
status426, status428, status429, | ||
status431, status500, status501, | ||
status502, status503, status504, | ||
status505, status511, | ||
switchingProtocols101, | ||
temporaryRedirect307, | ||
tooManyRequests429, unauthorized401, | ||
unprocessableEntity422, | ||
unsupportedMediaType415, | ||
upgradeRequired426, useProxy305) | ||
|
||
-- | Type that represents the body returned to an API Gateway when using HTTP | ||
-- Lambda Proxy integration. It is highly recommended that you do not use this | ||
-- type directly, and instead use the smart constructors exposed such as | ||
-- 'textPlain', 'applicationJson', and 'genericBinary'. These make sure that | ||
-- the base64 encodings work transparently. | ||
data ProxyBody = ProxyBody | ||
{ contentType :: T.Text | ||
, serialized :: T.Text | ||
, isBase64Encoded :: Bool | ||
} deriving (Show) | ||
|
||
-- | A response returned to an API Gateway when using the HTTP Lambda Proxy | ||
-- integration. ContentType will be set based on the ProxyBody (recommended) | ||
-- if a value is not present in the headers field. | ||
-- | ||
-- This type can be constructed explicity or via the smart constructor | ||
-- `response`. Headers can then be added incrementally with `addHeader` or | ||
-- `setHeader`. The smart constructor pattern is recommended because it avoids | ||
-- some of the awkwardness of dealing with the multiValueHeaders field's type. | ||
-- | ||
-- @ | ||
-- {-\# LANGUAGE NamedFieldPuns \#-} | ||
-- {-\# LANGUAGE DuplicateRecordFields \#-} | ||
-- {-\# LANGUAGE OverloadedStrings \#-} | ||
-- | ||
-- module Main where | ||
-- | ||
-- import AWS.Lambda.Runtime (pureRuntime) | ||
-- import AWS.Lambda.Events.ApiGateway.ProxyRequest (ProxyRequest(..), NoAuthorizer) | ||
-- import AWS.Lambda.Events.ApiGateway.ProxyResponse (ProxyResponse(..), textPlain, forbidden403, ok200, response) | ||
-- | ||
-- myHandler :: ProxyRequest NoAuthorizer -> ProxyResponse | ||
-- myHandler ProxyRequest { httpMethod = \"GET\", path = "/say_hello" } = | ||
-- -- Smart Constructor and added header (recommended) | ||
-- addHeader "My-Custom-Header" "Value" $ | ||
-- response ok200 $ textPlain \"Hello\" | ||
-- myHandler _ = | ||
-- -- Explicit Construction (not recommended) | ||
-- ProxyResponse | ||
-- { status = forbidden403 | ||
-- , body = textPlain \"Forbidden\" | ||
-- , multiValueHeaders = | ||
-- fromList [(mk "My-Custom-Header", ["Other Value])] | ||
-- } | ||
-- | ||
-- main :: IO () | ||
-- main = pureRuntime myHandler | ||
-- @ | ||
data ProxyResponse = ProxyResponse | ||
{ status :: Status | ||
, multiValueHeaders :: HashMap (CI T.Text) [T.Text] | ||
, body :: ProxyBody | ||
} deriving (Show) | ||
|
||
-- | Smart constructor for creating a ProxyResponse from a status and a body | ||
response :: Status -> ProxyBody -> ProxyResponse | ||
response = | ||
flip ProxyResponse mempty | ||
|
||
-- | Add a header to the ProxyResponse. If there was already a value for this | ||
-- header, this one is __added__, meaning the response will include multiple | ||
-- copies of this header (valid by the HTTP spec). This does NOT replace any | ||
-- previous headers or their values. | ||
addHeader :: T.Text -> T.Text -> ProxyResponse -> ProxyResponse | ||
addHeader header value (ProxyResponse s mvh b) = | ||
ProxyResponse s (insertWith (<>) (mk header) [value] mvh) b | ||
|
||
-- | Set a header to the ProxyResponse. If there were any previous values for | ||
-- this header they are __all replaced__ by this new value. | ||
setHeader :: T.Text -> T.Text -> ProxyResponse -> ProxyResponse | ||
setHeader header value (ProxyResponse s mvh b) = | ||
ProxyResponse s (insert (mk header) [value] mvh) b | ||
|
||
-- | Smart constructor for creating a ProxyBody with an arbitrary ByteString of | ||
-- the chosen content type. Use this smart constructor to avoid invalid JSON | ||
-- representations of binary data. | ||
-- | ||
-- From here it is easy to make more specific body constructors: | ||
-- | ||
-- @ | ||
-- imageGif :: ByteString -> ProxyBody | ||
-- imageGif = genericBinary "image/gif" | ||
-- | ||
-- imageJpeg :: ByteString -> ProxyBody | ||
-- imageJpeg = genericBinary "image/jpeg" | ||
-- @ | ||
genericBinary :: T.Text -> ByteString -> ProxyBody | ||
genericBinary contentType x = | ||
ProxyBody contentType (TE.decodeUtf8 $ B64.encode x) True | ||
|
||
-- | Smart constructor for creating a simple body of text. | ||
textPlain :: T.Text -> ProxyBody | ||
textPlain x = ProxyBody "text/plain; charset=utf-8" x False | ||
|
||
-- | Smart constructor for creating a simple body of JSON. | ||
applicationJson :: ToJSON a => a -> ProxyBody | ||
applicationJson x = | ||
ProxyBody | ||
"application/json; charset=utf-8" | ||
(TL.toStrict $ TLE.decodeUtf8 $ encode x) | ||
False | ||
|
||
-- | Smart constructor for creating a simple body of a GIF (that has already | ||
-- been converted to a ByteString). | ||
|
||
instance ToJSON ProxyResponse where | ||
toJSON (ProxyResponse status mvh (ProxyBody contentType body isBase64Encoded)) = | ||
let unCI = foldrWithKey (insert . original) mempty | ||
in object | ||
[ "statusCode" .= statusCode status | ||
, "multiValueHeaders" .= | ||
insertWith | ||
(\_ old -> old) | ||
("Content-Type" :: T.Text) | ||
[contentType] | ||
(unCI mvh) | ||
, "body" .= body | ||
, "isBase64Encoded" .= isBase64Encoded | ||
] |