1use super::pagination::PaginationInfo;
2use chrono::{DateTime, Utc};
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5
6#[cfg(feature = "web")]
7use axum::Json;
8#[cfg(feature = "web")]
9use axum::http::StatusCode;
10#[cfg(feature = "web")]
11use axum::response::IntoResponse;
12#[cfg(feature = "web")]
13use http::header;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ResponseLinks {
17 pub self_link: String,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub next: Option<String>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub prev: Option<String>,
24
25 pub docs: String,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ResponseMeta {
30 pub timestamp: DateTime<Utc>,
31
32 pub version: String,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub pagination: Option<PaginationInfo>,
36}
37
38impl ResponseMeta {
39 pub fn new() -> Self {
40 Self {
41 timestamp: Utc::now(),
42 version: "1.0.0".to_string(),
43 pagination: None,
44 }
45 }
46
47 pub fn with_pagination(mut self, pagination: PaginationInfo) -> Self {
48 self.pagination = Some(pagination);
49 self
50 }
51}
52
53impl Default for ResponseMeta {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59#[derive(Debug, Serialize, Deserialize)]
60pub struct ApiResponse<T>
61where
62 T: 'static,
63{
64 pub data: T,
65
66 pub meta: ResponseMeta,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub links: Option<ResponseLinks>,
70}
71
72impl<T: Serialize + 'static> ApiResponse<T> {
73 pub fn new(data: T) -> Self {
74 Self {
75 data,
76 meta: ResponseMeta::new(),
77 links: None,
78 }
79 }
80
81 pub fn with_links(mut self, links: ResponseLinks) -> Self {
82 self.links = Some(links);
83 self
84 }
85
86 pub fn with_meta(mut self, meta: ResponseMeta) -> Self {
87 self.meta = meta;
88 self
89 }
90}
91
92#[derive(Debug, Serialize, Deserialize)]
93pub struct SingleResponse<T>
94where
95 T: 'static,
96{
97 pub data: T,
98
99 pub meta: ResponseMeta,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub links: Option<ResponseLinks>,
103}
104
105impl<T: Serialize + 'static> SingleResponse<T> {
106 pub fn new(data: T) -> Self {
107 Self {
108 data,
109 meta: ResponseMeta::new(),
110 links: None,
111 }
112 }
113
114 pub const fn with_meta(data: T, meta: ResponseMeta) -> Self {
115 Self {
116 data,
117 meta,
118 links: None,
119 }
120 }
121
122 pub fn with_links(mut self, links: ResponseLinks) -> Self {
123 self.links = Some(links);
124 self
125 }
126}
127
128#[derive(Debug, Serialize, Deserialize)]
129pub struct CollectionResponse<T>
130where
131 T: 'static,
132{
133 pub data: Vec<T>,
134
135 pub meta: ResponseMeta,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub links: Option<ResponseLinks>,
139}
140
141impl<T: Serialize + 'static> CollectionResponse<T> {
142 pub fn new(data: Vec<T>) -> Self {
143 Self {
144 data,
145 meta: ResponseMeta::new(),
146 links: None,
147 }
148 }
149
150 pub fn paginated(data: Vec<T>, pagination: PaginationInfo) -> Self {
151 Self {
152 data,
153 meta: ResponseMeta::new().with_pagination(pagination),
154 links: None,
155 }
156 }
157
158 pub fn with_links(mut self, links: ResponseLinks) -> Self {
159 self.links = Some(links);
160 self
161 }
162}
163
164#[derive(Debug, Serialize, Deserialize)]
165pub struct SuccessResponse {
166 pub message: String,
167
168 pub meta: ResponseMeta,
169}
170
171impl SuccessResponse {
172 pub fn new(message: impl Into<String>) -> Self {
173 Self {
174 message: message.into(),
175 meta: ResponseMeta::new(),
176 }
177 }
178}
179
180#[derive(Debug, Serialize, Deserialize)]
181pub struct CreatedResponse<T>
182where
183 T: 'static,
184{
185 pub data: T,
186
187 pub meta: ResponseMeta,
188
189 pub location: String,
190}
191
192impl<T: Serialize + 'static> CreatedResponse<T> {
193 pub fn new(data: T, location: impl Into<String>) -> Self {
194 Self {
195 data,
196 meta: ResponseMeta::new(),
197 location: location.into(),
198 }
199 }
200}
201
202#[derive(Debug, Serialize, Deserialize)]
203pub struct AcceptedResponse {
204 pub message: String,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub job_id: Option<String>,
208
209 #[serde(skip_serializing_if = "Option::is_none")]
210 pub status_url: Option<String>,
211
212 pub meta: ResponseMeta,
213}
214
215impl AcceptedResponse {
216 pub fn new(message: impl Into<String>) -> Self {
217 Self {
218 message: message.into(),
219 job_id: None,
220 status_url: None,
221 meta: ResponseMeta::new(),
222 }
223 }
224
225 pub fn with_job(mut self, job_id: impl Into<String>, status_url: impl Into<String>) -> Self {
226 self.job_id = Some(job_id.into());
227 self.status_url = Some(status_url.into());
228 self
229 }
230}
231
232#[derive(Debug, Serialize, Deserialize)]
233pub struct Link {
234 pub href: String,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub title: Option<String>,
237}
238
239impl Link {
240 pub fn new(href: impl Into<String>, title: Option<String>) -> Self {
241 Self {
242 href: href.into(),
243 title,
244 }
245 }
246}
247
248#[derive(Debug, Serialize, Deserialize)]
249pub struct DiscoveryResponse<T>
250where
251 T: 'static,
252{
253 pub data: T,
254 pub meta: ResponseMeta,
255 #[serde(rename = "_links")]
256 pub links: IndexMap<String, Link>,
257}
258
259impl<T: Serialize + 'static> DiscoveryResponse<T> {
260 pub fn new(data: T, links: IndexMap<String, Link>) -> Self {
261 Self {
262 data,
263 meta: ResponseMeta::new(),
264 links,
265 }
266 }
267
268 pub fn with_meta(mut self, meta: ResponseMeta) -> Self {
269 self.meta = meta;
270 self
271 }
272}
273
274#[cfg(feature = "web")]
275impl<T: Serialize + 'static> IntoResponse for SingleResponse<T> {
276 fn into_response(self) -> axum::response::Response {
277 (StatusCode::OK, Json(self)).into_response()
278 }
279}
280
281#[cfg(feature = "web")]
282impl<T: Serialize + 'static> IntoResponse for CollectionResponse<T> {
283 fn into_response(self) -> axum::response::Response {
284 (StatusCode::OK, Json(self)).into_response()
285 }
286}
287
288#[cfg(feature = "web")]
289impl IntoResponse for SuccessResponse {
290 fn into_response(self) -> axum::response::Response {
291 (StatusCode::OK, Json(self)).into_response()
292 }
293}
294
295#[cfg(feature = "web")]
296impl<T: Serialize + 'static> IntoResponse for CreatedResponse<T> {
297 fn into_response(self) -> axum::response::Response {
298 (
299 StatusCode::CREATED,
300 [("Location", self.location.clone())],
301 Json(self),
302 )
303 .into_response()
304 }
305}
306
307#[cfg(feature = "web")]
308impl IntoResponse for AcceptedResponse {
309 fn into_response(self) -> axum::response::Response {
310 (StatusCode::ACCEPTED, Json(self)).into_response()
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct MarkdownFrontmatter {
316 pub title: String,
317 pub slug: String,
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub description: Option<String>,
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub author: Option<String>,
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub published_at: Option<String>,
324 #[serde(default, skip_serializing_if = "Vec::is_empty")]
325 pub tags: Vec<String>,
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub url: Option<String>,
328}
329
330impl MarkdownFrontmatter {
331 pub fn new(title: impl Into<String>, slug: impl Into<String>) -> Self {
332 Self {
333 title: title.into(),
334 slug: slug.into(),
335 description: None,
336 author: None,
337 published_at: None,
338 tags: Vec::new(),
339 url: None,
340 }
341 }
342
343 pub fn with_description(mut self, description: impl Into<String>) -> Self {
344 self.description = Some(description.into());
345 self
346 }
347
348 pub fn with_author(mut self, author: impl Into<String>) -> Self {
349 self.author = Some(author.into());
350 self
351 }
352
353 pub fn with_published_at(mut self, published_at: impl Into<String>) -> Self {
354 self.published_at = Some(published_at.into());
355 self
356 }
357
358 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
359 self.tags = tags;
360 self
361 }
362
363 pub fn with_url(mut self, url: impl Into<String>) -> Self {
364 self.url = Some(url.into());
365 self
366 }
367
368 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
369 serde_yaml::to_string(self)
370 }
371}
372
373#[derive(Debug, Clone)]
374pub struct MarkdownResponse {
375 pub frontmatter: MarkdownFrontmatter,
376 pub body: String,
377}
378
379impl MarkdownResponse {
380 pub fn new(frontmatter: MarkdownFrontmatter, body: impl Into<String>) -> Self {
381 Self {
382 frontmatter,
383 body: body.into(),
384 }
385 }
386
387 pub fn to_markdown(&self) -> String {
388 let yaml = self.frontmatter.to_yaml().unwrap_or_default();
389 format!("---\n{}---\n\n{}", yaml, self.body)
390 }
391}
392
393#[cfg(feature = "web")]
394impl IntoResponse for MarkdownResponse {
395 fn into_response(self) -> axum::response::Response {
396 let body = self.to_markdown();
397 (
398 StatusCode::OK,
399 [(header::CONTENT_TYPE, "text/markdown; charset=utf-8")],
400 body,
401 )
402 .into_response()
403 }
404}