-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/support api gateway proxy events #52
Merged
IamfromSpace
merged 52 commits into
master
from
feature/support-api-gateway-proxy-events
Jan 15, 2021
Merged
Changes from all commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
3577a6d
Very WIP for supporting API Gateway Lambda Proxy Events. Basic struc…
IamfromSpace b07a646
Reflect the API Gateway Events at a very low level.
IamfromSpace 8f80114
Add custom authorizer and correctly identify nullable fields.
IamfromSpace c2400ef
Use lazy text for ApiGatewayProxyRequest.
IamfromSpace 33d313a
Unparameterize ApiGatewayProxyRequest, since it doesn't make much sen…
IamfromSpace 9a78e23
WIP add a more idiomatic/typed api gateway event. In desperate need …
IamfromSpace 19d0c03
Add an 'ApiGatewayRuntime' that is almost the same interface as the g…
IamfromSpace 1de5e10
Experiment with a chaining/composable API based on profunctors.
IamfromSpace 7dbf8b3
Use combinators for the entire ApiGatewayRuntime module.
IamfromSpace 567f330
Use a default 400 so that the interfaces are almost identical between…
IamfromSpace 928e694
Use Text instead of Bytestring when converting a raw API Gateway requ…
IamfromSpace 9e153e2
Use combinators for the ApiGatewayRuntime.
IamfromSpace e523045
Fix an issue where the authorizer field was required, when it may be …
IamfromSpace 1eb7c8e
Use a HashMap for api gateway response headers so they encode a JSON …
IamfromSpace 3635921
Make the json decode for the ApiGatewayProxyRequest much more Haskell…
IamfromSpace c70eb35
Clean up warnings.
IamfromSpace dcc97d9
Decode an ApiGatewayProxyRequest body as a (contextually-correct) Byt…
IamfromSpace 84c5a27
Use Text instead of lazy Text in most Api Gateway event properties.
IamfromSpace f880f62
Parse additional API Gateway Request Event fields.
IamfromSpace 2497bb0
Use a LambdaSerializable class to help safely encode a variety of con…
IamfromSpace 9dfe827
Export types that should have been available.
IamfromSpace dd7294d
Add metadata about encoding to LambdaSerializable and the resulting J…
IamfromSpace 6fe29c8
Create an ApiGatewayProxyBody type instead using a typeclass strategy.
IamfromSpace 66530e5
Use the HTTP status type instead of Ints for status codes.
IamfromSpace eaf8b33
Use maybes for unguaranteed fields (not present in sam local).
IamfromSpace e005b69
Re-export all http-types Statuses.
IamfromSpace 6b34ef5
Update simple example to be up to date with API Gateway proxy events.…
IamfromSpace 005895f
Remove authorizer information with a TODO to return it with a proper …
IamfromSpace 1dbdd3a
Allow an arbitrary value for custom authorizor data, with that type a…
IamfromSpace c2161ce
Fix parsing for domainName, domainPrefix, and extendedRequestId (all …
IamfromSpace e42e35e
Add TODO.
IamfromSpace 3a314c4
Styling
IamfromSpace c86a6a4
Move ApiGateway events into a more nested module to group them.
IamfromSpace a5b0d85
Add NoAuthorizer and StrictlyNoAuthorizer as helper types to correctl…
IamfromSpace a47beef
Add documentation strings.
IamfromSpace fe44c1c
Require that headers on the API Gateway proxy response be case insens…
IamfromSpace 7e69a37
Allow API Gateway responses to include multi-value headers.
IamfromSpace 98d943e
Restore simple example.
IamfromSpace 1749a29
Remove diff noise.
IamfromSpace 0011086
Set DuplicateRecordFields as a default extension.
IamfromSpace 53a6540
Remove unneeded extensions.
IamfromSpace 8d5060e
Fix copyright dates to earliest date of publish.
IamfromSpace 3219d55
Use _only_ multiValueHeaders instead of headers, and supply smart con…
IamfromSpace 65a235f
Move status exports to the bottom of the exports for better haddock r…
IamfromSpace fb1ce37
Move imageGif and imageJpeg into doc examples rather than exorts. Al…
IamfromSpace 3884635
Fix incorrect doc reference.
IamfromSpace d8d6649
Remove redundant indentation.
IamfromSpace 0402a92
Explicitly choose statuses for export to ensure nothing else gets exp…
IamfromSpace f9f2b12
Add module level documentation for ProxyRequest and ProxyResponse.
IamfromSpace e6b8adb
Use fold for defaulting Monoidal parsed fields.
IamfromSpace 52fe784
Use withObject instead of pattern matching for parseJSON.
IamfromSpace 6565b44
Force StrictlyNoAuthorizer to fail parsing in all cases where an auth…
IamfromSpace File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 | ||
IamfromSpace marked this conversation as resolved.
Show resolved
Hide resolved
|
||
-- 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 | ||
] |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created a card here: #53
Other than adding the fields, is there anything else we need to support websockets from apig?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is a bit of an odd one. The more I think about it, the more I think that this should be a separate event that maybe has some common models (Identity ab RequestContext maybe). But ultimately it makes a bunch of fields optional if we try to do them together. Which makes sense for JSON, but less so here—where we want to strongly type and communicate guarantees wherever possible.
The downside of the separate is a bunch of boilerplate. The record field destructuring is super useful for these types, but it doesn’t seem to compose for reuse very nicely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have to chew on this one a bit more before responding more completely