mas_handlers/admin/
response.rs1#![allow(clippy::module_name_repetitions)]
8
9use mas_storage::{Pagination, pagination::Edge};
10use schemars::JsonSchema;
11use serde::Serialize;
12use ulid::Ulid;
13
14use super::model::Resource;
15
16#[derive(Serialize, JsonSchema)]
18struct PaginationLinks {
19 #[serde(rename = "self")]
21 self_: String,
22
23 #[serde(skip_serializing_if = "Option::is_none")]
25 first: Option<String>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 last: Option<String>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
35 next: Option<String>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
41 prev: Option<String>,
42}
43
44#[derive(Serialize, JsonSchema)]
45struct PaginationMeta {
46 #[serde(skip_serializing_if = "Option::is_none")]
48 count: Option<usize>,
49}
50
51impl PaginationMeta {
52 fn is_empty(&self) -> bool {
53 self.count.is_none()
54 }
55}
56
57#[derive(Serialize, JsonSchema)]
59#[schemars(rename = "PaginatedResponse_for_{T}")]
60pub struct PaginatedResponse<T> {
61 #[serde(skip_serializing_if = "PaginationMeta::is_empty")]
63 #[schemars(with = "Option<PaginationMeta>")]
64 meta: PaginationMeta,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 data: Option<Vec<SingleResource<T>>>,
69
70 links: PaginationLinks,
72}
73
74fn url_with_pagination(base: &str, pagination: Pagination) -> String {
75 let (path, query) = base.split_once('?').unwrap_or((base, ""));
76 let mut query = query.to_owned();
77
78 if let Some(before) = pagination.before {
79 query = format!("{query}&page[before]={before}");
80 }
81
82 if let Some(after) = pagination.after {
83 query = format!("{query}&page[after]={after}");
84 }
85
86 let count = pagination.count;
87 match pagination.direction {
88 mas_storage::pagination::PaginationDirection::Forward => {
89 query = format!("{query}&page[first]={count}");
90 }
91 mas_storage::pagination::PaginationDirection::Backward => {
92 query = format!("{query}&page[last]={count}");
93 }
94 }
95
96 let query = query.trim_start_matches('&');
98
99 format!("{path}?{query}")
100}
101
102impl<T: Resource> PaginatedResponse<T> {
103 pub fn for_page(
104 page: mas_storage::Page<T>,
105 current_pagination: Pagination,
106 count: Option<usize>,
107 base: &str,
108 ) -> Self {
109 let links = PaginationLinks {
110 self_: url_with_pagination(base, current_pagination),
111 first: Some(url_with_pagination(
112 base,
113 Pagination::first(current_pagination.count),
114 )),
115 last: Some(url_with_pagination(
116 base,
117 Pagination::last(current_pagination.count),
118 )),
119 next: page.has_next_page.then(|| {
120 url_with_pagination(
121 base,
122 current_pagination
123 .clear_before()
124 .after(page.edges.last().unwrap().cursor),
125 )
126 }),
127 prev: if page.has_previous_page {
128 Some(url_with_pagination(
129 base,
130 current_pagination
131 .clear_after()
132 .before(page.edges.first().unwrap().cursor),
133 ))
134 } else {
135 None
136 },
137 };
138
139 let data = page
140 .edges
141 .into_iter()
142 .map(SingleResource::from_edge)
143 .collect();
144
145 Self {
146 meta: PaginationMeta { count },
147 data: Some(data),
148 links,
149 }
150 }
151
152 pub fn for_count_only(count: usize, base: &str) -> Self {
153 let links = PaginationLinks {
154 self_: base.to_owned(),
155 first: None,
156 last: None,
157 next: None,
158 prev: None,
159 };
160
161 Self {
162 meta: PaginationMeta { count: Some(count) },
163 data: None,
164 links,
165 }
166 }
167}
168
169#[derive(Serialize, JsonSchema)]
171#[schemars(rename = "SingleResource_for_{T}")]
172struct SingleResource<T> {
173 #[serde(rename = "type")]
175 type_: &'static str,
176
177 #[schemars(with = "super::schema::Ulid")]
179 id: Ulid,
180
181 attributes: T,
183
184 links: SelfLinks,
186
187 #[serde(skip_serializing_if = "SingleResourceMeta::is_empty")]
189 #[schemars(with = "Option<SingleResourceMeta>")]
190 meta: SingleResourceMeta,
191}
192
193#[derive(Serialize, JsonSchema)]
195struct SingleResourceMeta {
196 #[serde(skip_serializing_if = "Option::is_none")]
198 page: Option<SingleResourceMetaPage>,
199}
200
201impl SingleResourceMeta {
202 fn is_empty(&self) -> bool {
203 self.page.is_none()
204 }
205}
206
207#[derive(Serialize, JsonSchema)]
209struct SingleResourceMetaPage {
210 cursor: String,
212}
213
214impl<T: Resource> SingleResource<T> {
215 fn new(resource: T) -> Self {
216 let self_ = resource.path();
217 Self {
218 type_: T::KIND,
219 id: resource.id(),
220 attributes: resource,
221 links: SelfLinks { self_ },
222 meta: SingleResourceMeta { page: None },
223 }
224 }
225
226 fn from_edge<C: ToString>(edge: Edge<T, C>) -> Self {
227 let cursor = edge.cursor.to_string();
228 let mut resource = Self::new(edge.node);
229 resource.meta.page = Some(SingleResourceMetaPage { cursor });
230 resource
231 }
232}
233
234#[derive(Serialize, JsonSchema)]
236struct SelfLinks {
237 #[serde(rename = "self")]
239 self_: String,
240}
241
242#[derive(Serialize, JsonSchema)]
244#[schemars(rename = "SingleResponse_for_{T}")]
245pub struct SingleResponse<T> {
246 data: SingleResource<T>,
247 links: SelfLinks,
248}
249
250impl<T: Resource> SingleResponse<T> {
251 pub fn new(resource: T, self_: String) -> Self {
253 Self {
254 data: SingleResource::new(resource),
255 links: SelfLinks { self_ },
256 }
257 }
258
259 pub fn new_canonical(resource: T) -> Self {
261 let self_ = resource.path();
262 Self::new(resource, self_)
263 }
264}
265
266#[derive(Serialize, JsonSchema)]
268struct Error {
269 title: String,
271}
272
273impl Error {
274 fn from_error(error: &(dyn std::error::Error + 'static)) -> Self {
275 Self {
276 title: error.to_string(),
277 }
278 }
279}
280
281#[derive(Serialize, JsonSchema)]
283pub struct ErrorResponse {
284 errors: Vec<Error>,
286}
287
288impl ErrorResponse {
289 pub fn from_error(error: &(dyn std::error::Error + 'static)) -> Self {
291 let mut errors = Vec::new();
292 let mut head = Some(error);
293 while let Some(error) = head {
294 errors.push(Error::from_error(error));
295 head = error.source();
296 }
297 Self { errors }
298 }
299}