Skip to main content

systemprompt_generator/
error.rs

1use std::path::PathBuf;
2
3#[derive(Debug, thiserror::Error)]
4pub enum PublishError {
5    #[error("Missing field '{field}' for content '{slug}'")]
6    MissingField {
7        field: String,
8        slug: String,
9        source_path: Option<PathBuf>,
10        suggestion: Option<String>,
11    },
12
13    #[error("No template for content type '{content_type}'")]
14    TemplateNotFound {
15        content_type: String,
16        slug: String,
17        available_templates: Vec<String>,
18    },
19
20    #[error("Page data provider '{provider_id}' failed: {cause}")]
21    ProviderFailed {
22        provider_id: String,
23        cause: String,
24        suggestion: Option<String>,
25    },
26
27    #[error("Template render failed for '{template_name}'")]
28    RenderFailed {
29        template_name: String,
30        slug: Option<String>,
31        cause: String,
32    },
33
34    #[error("Content fetch failed for source '{source_name}'")]
35    FetchFailed { source_name: String, cause: String },
36
37    #[error("Configuration error: {message}")]
38    Config {
39        message: String,
40        path: Option<String>,
41    },
42
43    #[error("Page prerenderer '{page_type}' failed: {cause}")]
44    PagePrerendererFailed { page_type: String, cause: String },
45}
46
47impl PublishError {
48    pub fn missing_field(field: impl Into<String>, slug: impl Into<String>) -> Self {
49        let field_str = field.into();
50        Self::MissingField {
51            suggestion: suggest_fix_for_field(&field_str),
52            field: field_str,
53            slug: slug.into(),
54            source_path: None,
55        }
56    }
57
58    pub fn missing_field_with_path(
59        field: impl Into<String>,
60        slug: impl Into<String>,
61        path: PathBuf,
62    ) -> Self {
63        let field_str = field.into();
64        Self::MissingField {
65            suggestion: suggest_fix_for_field(&field_str),
66            field: field_str,
67            slug: slug.into(),
68            source_path: Some(path),
69        }
70    }
71
72    pub fn template_not_found(
73        content_type: impl Into<String>,
74        slug: impl Into<String>,
75        available: Vec<String>,
76    ) -> Self {
77        Self::TemplateNotFound {
78            content_type: content_type.into(),
79            slug: slug.into(),
80            available_templates: available,
81        }
82    }
83
84    pub fn provider_failed(provider_id: impl Into<String>, cause: impl Into<String>) -> Self {
85        Self::ProviderFailed {
86            provider_id: provider_id.into(),
87            cause: cause.into(),
88            suggestion: None,
89        }
90    }
91
92    pub fn render_failed(
93        template_name: impl Into<String>,
94        slug: Option<String>,
95        cause: impl Into<String>,
96    ) -> Self {
97        Self::RenderFailed {
98            template_name: template_name.into(),
99            slug,
100            cause: cause.into(),
101        }
102    }
103
104    pub fn fetch_failed(source_name: impl Into<String>, cause: impl Into<String>) -> Self {
105        Self::FetchFailed {
106            source_name: source_name.into(),
107            cause: cause.into(),
108        }
109    }
110
111    pub fn config(message: impl Into<String>) -> Self {
112        Self::Config {
113            message: message.into(),
114            path: None,
115        }
116    }
117
118    pub fn page_prerenderer_failed(page_type: impl Into<String>, cause: impl Into<String>) -> Self {
119        Self::PagePrerendererFailed {
120            page_type: page_type.into(),
121            cause: cause.into(),
122        }
123    }
124
125    pub fn location(&self) -> Option<String> {
126        match self {
127            Self::MissingField { source_path, .. } => {
128                source_path.as_ref().map(|p| p.display().to_string())
129            },
130            Self::Config { path, .. } => path.clone(),
131            _ => None,
132        }
133    }
134
135    pub fn suggestion_string(&self) -> Option<String> {
136        match self {
137            Self::MissingField { suggestion, .. } | Self::ProviderFailed { suggestion, .. } => {
138                suggestion.clone()
139            },
140            Self::TemplateNotFound {
141                available_templates,
142                content_type,
143                ..
144            } => {
145                if available_templates.is_empty() {
146                    Some("Add templates to the templates directory".to_string())
147                } else {
148                    Some(format!(
149                        "Change content type from '{}' to one of: {}",
150                        content_type,
151                        available_templates.join(", ")
152                    ))
153                }
154            },
155            _ => None,
156        }
157    }
158
159    pub fn cause_string(&self) -> Option<String> {
160        match self {
161            Self::ProviderFailed { cause, .. }
162            | Self::RenderFailed { cause, .. }
163            | Self::FetchFailed { cause, .. }
164            | Self::PagePrerendererFailed { cause, .. } => Some(cause.clone()),
165            _ => None,
166        }
167    }
168}
169
170fn suggest_fix_for_field(field: &str) -> Option<String> {
171    match field {
172        "image" | "cover_image" => {
173            Some("Add 'image: \"/files/images/placeholder.svg\"' to frontmatter".to_string())
174        },
175        "published_at" | "date" | "created_at" | "published_at/date/created_at" => {
176            Some("Add 'date: YYYY-MM-DD' to frontmatter".to_string())
177        },
178        "author" => Some(
179            "Add 'author: Your Name' to frontmatter or set metadata.default_author in config"
180                .to_string(),
181        ),
182        "title" => Some("Add 'title: Your Title' to frontmatter".to_string()),
183        "slug" => Some("Add 'slug: your-slug' to frontmatter".to_string()),
184        "content_type" => {
185            Some("Ensure content has a valid 'kind' field in frontmatter".to_string())
186        },
187        field if field.starts_with("organization.") => Some(format!(
188            "Add '{}' under metadata.structured_data.organization in content.yaml",
189            field.strip_prefix("organization.").unwrap_or(field)
190        )),
191        field if field.starts_with("article.") => Some(format!(
192            "Add '{}' under metadata.structured_data.article in content.yaml",
193            field.strip_prefix("article.").unwrap_or(field)
194        )),
195        field if field.starts_with("branding.") => Some(format!(
196            "Add '{}' under branding in web.yaml",
197            field.strip_prefix("branding.").unwrap_or(field)
198        )),
199        _ => None,
200    }
201}