Skip to content

Commit baf4b13

Browse files
committed
Added documentation for Response and Response models
1 parent a683db0 commit baf4b13

File tree

5 files changed

+396
-55
lines changed

5 files changed

+396
-55
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# Response Model
2+
3+
Each route handler has key-value pair of status codes and a response model.
4+
This response model holds information on the type of response to be returned.
5+
6+
```python
7+
# project_name/apps/items/controllers.py
8+
9+
from ellar.serializer import Serializer
10+
from ellar.common import Controller, get
11+
from ellar.core import ControllerBase
12+
13+
class UserSchema(Serializer):
14+
username: str
15+
email: str = None
16+
first_name: str = None
17+
last_name: str = None
18+
19+
20+
@Controller
21+
class ItemsController(ControllerBase):
22+
@get("/me", response=UserSchema)
23+
def me(self):
24+
return dict(username='Ellar', email='ellar@example.com')
25+
```
26+
27+
During route response computation, the `me` route handler response will evaluate to a
28+
`JSONResponseModel` with `UserSchema` as content validation schema.
29+
30+
The resulting route responses will be:
31+
32+
```python
33+
from ellar.serializer import Serializer
34+
from ellar.core.response.model import JSONResponseModel
35+
36+
37+
class UserSchema(Serializer):
38+
username: str
39+
email: str = None
40+
first_name: str = None
41+
last_name: str = None
42+
43+
44+
response = {200: JSONResponseModel(model_field_or_schema=UserSchema)}
45+
```
46+
47+
For documentation purposes, we can apply some `description` to the returned response
48+
49+
```python
50+
@get("/me", response=(UserSchema, 'User Schema Response'))
51+
def me(self):
52+
return dict(username='Ellar', email='ellar@example.com')
53+
```
54+
This will be translated to:
55+
56+
```python
57+
58+
response = {200: JSONResponseModel(model_field_or_schema=UserSchema, description='User Schema Response')}
59+
```
60+
61+
![response description](../img/response_description.png)
62+
63+
!!! info
64+
Each route handler has its own `ResponseModel` computation and validation. If there is no response definition, Ellar default the route handler model to `EmptyAPIResponseModel`.
65+
66+
67+
## Override Response Type
68+
69+
When you use a `Response` class as response, a `ResponseModel` is used and the `response_type` is replaced with applied response class.
70+
71+
For example:
72+
73+
```python
74+
# project_name/apps/items/controllers.py
75+
76+
from ellar.common import Controller, get
77+
from ellar.core import ControllerBase
78+
from starlette.responses import PlainTextResponse
79+
from ellar.serializer import Serializer
80+
81+
82+
class UserSchema(Serializer):
83+
username: str
84+
email: str = None
85+
first_name: str = None
86+
last_name: str = None
87+
88+
89+
@Controller
90+
class ItemsController(ControllerBase):
91+
@get("/me", response={200: PlainTextResponse, 201: UserSchema})
92+
def me(self):
93+
return "some text response."
94+
```
95+
This will be translated to:
96+
97+
```python
98+
from ellar.core.response.model import ResponseModel, JSONResponseModel
99+
from starlette.responses import PlainTextResponse
100+
101+
response = {200: ResponseModel(response_type=PlainTextResponse), 201: JSONResponseModel(model_field_or_schema=UserSchema)}
102+
```
103+
104+
## Response Model Properties
105+
106+
All response model follows `IResponseModel` contract.
107+
108+
```python
109+
import typing as t
110+
111+
from pydantic.fields import ModelField
112+
from ellar.core.context import IExecutionContext
113+
from ellar.core.response import Response
114+
115+
116+
class IResponseModel:
117+
media_type: str
118+
description: str
119+
get_model_field: t.Callable[..., t.Optional[t.Union[ModelField, t.Any]]]
120+
create_response: t.Callable[[IExecutionContext, t.Any], Response]
121+
```
122+
Properties Overview:
123+
124+
- `media_type`: Read from response media type. **Required**
125+
- `description`: For documentation purpose. Default: `Success Response`. **Optional**
126+
- `get_model_field`: returns response schema if any. **Optional**
127+
- `create_response`: returns a response for the client. **Optional**
128+
129+
There is also a `BaseResponseModel` concrete class for more generic implementation.
130+
And its adds extra properties for configuration purposes.
131+
132+
They include:
133+
134+
- `response_type`: Response classes eg. JSONResponse, PlainResponse, HTMLResponse. etc. Default: `Response`. **Required**
135+
- `model_field_or_schema`: `Optional` property. For return data validation. Default: `None` **Optional**
136+
137+
138+
## Different Response Models
139+
Let's see different `ResponseModel` available in Ellar and how you can create one too.
140+
141+
### **ResponseModel**
142+
Response model that manages rendering of other response types.
143+
144+
- Location: `ellar.core.response.model.base.ResponseModel`
145+
- response_type: `Response`
146+
- model_field_or_schema: `None`
147+
- media_type: `text/plain`
148+
149+
### **JSONResponseModel**
150+
Response model that manages `JSON` response.
151+
152+
- Location: `ellar.core.response.model.json.JSONResponseModel`
153+
- response_type: `JSONResponse` OR `config.DEFAULT_JSON_CLASS`
154+
- model_field_or_schema: `Required`
155+
- media_type: `application/json`
156+
157+
### **HTMLResponseModel**
158+
Response model that manages `HTML` templating response. see [`@render`]() decorator.
159+
160+
- Location: `ellar.core.response.model.html.HTMLResponseModel`
161+
- response_type: `TemplateResponse`
162+
- model_field_or_schema: `None`
163+
- media_type: `text/html`
164+
165+
166+
### **FileResponseModel**
167+
Response model that manages `FILE` response. see [`@file`]() decorator.
168+
169+
- Location: `ellar.core.response.model.file.FileResponseModel`
170+
- response_type: `FileResponse`
171+
- model_field_or_schema: `None`
172+
- media_type: `Required`
173+
174+
175+
### **StreamingResponseModel**
176+
Response model that manages `STREAMING` response. see [`@file`]() decorator.
177+
178+
- Location: `ellar.core.response.model.file.StreamingResponseModel`
179+
- response_type: `StreamingResponse`
180+
- model_field_or_schema: `None`
181+
- media_type: `Required`
182+
183+
184+
### **EmptyAPIResponseModel**
185+
Default `ResponseModel` applied when no response is defined.
186+
187+
- Location: `ellar.core.response.model.html.EmptyAPIResponseModel`
188+
- response_type: `JSONResponse` OR `config.DEFAULT_JSON_CLASS`
189+
- model_field_or_schema: `dict`
190+
- media_type: `application/json`
191+
192+
## Custom Response Model
193+
194+
Lets create a new JSON response model.
195+
196+
```python
197+
# project_name/apps/items/controllers.py
198+
199+
import typing as t
200+
from ellar.common import Controller, get
201+
from ellar.core import ControllerBase
202+
from ellar.core.response.model.base import ResponseModel
203+
from ellar.core.response import JSONResponse
204+
from dataclasses import dataclass
205+
206+
207+
@dataclass
208+
class NoteSchema:
209+
id: t.Union[int, None]
210+
text: str
211+
completed: bool
212+
213+
214+
class JsonApiResponse(JSONResponse):
215+
media_type = "application/vnd.api+json"
216+
217+
218+
class JsonApiResponseModel(ResponseModel):
219+
response_type = JsonApiResponse
220+
model_field_or_schema = t.List[NoteSchema]
221+
default_description = 'Successful JsonAPI Response'
222+
223+
224+
@Controller
225+
class ItemsController(ControllerBase):
226+
@get("/notes/", response=JsonApiResponseModel())
227+
def get_notes(self):
228+
return [
229+
dict(id=1, text='My Json Api Response 1', completed=True),
230+
dict(id=2, text='My Json Api Response 2', completed=True),
231+
]
232+
```
233+
234+
![JsonApiResponseModel Image](../img/json_api_response_model.png)

