Skip to content

Feature request: improve support for parameter injection #5325

Open
@tobocop2

Description

@tobocop2

Why is this needed?

My apologies if there something already exists that addresses this issue. I searched all of the code and I just could not figure out a reusable way to handle parameter injection without rolling my own system.

Request body and query string parsing as well as other aspects of request parsing are not reusable. For example, a pydantic based parameter injection system can be put in place at some point in the stack to make it more reusable. I've implemented this in a work project using a custom decorator

from functools import wraps
from typing import Optional, Type
import inspect
import json

from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent
from pydantic import BaseModel, ValidationError

from lib.common.app import app

# NOTE: this utility does not map these types to the open api specification. 
def inject_params(
    query_model: Optional[Type[BaseModel]] = None,
    body_model: Optional[Type[BaseModel]] = None,
):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            _ = args
            api_event: APIGatewayProxyEvent = app.current_event
            query_params = None
            body_params = None

            if query_model:
                query_dict = api_event.query_string_parameters or {}
                try:
                    query_params = query_model(**query_dict)
                except ValidationError as e:
                    # Raise validation error to be handled by existing middleware
                    raise e

            if body_model:
                try:
                    body_dict = json.loads(api_event.body or "{}")
                    body_params = body_model(**body_dict)
                except (ValidationError, json.JSONDecodeError) as e:
                    # Raise validation error to be handled by existing middleware
                    raise e

            # Dynamically inject only the parameters that the handler expects
            handler_sig = inspect.signature(func)
            if "query_params" in handler_sig.parameters and query_params is not None:
                kwargs["query_params"] = query_params
            if "body_params" in handler_sig.parameters and body_params is not None:
                kwargs["body_params"] = body_params

            return func(*args, **kwargs)

        return wrapper

    return decorator

Example usage:

from http import HTTPStatus

from get_things import get_things
from lib.common.api_client.models.responses import PaginatedThingResponse
from lib.common.app import app, logger, metrics
from lib.common.aws import JsonResponse # custom class I implemented, ignore for the scope of this issue
from lib.common.dtos import GetThingsRequest
from lib.common.middleware.inject_params import inject_params
from lib.common.utils import run_async


@app.get("/thing")
@inject_params(query_model=GetThingsRequest)
def handler(
    query_params: GetThingsRequest,
) -> JsonResponse[PaginatedThingResponse]:
    response: PaginatedThingResponse = run_async(get_things, query_params)
    return JsonResponse(response, status_code=HTTPStatus.OK)

So this allows for reusable request parsing with pydantic. The documented parsing strategy at this time is something that otherwise has to be repeated in every handler. Since this framework really leans into pydantic already I think this kind of functionality should exist. My implementation doesn't cover header parsing or anything else, but it could be extended to support it.

I'd be happy to make an open source contribution if it makes sense. If so, where would be the best place?

Which area does this relate to?

No response

Suggestion

No response

Acknowledgment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions