-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi_ext.rs
More file actions
187 lines (150 loc) · 6.6 KB
/
Copy pathapi_ext.rs
File metadata and controls
187 lines (150 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/// See comments in shared/api
///
/// Bottom line: once an api endpoint is defined and implemented there, the frontend
/// automatically gets the typechecked synchronization for free
///
/// In the backend here, we need to implement the specific extension trait for each api endpoint
/// such that it binds the request and response type to eachother for each route
///
/// However, it's a generic pattern, and could be abstracted into a macro too
/// the key is defining the associated types like:
///
/// ```rust
/// type Req = <Foo as ApiBoth>::Req;
/// type Res = <Foo as ApiBoth>::Res;
/// ```
///
/// (adjusting for the specific struct and trait ofc)
///
/// then, the functions can take concrete types for readability - but it's typechecked
/// and constrained to make sure the request and response types are in sync
///
/// the specific part which can't be generalized is the actual handling logic, of course :)
///
/// Each of the api endpoints are implemented in the corresponding handler.rs
/// in this file we're just defining the traits
///
/// Only *one* of the traits should be implemented for each api endpoint
use async_trait::async_trait;
use serde::{de::DeserializeOwned, Serialize};
use shared::backend::{result::ApiResult, worker::{RequestExt, ResponseExt}};
use web_sys::Response;
use crate::{ApiContext, ApiResponse};
#[async_trait(?Send)]
pub trait ApiBothExt {
type Req: DeserializeOwned;
type Res: Serialize;
// this is just called from the router... don't override
async fn router(ctx: ApiContext) -> ApiResponse {
let request_data = ctx.req.try_from_json::<Self::Req>().await?;
let response_data = Self::handle(&ctx, request_data).await?;
Ok(Self::response(&ctx, response_data))
}
// override this for main logic getting from a request to a response data
async fn handle(ctx: &ApiContext, req: Self::Req) -> ApiResult<Self::Res>;
// and finally, override this to modify the response before returning
// by default it will just return as json
fn response(_ctx: &ApiContext, res: Self::Res) -> Response {
Response::new_json(&res)
}
}
#[async_trait(?Send)]
pub trait ApiResExt {
type Res: Serialize;
// this is just called from the router... don't override
async fn router(ctx: ApiContext) -> ApiResponse {
let response_data = Self::handle(&ctx).await?;
Ok(Self::response(&ctx, response_data))
}
// override this for main logic to get a response data
async fn handle(ctx: &ApiContext) -> ApiResult<Self::Res>;
// and finally, override this to modify the response before returning
// by default it will just return as json
fn response(_ctx: &ApiContext, res: Self::Res) -> Response {
Response::new_json(&res)
}
}
#[async_trait(?Send)]
pub trait ApiReqExt {
type Req: DeserializeOwned;
// this is just called from the router... don't override
async fn router(ctx: ApiContext) -> ApiResponse {
let request_data = ctx.req.try_from_json::<Self::Req>().await?;
let _ = Self::handle(&ctx, request_data).await?;
Ok(Self::response(&ctx))
}
// override this for main logic handling the request data
async fn handle(ctx: &ApiContext, req: Self::Req) -> ApiResult<()>;
// and finally, override this to modify the response before returning
// by default it will just return empty
fn response(_ctx: &ApiContext) -> Response {
Response::new_empty()
}
}
#[async_trait(?Send)]
pub trait ApiEmptyExt {
// this is just called from the router... don't override
async fn router(ctx: ApiContext) -> ApiResponse {
let _ = Self::handle(&ctx).await?;
Ok(Self::response(&ctx))
}
// override this for main logic handling the request
async fn handle(ctx: &ApiContext) -> ApiResult<()>;
// and finally, override this to modify the response before returning
// by default it will just return empty
fn response(_ctx: &ApiContext) -> Response {
Response::new_empty()
}
}
// rarely used, extends ApiBoth and allows passing Extra data from the handle
// useful for dealing with cookies in the response when it's not derived
// from the response data
#[async_trait(?Send)]
pub trait ApiBothWithExtraExt {
type Req: DeserializeOwned;
type Res: Serialize;
type Extra;
// this is just called from the router... don't override
async fn router(ctx: ApiContext) -> ApiResponse {
let request_data = ctx.req.try_from_json::<Self::Req>().await?;
let (response_data, extra) = Self::handle(&ctx, request_data).await?;
Ok(Self::response(&ctx, response_data, extra))
}
// override this for main logic getting from a request to a response
async fn handle(ctx: &ApiContext, req: Self::Req) -> ApiResult<(Self::Res, Self::Extra)>;
// and finally, override this to modify the response before returning
fn response(ctx: &ApiContext, res: Self::Res, extra: Self::Extra) -> Response;
}
// rarely used, extends ApiResDynRoute and allows passing Extra data from the handle
// useful for dealing with cookies in the response when it's not derived
// from the response data *and* where the route is dynamic (phew)
#[async_trait(?Send)]
pub trait ApiResDynRouteWithExtraExt {
type Res: Serialize;
type Extra;
// this is just called from the router... don't override
async fn router(&self, ctx: ApiContext) -> ApiResponse {
let (response_data, extra) = self.handle(&ctx).await?;
Ok(self.response(&ctx, response_data, extra))
}
// override this for main logic getting from a request to a response data
async fn handle(&self, ctx: &ApiContext) -> ApiResult<(Self::Res, Self::Extra)>;
// and finally, override this to modify the response before returning
fn response(&self, ctx: &ApiContext, res: Self::Res, extra: Self::Extra) -> Response;
}
// rarely used, extends ApiEmptyDynRoute and allows passing Extra data from the handle
// useful for dealing with cookies in the response when it's not derived
// from the response data *and* where the route is dynamic (phew)
#[async_trait(?Send)]
pub trait ApiEmptyDynRouteWithExtraExt {
type Extra;
// this is just called from the router... don't override
async fn router(&self, ctx: ApiContext) -> ApiResponse {
let extra = self.handle(&ctx).await?;
Ok(self.response(&ctx, extra))
}
// override this for main logic getting from a request to a response data
async fn handle(&self, ctx: &ApiContext) -> ApiResult<Self::Extra>;
// and finally, override this to modify the response before returning
fn response(&self, ctx: &ApiContext, extra: Self::Extra) -> Response;
}