BlackSheep icon indicating copy to clipboard operation
BlackSheep copied to clipboard

Allowing Enums or Literals in Path parameters

Open arthurbrenno opened this issue 7 months ago • 4 comments

🚀 Feature Request

Hi. Is it possible to use Literals or Enums with route parameters? This would help users in the auto OpenAPI documentation generation to see valid parameters.

Consider the following real-world scenario:

# imports omitted for brevity

class Documents(CortexAPIController):
    @classmethod
    def version(cls) -> str:
        return "v1"

    @classmethod
    def route(cls):
        return "/documentos-institucionais/"

    @docs(extract_document_contents_docs)
    @post("/{document_type}/extracoes")
    async def extract_contents_async(
        self,
        files: FromFiles,
        document_type: str,
        extraction_service: InsitutionalDocumentInfoExtractionProtocol,
        consumer_id: FromConsumerIDHeader,
    ) -> StructuredDocumentType:
        data = files.value
        form_part = data[0]

        if form_part.file_name is None:
            raise BadRequest("The provided file does not contain any name")

        file_contents = form_part.data
        path = Path(form_part.file_name.decode("utf-8"))
        extension = path.suffix.lstrip(".")

        try:
            with tempfile.TemporaryDirectory() as temp_dir:
                temp_file_path = os.path.join(temp_dir, f"temp.{extension}")

                with open(temp_file_path, "wb") as temp_file:
                    temp_file.write(file_contents)
                    return await extraction_service.extract_info_async(
                        file_path=temp_file_path,
                        declared_type=DocumentType(document_type), # this woudn' be necessary anymore!
                        consumer_id=consumer_id.value,
                    )
        except InvalidDocumentException as e:
            raise BadRequest(str(e))

It would be really nice to have "document_type" in this case, typed with a Literal or Enum. But in both cases I have some errors. I tried to research this in the docs, but I found nothing related to this. Is this possible with current BlackSheep version?

arthurbrenno avatar Jun 23 '25 20:06 arthurbrenno

Hi Arthur, Good point! The built-in classes to bind route values do not support literals or enums. Support for these could be added, it would be a good idea.

BlackSheep offers an API to define custom binders. I prepared an example for you:

from enum import StrEnum
from urllib.request import Request

from openapidocs.v3 import Info

from blacksheep import Application
from blacksheep.exceptions import BadRequest
from blacksheep.server.bindings import Binder, BoundValue, T
from blacksheep.server.openapi.v3 import OpenAPIHandler


class DocumentType(StrEnum):
    ONE = "One"
    TWO = "Two"
    THREE = "Three"


class EnumFromRoute(BoundValue[T]):
    pass


class EnumFromRouteBinder(Binder):

    handle = EnumFromRoute

    async def get_value(self, request: Request):
        value = request.route_values.get(self.parameter_name)
        try:
            return self.expected_type(value)
        except ValueError:
            raise BadRequest(
                f"Invalid parameter: '{value}' "
                f"is not a valid {self.expected_type.__name__}"
            )


app = Application()

docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))
docs.bind_app(app)


@app.router.get("/{document_type}")
def home(document_type: EnumFromRoute[DocumentType]):
    return f"Hello World {document_type}"


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, port=44777)
curl http://localhost:44777/Two
Hello World <EnumFromRoute(Two)>

curl http://localhost:44777/two
Bad Request: Invalid parameter: 'two' is not a valid DocumentType

Note that in this case, since the Enum is obtained with self.expected_type(value), values are case sensitive. Additional code is needed for a case insensitive check.

For literals, you would need introspecting code to validate the possible values:

from typing import get_args, Literal

def validate_literal(literal_type, value: str):
    allowed = get_args(literal_type)
    for allowed_value in allowed:
        if isinstance(allowed_value, str) and allowed_value.lower() == value.lower():
            return allowed_value
        if allowed_value == value:
            return allowed_value
    raise ValueError(f"{value!r} is not a valid {literal_type}")

RobertoPrevato avatar Jun 24 '25 19:06 RobertoPrevato

Hi, Roberto. Thanks for the feedback! I'll consider this implementation for now. Thanks a lot!

arthurbrenno avatar Jun 25 '25 15:06 arthurbrenno

Thanks, Arthur! I reopened the issue because I would like to add support for StrEnum and IntEnum (I started this). 👍🏼

RobertoPrevato avatar Jun 25 '25 20:06 RobertoPrevato

Thanks, Arthur! I reopened the issue because I would like to add support for StrEnum and IntEnum (I started this). 👍🏼

Oh. Okay! Thanks! I think this would be a nice addition to BlackSheep.

arthurbrenno avatar Jun 25 '25 20:06 arthurbrenno

Done at https://github.com/Neoteroi/BlackSheep/pull/597.

RobertoPrevato avatar Sep 27 '25 19:09 RobertoPrevato