v0.154.0
  1from collections.abc import Callable
  2from http import HTTPStatus
  3from typing import Any, TypeVar
  4
  5from plain.forms import fields
  6from plain.forms.forms import BaseForm
  7
  8from .helpers import json_content
  9from .utils import merge_data, schema_from_type
 10
 11F = TypeVar("F", bound=Callable[..., Any])
 12
 13
 14def response_typed_dict(
 15    status_code: int | HTTPStatus | str,
 16    return_type: Any,
 17    *,
 18    description: str = "",
 19    component_name: str = "",
 20) -> Callable[[F], F]:
 21    """
 22    A decorator to attach responses to a view method.
 23    """
 24
 25    def decorator(func: F) -> F:
 26        # TODO if return_type is a list/tuple,
 27        # then use anyOf or oneOf?
 28
 29        response_schema: dict[str, Any] = {
 30            "description": description or HTTPStatus(int(status_code)).phrase,
 31        }
 32
 33        if return_type:
 34            registry: dict[str, Any] = {}
 35            top_ref = schema_from_type(return_type, components=registry)
 36            response_schema["content"] = json_content(top_ref)
 37            func.openapi_components = merge_data(  # ty: ignore[unresolved-attribute] (dynamic attribute stamped by decorator)
 38                getattr(func, "openapi_components", {}),
 39                registry,
 40            )
 41
 42        if component_name:
 43            _schema = {
 44                "responses": {
 45                    str(status_code): {
 46                        "$ref": f"#/components/responses/{component_name}"
 47                    }
 48                }
 49            }
 50            func.openapi_components = merge_data(  # ty: ignore[unresolved-attribute] (dynamic attribute stamped by decorator)
 51                getattr(func, "openapi_components", {}),
 52                {
 53                    "responses": {
 54                        component_name: response_schema,
 55                    }
 56                },
 57            )
 58        else:
 59            _schema = {"responses": {str(status_code): response_schema}}
 60
 61        # Add the response schema to the function
 62        func.openapi_schema = merge_data(  # ty: ignore[unresolved-attribute] (dynamic attribute stamped by decorator)
 63            getattr(func, "openapi_schema", {}),
 64            _schema,
 65        )
 66
 67        return func
 68
 69    return decorator
 70
 71
 72def request_form(form_class: type[BaseForm]) -> Callable[[F], F]:
 73    """
 74    Create OpenAPI parameters from a form class.
 75    """
 76
 77    def decorator(func: F) -> F:
 78        field_mappings: dict[type[fields.Field], dict[str, str]] = {
 79            fields.IntegerField: {
 80                "type": "integer",
 81            },
 82            fields.FloatField: {
 83                "type": "number",
 84            },
 85            fields.DateTimeField: {
 86                "type": "string",
 87                "format": "date-time",
 88            },
 89            fields.DateField: {
 90                "type": "string",
 91                "format": "date",
 92            },
 93            fields.TimeField: {
 94                "type": "string",
 95                "format": "time",
 96            },
 97            fields.EmailField: {
 98                "type": "string",
 99                "format": "email",
100            },
101            fields.URLField: {
102                "type": "string",
103                "format": "uri",
104            },
105            fields.UUIDField: {
106                "type": "string",
107                "format": "uuid",
108            },
109            fields.DecimalField: {
110                "type": "number",
111            },
112            # fields.FileField: {
113            #     "type": "string",
114            #     "format": "binary",
115            # },
116            fields.ImageField: {
117                "type": "string",
118                "format": "binary",
119            },
120            fields.BooleanField: {
121                "type": "boolean",
122            },
123            fields.TextField: {
124                "type": "string",
125            },
126            fields.EmailField: {
127                "type": "string",
128                "format": "email",
129            },
130        }
131        json_schema: dict[str, Any] = {
132            "type": "object",
133            "properties": {},
134        }
135        request_body: dict[str, Any] = {
136            "content": json_content(json_schema),
137            # could add application/x-www-form-urlencoded?
138        }
139        _schema: dict[str, Any] = {
140            "requestBody": request_body,
141        }
142
143        required_fields = []
144
145        for field_name, field in form_class.base_fields.items():
146            field_schema = field_mappings[field.__class__].copy()
147            json_schema["properties"][field_name] = field_schema
148
149            if field.required:
150                required_fields.append(field_name)
151
152            # TODO add description to the schema
153            # TODO add example to the schema
154            # TODO add default to the schema
155
156        if required_fields:
157            json_schema["required"] = required_fields
158            # The body is required if any field is
159            request_body["required"] = True
160
161        func.openapi_schema = merge_data(  # ty: ignore[unresolved-attribute] (dynamic attribute stamped by decorator)
162            getattr(func, "openapi_schema", {}),
163            _schema,
164        )
165
166        return func
167
168    return decorator
169
170
171def schema(data: dict[str, Any]) -> Callable[[F], F]:
172    """
173    A decorator to attach raw OpenAPI schema to a router, view, or view method.
174    """
175
176    def decorator(func: F) -> F:
177        func.openapi_schema = merge_data(  # ty: ignore[unresolved-attribute] (dynamic attribute stamped by decorator)
178            getattr(func, "openapi_schema", {}),
179            data,
180        )
181        return func
182
183    return decorator