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)]
59pub struct PaginatedResponse<T> {
60 #[serde(skip_serializing_if = "PaginationMeta::is_empty")]
62 #[schemars(with = "Option<PaginationMeta>")]
63 meta: PaginationMeta,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
67 data: Option<Vec<SingleResource<T>>>,
68
69 links: PaginationLinks,
71}
72
73fn url_with_pagination(base: &str, pagination: Pagination) -> String {
74 let (path, query) = base.split_once('?').unwrap_or((base, ""));
75 let mut query = query.to_owned();
76
77 if let Some(before) = pagination.before {
78 query = format!("{query}&page[before]={before}");
79 }
80
81 if let Some(after) = pagination.after {
82 query = format!("{query}&page[after]={after}");
83 }
84
85 let count = pagination.count;
86 match pagination.direction {
87 mas_storage::pagination::PaginationDirection::Forward => {
88 query = format!("{query}&page[first]={count}");
89 }
90 mas_storage::pagination::PaginationDirection::Backward => {
91 query = format!("{query}&page[last]={count}");
92 }
93 }
94
95 let query = query.trim_start_matches('&');
97
98 format!("{path}?{query}")
99}
100
101impl<T: Resource> PaginatedResponse<T> {
102 pub fn for_page(
103 page: mas_storage::Page<T>,
104 current_pagination: Pagination,
105 count: Option<usize>,
106 base: &str,
107 ) -> Self {
108 let links = PaginationLinks {
109 self_: url_with_pagination(base, current_pagination),
110 first: Some(url_with_pagination(
111 base,
112 Pagination::first(current_pagination.count),
113 )),
114 last: Some(url_with_pagination(
115 base,
116 Pagination::last(current_pagination.count),
117 )),
118 next: page.has_next_page.then(|| {
119 url_with_pagination(
120 base,
121 current_pagination
122 .clear_before()
123 .after(page.edges.last().unwrap().cursor),
124 )
125 }),
126 prev: if page.has_previous_page {
127 Some(url_with_pagination(
128 base,
129 current_pagination
130 .clear_after()
131 .before(page.edges.first().unwrap().cursor),
132 ))
133 } else {
134 None
135 },
136 };
137
138 let data = page
139 .edges
140 .into_iter()
141 .map(SingleResource::from_edge)
142 .collect();
143
144 Self {
145 meta: PaginationMeta { count },
146 data: Some(data),
147 links,
148 }
149 }
150
151 pub fn for_count_only(count: usize, base: &str) -> Self {
152 let links = PaginationLinks {
153 self_: base.to_owned(),
154 first: None,
155 last: None,
156 next: None,
157 prev: None,
158 };
159
160 Self {
161 meta: PaginationMeta { count: Some(count) },
162 data: None,
163 links,
164 }
165 }
166}
167
168#[derive(Serialize, JsonSchema)]
170struct SingleResource<T> {
171 #[serde(rename = "type")]
173 type_: &'static str,
174
175 #[schemars(with = "super::schema::Ulid")]
177 id: Ulid,
178
179 attributes: T,
181
182 links: SelfLinks,
184
185 #[serde(skip_serializing_if = "SingleResourceMeta::is_empty")]
187 #[schemars(with = "Option<SingleResourceMeta>")]
188 meta: SingleResourceMeta,
189}
190
191#[derive(Serialize, JsonSchema)]
193struct SingleResourceMeta {
194 #[serde(skip_serializing_if = "Option::is_none")]
196 page: Option<SingleResourceMetaPage>,
197}
198
199impl SingleResourceMeta {
200 fn is_empty(&self) -> bool {
201 self.page.is_none()
202 }
203}
204
205#[derive(Serialize, JsonSchema)]
207struct SingleResourceMetaPage {
208 cursor: String,
210}
211
212impl<T: Resource> SingleResource<T> {
213 fn new(resource: T) -> Self {
214 let self_ = resource.path();
215 Self {
216 type_: T::KIND,
217 id: resource.id(),
218 attributes: resource,
219 links: SelfLinks { self_ },
220 meta: SingleResourceMeta { page: None },
221 }
222 }
223
224 fn from_edge<C: ToString>(edge: Edge<T, C>) -> Self {
225 let cursor = edge.cursor.to_string();
226 let mut resource = Self::new(edge.node);
227 resource.meta.page = Some(SingleResourceMetaPage { cursor });
228 resource
229 }
230}
231
232#[derive(Serialize, JsonSchema)]
234struct SelfLinks {
235 #[serde(rename = "self")]
237 self_: String,
238}
239
240#[derive(Serialize, JsonSchema)]
242pub struct SingleResponse<T> {
243 data: SingleResource<T>,
244 links: SelfLinks,
245}
246
247impl<T: Resource> SingleResponse<T> {
248 pub fn new(resource: T, self_: String) -> Self {
250 Self {
251 data: SingleResource::new(resource),
252 links: SelfLinks { self_ },
253 }
254 }
255
256 pub fn new_canonical(resource: T) -> Self {
258 let self_ = resource.path();
259 Self::new(resource, self_)
260 }
261}
262
263#[derive(Serialize, JsonSchema)]
265struct Error {
266 title: String,
268}
269
270impl Error {
271 fn from_error(error: &(dyn std::error::Error + 'static)) -> Self {
272 Self {
273 title: error.to_string(),
274 }
275 }
276}
277
278#[derive(Serialize, JsonSchema)]
280pub struct ErrorResponse {
281 errors: Vec<Error>,
283}
284
285impl ErrorResponse {
286 pub fn from_error(error: &(dyn std::error::Error + 'static)) -> Self {
288 let mut errors = Vec::new();
289 let mut head = Some(error);
290 while let Some(error) = head {
291 errors.push(Error::from_error(error));
292 head = error.source();
293 }
294 Self { errors }
295 }
296}