Skip to main content

systemprompt_models/api/
responses.rs

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}