Skip to content

Commit

Permalink
api: Add new typed_endpoint decorators.
Browse files Browse the repository at this point in the history
The goal of typed_endpoint is to replicate most features supported by
has_request_variables, and to improve on top of it. There are some
unresolved issues that we don't plan to work on currently. For example,
typed_endpoint does not support ignored_parameters_supported for 400
responses, and it does not run validators on path-only arguments.

Unlike has_request_variables, typed_endpoint supports error handling by
processing validation errors from Pydantic.

Most features supported by has_request_variables are supported by
typed_endpoint in various ways.

To define a function, use a syntax like this with Annotated if there is
any metadata you want to associate with a parameter:
```
@typed_endpoint
def view(
    request: HttpRequest,
    user_profile: UserProfile,
    foo: int,
    bar: Annotated[int, ApiParamConfig(path_only=True)],
    baz: Json[int],
    other: Annotated[
        Json[int],
        ApiParamConfig(
            whence="lorem",
            documentation_status=NTENTIONALLY_UNDOCUMENTED
        )
    ] = 10,
) -> HttpResponse:
    ....
```

There are also some shorthands for the commonly used annotated types,
which are encouraged when applicable for better readability:
```
WebhookPayload = Annotated[Json[T], ApiParamConfig(argument_type_is_body=True)]
PathOnly = Annotated[T, ApiParamConfig(path_only=True)]
```

Then the view function above can be rewritten as:
```
@typed_endpoint
def view(
    request: HttpRequest,
    user_profile: UserProfile,
    foo: int,
    bar: PathOnly[int],
    baz: Json[int],
    other: Annotated[
        int,
        ApiParamConfig(
            whence="lorem",
            documentation_status=INTENTIONALLY_UNDOCUMENTED
        )
    ] = 10,
) -> HttpResponse:
    ....
```

There are some intentional restrictions:
- A single parameter cannot have more than one ApiParamConfig
- Path-only parameters cannot have default values
- argument_type_is_body is incompatible with whence
- Arguments of name "request", "user_profile", "args", and "kwargs" and
  etc. are ignored by typed_endpoint.
- positional-only arguments are not supported by endpoint.

typed_endpoint's handling of ignored_parameters_unsupported is mostly
identical to that of has_request_variables.
  • Loading branch information
PIG208 committed Aug 29, 2023
1 parent a3fa031 commit 6cbda82
Show file tree
Hide file tree
Showing 5 changed files with 1,126 additions and 0 deletions.
1 change: 1 addition & 0 deletions tools/linter_lib/custom_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"zerver/lib/email_mirror.py",
"zerver/lib/email_notifications.py",
"zerver/lib/send_email.py",
"zerver/lib/typed_endpoint.py",
"zerver/tests/test_new_users.py",
"zerver/tests/test_email_mirror.py",
"zerver/tests/test_message_notification_emails.py",
Expand Down
7 changes: 7 additions & 0 deletions zerver/lib/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum, auto
from typing import Any, Dict, List, Optional, Tuple, Union

import pydantic
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from django_stubs_ext import StrPromise
Expand Down Expand Up @@ -527,3 +528,9 @@ def __init__(self) -> None:
@staticmethod
def msg_format() -> str:
return _("Reaction doesn't exist.")


class ApiParamValidationError(JsonableError):
def __init__(self, msg: str, error: pydantic.ValidationError) -> None:
super().__init__(msg)
self.errors = error.errors()

0 comments on commit 6cbda82

Please sign in to comment.