docs/handling-response/response.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Handling Responses
2+
3+
## Define a response Schema
4+
5+
**Ellar** allows you to define the schema of your responses both for validation and documentation purposes.
6+
7+
The response schema is defined on the HTTP method decorator and its applied
8+
to OPENAPI documentation and validation of the data returned from route handler function.
9+
10+
We'll create a third operation that will return information about a Fake user.
11+
12+
```python
13+
# project_name/apps/items/controllers.py
14+
15+
from ellar.serializer import Serializer
16+
from ellar.common import Controller, get
17+
from ellar.core import ControllerBase
18+
19+
class User:
20+
def __init__(self, username: str, email:str=None, first_name:str=None, last_name:str=None) -> None:
21+
self.username = username
22+
self.email = email
23+
self.first_name = first_name
24+
self.last_name = last_name
25+
self.is_authenticated = False
26+
27+
@property
28+
def full_name(self) -> str:
29+
assert self.first_name and self.last_name
30+
return f'{self.first_name} {self.last_name}'
31+
32+
33+
class UserSchema(Serializer):
34+
username: str
35+
email: str = None
36+
first_name: str = None
37+
last_name: str = None
38+
39+
40+
current_user = User(username='ellar', email='ellar@example.com', first_name='ellar', last_name='asgi')
41+
42+
43+
@Controller
44+
class ItemsController(ControllerBase):
45+
@get("/me", response=UserSchema)
46+
def me(self):
47+
return current_user
48+
```
49+
50+
This will convert the `User` object into a dictionary of only the defined fields.
51+
52+
### Multiple Response Types
53+
54+
The `response` parameter takes different shape. Let's see how to return a different response if the user is not authenticated.
55+
56+
```python
57+
# project_name/apps/items/controllers.py
58+
59+
from ellar.serializer import Serializer
60+
from ellar.common import Controller, get
61+
from ellar.core import ControllerBase
62+
63+
class User:
64+
def __init__(self, username: str, email:str=None, first_name:str=None, last_name:str=None) -> None:
65+
self.username = username
66+
self.email = email
67+
self.first_name = first_name
68+
self.last_name = last_name
69+
self.is_authenticated = False
70+
71+
@property
72+
def full_name(self) -> str:
73+
assert self.first_name and self.last_name
74+
return f'{self.first_name} {self.last_name}'
75+
76+
77+
class UserSchema(Serializer):
78+
username: str
79+
email: str = None
80+
first_name: str = None
81+
last_name: str = None
82+
83+
84+
class MessageSchema(Serializer):
85+
message: str
86+
87+
88+
current_user = User(username='ellar', email='ellar@example.com', first_name='ellar', last_name='asgi')
89+
90+
91+
@Controller
92+
class ItemsController(ControllerBase):
93+
@get("/me", response={200: UserSchema, 403: MessageSchema})
94+
def me(self):
95+
if not current_user.is_authenticated:
96+
return 403, {"message": "Please sign in first"}
97+
return current_user
98+
99+
@get("/login", response=MessageSchema)
100+
def login(self):
101+
if current_user.is_authenticated:
102+
return dict(message=f'{current_user.full_name} already logged in.')
103+
104+
current_user.is_authenticated = True
105+
return MessageSchema(
106+
message=f'{current_user.full_name} logged in successfully.'
107+
)
108+
# the same as returning dict(message=f'{current_user.full_name} logged in successfully.')
109+
```
110+
111+
Here, the `response` parameter takes a KeyValuePair of the `status` and response `Schema`.
112+
113+
!!! info
114+
Note that we returned a tuple of status code and response data (`403, {"message": "Please sign in first"}`) to specify the response validation to use.
115+
116+
117+
## Using Response Type/Object As Response
118+
119+
You can use `Response` type to change the format of data returned from endpoint functions.
120+
121+
```python
122+
# project_name/apps/items/controllers.py
123+
124+
from ellar.common import Controller, get
125+
from ellar.core import ControllerBase
126+
from starlette.responses import PlainTextResponse
127+
128+
129+
@Controller
130+
class ItemsController(ControllerBase):
131+
@get("/me", response=PlainTextResponse)
132+
def me(self):
133+
return "some text response."
134+
135+
```
136+
137+
Also, we can return response object from endpoint functions, and it will override initial `response` declared before.
138+
139+
```python
140+
# project_name/apps/items/controllers.py
141+
142+
from ellar.serializer import Serializer
143+
from ellar.common import Controller, get
144+
from ellar.core import ControllerBase
145+
from starlette.responses import PlainTextResponse
146+
147+
class UserSchema(Serializer):
148+
username: str
149+
email: str = None
150+
first_name: str = None
151+
last_name: str = None
152+
153+
154+
@Controller
155+
class ItemsController(ControllerBase):
156+
@get("/me", response=UserSchema)
157+
def me(self):
158+
return PlainTextResponse("some text response.", status_code=200)
159+
```

docs/handling-responses/response-schema.md

Whitespace-only changes.

0 commit comments

Comments
 (0)