Skip to main content

systemprompt_models/api/responses/
markdown.rs

1//! Markdown response with structured YAML frontmatter for rendering
2//! authored content over the API.
3
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 http::header;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct MarkdownFrontmatter {
15    pub title: String,
16    pub slug: String,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub description: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub author: Option<String>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub published_at: Option<String>,
23    #[serde(default, skip_serializing_if = "Vec::is_empty")]
24    pub tags: Vec<String>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub url: Option<String>,
27}
28
29impl MarkdownFrontmatter {
30    pub fn new(title: impl Into<String>, slug: impl Into<String>) -> Self {
31        Self {
32            title: title.into(),
33            slug: slug.into(),
34            description: None,
35            author: None,
36            published_at: None,
37            tags: Vec::new(),
38            url: None,
39        }
40    }
41
42    #[must_use]
43    pub fn with_description(mut self, description: impl Into<String>) -> Self {
44        self.description = Some(description.into());
45        self
46    }
47
48    #[must_use]
49    pub fn with_author(mut self, author: impl Into<String>) -> Self {
50        self.author = Some(author.into());
51        self
52    }
53
54    #[must_use]
55    pub fn with_published_at(mut self, published_at: impl Into<String>) -> Self {
56        self.published_at = Some(published_at.into());
57        self
58    }
59
60    #[must_use]
61    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
62        self.tags = tags;
63        self
64    }
65
66    #[must_use]
67    pub fn with_url(mut self, url: impl Into<String>) -> Self {
68        self.url = Some(url.into());
69        self
70    }
71
72    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
73        serde_yaml::to_string(self)
74    }
75}
76
77#[derive(Debug, Clone)]
78pub struct MarkdownResponse {
79    pub frontmatter: MarkdownFrontmatter,
80    pub body: String,
81}
82
83impl MarkdownResponse {
84    pub fn new(frontmatter: MarkdownFrontmatter, body: impl Into<String>) -> Self {
85        Self {
86            frontmatter,
87            body: body.into(),
88        }
89    }
90
91    pub fn to_markdown(&self) -> Result<String, serde_yaml::Error> {
92        let yaml = self.frontmatter.to_yaml()?;
93        Ok(format!("---\n{}---\n\n{}", yaml, self.body))
94    }
95}
96
97#[cfg(feature = "web")]
98impl IntoResponse for MarkdownResponse {
99    fn into_response(self) -> axum::response::Response {
100        match self.to_markdown() {
101            Ok(body) => (
102                StatusCode::OK,
103                [(header::CONTENT_TYPE, "text/markdown; charset=utf-8")],
104                body,
105            )
106                .into_response(),
107            Err(e) => {
108                tracing::error!(error = %e, "MarkdownResponse frontmatter serialization failed");
109                StatusCode::INTERNAL_SERVER_ERROR.into_response()
110            },
111        }
112    }
113}