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}