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