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::http::StatusCode;
8#[cfg(feature = "web")]
9use axum::response::IntoResponse;
10#[cfg(feature = "web")]
11use axum::Json;
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    pub job_id: Option<String>,
207
208    pub status_url: Option<String>,
209
210    pub meta: ResponseMeta,
211}
212
213impl AcceptedResponse {
214    pub fn new(message: impl Into<String>) -> Self {
215        Self {
216            message: message.into(),
217            job_id: None,
218            status_url: None,
219            meta: ResponseMeta::new(),
220        }
221    }
222
223    pub fn with_job(mut self, job_id: impl Into<String>, status_url: impl Into<String>) -> Self {
224        self.job_id = Some(job_id.into());
225        self.status_url = Some(status_url.into());
226        self
227    }
228}
229
230#[derive(Debug, Serialize, Deserialize)]
231pub struct Link {
232    pub href: String,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub title: Option<String>,
235}
236
237impl Link {
238    pub fn new(href: impl Into<String>, title: Option<String>) -> Self {
239        Self {
240            href: href.into(),
241            title,
242        }
243    }
244}
245
246#[derive(Debug, Serialize, Deserialize)]
247pub struct DiscoveryResponse<T>
248where
249    T: 'static,
250{
251    pub data: T,
252    pub meta: ResponseMeta,
253    #[serde(rename = "_links")]
254    pub links: IndexMap<String, Link>,
255}
256
257impl<T: Serialize + 'static> DiscoveryResponse<T> {
258    pub fn new(data: T, links: IndexMap<String, Link>) -> Self {
259        Self {
260            data,
261            meta: ResponseMeta::new(),
262            links,
263        }
264    }
265
266    pub fn with_meta(mut self, meta: ResponseMeta) -> Self {
267        self.meta = meta;
268        self
269    }
270}
271
272#[cfg(feature = "web")]
273impl<T: Serialize + 'static> IntoResponse for SingleResponse<T> {
274    fn into_response(self) -> axum::response::Response {
275        (StatusCode::OK, Json(self)).into_response()
276    }
277}
278
279#[cfg(feature = "web")]
280impl<T: Serialize + 'static> IntoResponse for CollectionResponse<T> {
281    fn into_response(self) -> axum::response::Response {
282        (StatusCode::OK, Json(self)).into_response()
283    }
284}
285
286#[cfg(feature = "web")]
287impl IntoResponse for SuccessResponse {
288    fn into_response(self) -> axum::response::Response {
289        (StatusCode::OK, Json(self)).into_response()
290    }
291}
292
293#[cfg(feature = "web")]
294impl<T: Serialize + 'static> IntoResponse for CreatedResponse<T> {
295    fn into_response(self) -> axum::response::Response {
296        (
297            StatusCode::CREATED,
298            [("Location", self.location.clone())],
299            Json(self),
300        )
301            .into_response()
302    }
303}
304
305#[cfg(feature = "web")]
306impl IntoResponse for AcceptedResponse {
307    fn into_response(self) -> axum::response::Response {
308        (StatusCode::ACCEPTED, Json(self)).into_response()
309    }
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct MarkdownFrontmatter {
314    pub title: String,
315    pub slug: String,
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub description: Option<String>,
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub author: Option<String>,
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub published_at: Option<String>,
322    #[serde(default, skip_serializing_if = "Vec::is_empty")]
323    pub tags: Vec<String>,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub url: Option<String>,
326}
327
328impl MarkdownFrontmatter {
329    pub fn new(title: impl Into<String>, slug: impl Into<String>) -> Self {
330        Self {
331            title: title.into(),
332            slug: slug.into(),
333            description: None,
334            author: None,
335            published_at: None,
336            tags: Vec::new(),
337            url: None,
338        }
339    }
340
341    pub fn with_description(mut self, description: impl Into<String>) -> Self {
342        self.description = Some(description.into());
343        self
344    }
345
346    pub fn with_author(mut self, author: impl Into<String>) -> Self {
347        self.author = Some(author.into());
348        self
349    }
350
351    pub fn with_published_at(mut self, published_at: impl Into<String>) -> Self {
352        self.published_at = Some(published_at.into());
353        self
354    }
355
356    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
357        self.tags = tags;
358        self
359    }
360
361    pub fn with_url(mut self, url: impl Into<String>) -> Self {
362        self.url = Some(url.into());
363        self
364    }
365
366    pub fn to_yaml(&self) -> String {
367        serde_yaml::to_string(self).unwrap_or_default()
368    }
369}
370
371#[derive(Debug, Clone)]
372pub struct MarkdownResponse {
373    pub frontmatter: MarkdownFrontmatter,
374    pub body: String,
375}
376
377impl MarkdownResponse {
378    pub fn new(frontmatter: MarkdownFrontmatter, body: impl Into<String>) -> Self {
379        Self {
380            frontmatter,
381            body: body.into(),
382        }
383    }
384
385    pub fn to_markdown(&self) -> String {
386        format!("---\n{}---\n\n{}", self.frontmatter.to_yaml(), self.body)
387    }
388}
389
390#[cfg(feature = "web")]
391impl IntoResponse for MarkdownResponse {
392    fn into_response(self) -> axum::response::Response {
393        let body = self.to_markdown();
394        (
395            StatusCode::OK,
396            [(header::CONTENT_TYPE, "text/markdown; charset=utf-8")],
397            body,
398        )
399            .into_response()
400    }
401}