Skip to content
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

feat(parser): add support for Lambda Function URL #1442

Merged
merged 5 commits into from
Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .dynamodb import DynamoDBStreamEnvelope
from .event_bridge import EventBridgeEnvelope
from .kinesis import KinesisDataStreamEnvelope
from .lambda_function_url import LambdaFunctionUrlEnvelope
from .sns import SnsEnvelope, SnsSqsEnvelope
from .sqs import SqsEnvelope

Expand All @@ -15,6 +16,7 @@
"DynamoDBStreamEnvelope",
"EventBridgeEnvelope",
"KinesisDataStreamEnvelope",
"LambdaFunctionUrlEnvelope",
"SnsEnvelope",
"SnsSqsEnvelope",
"SqsEnvelope",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging
from typing import Any, Dict, Optional, Type, Union

from ..models import LambdaFunctionUrlModel
from ..types import Model
from .base import BaseEnvelope

logger = logging.getLogger(__name__)


class LambdaFunctionUrlEnvelope(BaseEnvelope):
"""Lambda function URL envelope to extract data within body key"""

def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) -> Optional[Model]:
"""Parses data found with model provided

Parameters
----------
data : Dict
Lambda event to be parsed
model : Type[Model]
Data model provided to parse after extracting data using envelope

Returns
-------
Any
Parsed detail payload with model provided
"""
logger.debug(f"Parsing incoming data with Lambda function URL model {LambdaFunctionUrlModel}")
parsed_envelope: LambdaFunctionUrlModel = LambdaFunctionUrlModel.parse_obj(data)
logger.debug(f"Parsing event payload in `detail` with {model}")
return self._parse(data=parsed_envelope.body, model=model)
2 changes: 2 additions & 0 deletions aws_lambda_powertools/utilities/parser/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .dynamodb import DynamoDBStreamChangedRecordModel, DynamoDBStreamModel, DynamoDBStreamRecordModel
from .event_bridge import EventBridgeModel
from .kinesis import KinesisDataStreamModel, KinesisDataStreamRecord, KinesisDataStreamRecordPayload
from .lambda_function_url import LambdaFunctionUrlModel
from .s3 import S3Model, S3RecordModel
from .s3_object_event import (
S3ObjectConfiguration,
Expand Down Expand Up @@ -66,6 +67,7 @@
"KinesisDataStreamModel",
"KinesisDataStreamRecord",
"KinesisDataStreamRecordPayload",
"LambdaFunctionUrlModel",
"S3Model",
"S3RecordModel",
"S3ObjectLambdaEvent",
Expand Down
2 changes: 1 addition & 1 deletion aws_lambda_powertools/utilities/parser/models/apigwv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class RequestContextV2AuthorizerIam(BaseModel):
principalOrgId: Optional[str]
userArn: Optional[str]
userId: Optional[str]
cognitoIdentity: RequestContextV2AuthorizerIamCognito
cognitoIdentity: Optional[RequestContextV2AuthorizerIamCognito]


class RequestContextV2AuthorizerJwt(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventV2Model


class LambdaFunctionUrlModel(APIGatewayProxyEventV2Model):
"""AWS Lambda Function URL model

Notes:
-----
Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0.

Keys related to API Gateway features not available in Function URL use a sentinel value (e.g.`routeKey`, `stage`).

Documentation:
- https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html
- https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads
"""

pass
3 changes: 3 additions & 0 deletions docs/utilities/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
title: Parser
description: Utility
---
<!-- markdownlint-disable MD043 -->

This utility provides data parsing and deep validation using [Pydantic](https://pydantic-docs.helpmanual.io/).

Expand Down Expand Up @@ -166,6 +167,7 @@ Parser comes with the following built-in models:
| **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service |
| **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway |
| **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload |
| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda Function URL payload |

### extending built-in models

Expand Down Expand Up @@ -305,6 +307,7 @@ Parser comes with the following built-in envelopes, where `Model` in the return
| **SnsSqsEnvelope** | 1. Parses data using `SqsModel`. <br/> 2. Parses SNS records in `body` key using `SnsNotificationModel`. <br/> 3. Parses data in `Message` key using your model and return them in a list. | `List[Model]` |
| **ApiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventModel`. <br/> 2. Parses `body` key using your model and returns it. | `Model` |
| **ApiGatewayV2Envelope** | 1. Parses data using `APIGatewayProxyEventV2Model`. <br/> 2. Parses `body` key using your model and returns it. | `Model` |
| **LambdaFunctionUrlEnvelope** | 1. Parses data using `LambdaFunctionUrlModel`. <br/> 2. Parses `body` key using your model and returns it. | `Model` |

### Bringing your own envelope

Expand Down
5 changes: 5 additions & 0 deletions tests/functional/parser/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,8 @@ class MyCloudWatchBusiness(BaseModel):
class MyApiGatewayBusiness(BaseModel):
message: str
username: str


class MyALambdaFuncUrlBusiness(BaseModel):
message: str
username: str
128 changes: 128 additions & 0 deletions tests/functional/parser/test_lambda_function_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from aws_lambda_powertools.utilities.parser import envelopes, event_parser
from aws_lambda_powertools.utilities.parser.models import LambdaFunctionUrlModel
from aws_lambda_powertools.utilities.typing import LambdaContext
from tests.functional.parser.schemas import MyALambdaFuncUrlBusiness
from tests.functional.utils import load_event


@event_parser(model=MyALambdaFuncUrlBusiness, envelope=envelopes.LambdaFunctionUrlEnvelope)
def handle_lambda_func_url_with_envelope(event: MyALambdaFuncUrlBusiness, _: LambdaContext):
assert event.message == "Hello"
assert event.username == "Ran"


@event_parser(model=LambdaFunctionUrlModel)
def handle_lambda_func_url_event(event: LambdaFunctionUrlModel, _: LambdaContext):
return event


def test_lambda_func_url_event_with_envelope():
event = load_event("lambdaFunctionUrlEvent.json")
event["body"] = '{"message": "Hello", "username": "Ran"}'
handle_lambda_func_url_with_envelope(event, LambdaContext())


def test_lambda_function_url_event():
json_event = load_event("lambdaFunctionUrlEvent.json")
event: LambdaFunctionUrlModel = handle_lambda_func_url_event(json_event, LambdaContext())

assert event.version == "2.0"
assert event.routeKey == "$default"

assert event.rawQueryString == ""

assert event.cookies is None

headers = event.headers
assert len(headers) == 20

assert event.queryStringParameters is None

assert event.isBase64Encoded is False
assert event.body is None
assert event.pathParameters is None
assert event.stageVariables is None

request_context = event.requestContext

assert request_context.accountId == "anonymous"
assert request_context.apiId is not None
assert request_context.domainName == "<url-id>.lambda-url.us-east-1.on.aws"
assert request_context.domainPrefix == "<url-id>"
assert request_context.requestId == "id"
assert request_context.routeKey == "$default"
assert request_context.stage == "$default"
assert request_context.time is not None
convert_time = int(round(request_context.timeEpoch.timestamp() * 1000))
assert convert_time == 1659687279885
assert request_context.authorizer is None

http = request_context.http
assert http.method == "GET"
assert http.path == "/"
assert http.protocol == "HTTP/1.1"
assert str(http.sourceIp) == "123.123.123.123/32"
assert http.userAgent == "agent"

assert request_context.authorizer is None


def test_lambda_function_url_event_iam():
json_event = load_event("lambdaFunctionUrlIAMEvent.json")
event: LambdaFunctionUrlModel = handle_lambda_func_url_event(json_event, LambdaContext())

assert event.version == "2.0"
assert event.routeKey == "$default"

assert event.rawQueryString == "parameter1=value1&parameter1=value2&parameter2=value"

cookies = event.cookies
assert len(cookies) == 2
assert cookies[0] == "cookie1"

headers = event.headers
assert len(headers) == 2

query_string_parameters = event.queryStringParameters
assert len(query_string_parameters) == 2
assert query_string_parameters.get("parameter2") == "value"

assert event.isBase64Encoded is False
assert event.body == "Hello from client!"
assert event.pathParameters is None
assert event.stageVariables is None

request_context = event.requestContext

assert request_context.accountId == "123456789012"
assert request_context.apiId is not None
assert request_context.domainName == "<url-id>.lambda-url.us-west-2.on.aws"
assert request_context.domainPrefix == "<url-id>"
assert request_context.requestId == "id"
assert request_context.routeKey == "$default"
assert request_context.stage == "$default"
assert request_context.time is not None
convert_time = int(round(request_context.timeEpoch.timestamp() * 1000))
assert convert_time == 1583348638390

http = request_context.http
assert http.method == "POST"
assert http.path == "/my/path"
assert http.protocol == "HTTP/1.1"
assert str(http.sourceIp) == "123.123.123.123/32"
assert http.userAgent == "agent"

authorizer = request_context.authorizer
assert authorizer is not None
assert authorizer.jwt is None
assert authorizer.lambda_value is None

iam = authorizer.iam
assert iam is not None
assert iam.accessKey == "AKIA..."
assert iam.accountId == "111122223333"
assert iam.callerId == "AIDA..."
assert iam.cognitoIdentity is None
assert iam.principalOrgId is None
assert iam.userId == "AIDA..."
assert iam.userArn == "arn:aws:iam::111122223333:user/example-user"