Skip to main content

systemprompt_generator/error/
mod.rs

1//! Error types for the static-site generator pipeline.
2//!
3//! [`PublishError`] is the unified error type returned by every public function
4//! in `systemprompt-generator`. It composes upstream I/O, YAML, and JSON errors
5//! via [`From`] so call sites can use `?` without manual mapping, and exposes
6//! domain-specific variants (`MissingField`, `TemplateNotFound`,
7//! `RenderFailed`, etc.) so CLI/API layers can surface actionable diagnostics.
8//!
9//! [`GeneratorResult`] is the canonical `Result` alias — prefer it over bare
10//! `Result<T, PublishError>` in new code.
11//!
12//! Copyright (c) systemprompt.io — Business Source License 1.1.
13//! See <https://systemprompt.io> for licensing details.
14
15use std::path::PathBuf;
16
17mod suggestions;
18use suggestions::suggest_fix_for_field;
19
20#[derive(Debug, thiserror::Error)]
21pub enum PublishError {
22    #[error("Missing field '{field}' for content '{slug}'")]
23    MissingField {
24        field: String,
25        slug: String,
26        source_path: Option<PathBuf>,
27        suggestion: Option<String>,
28    },
29
30    #[error("No template for content type '{content_type}'")]
31    TemplateNotFound {
32        content_type: String,
33        slug: String,
34        available_templates: Vec<String>,
35    },
36
37    #[error("Page data provider '{provider_id}' failed: {cause}")]
38    ProviderFailed {
39        provider_id: String,
40        cause: String,
41        suggestion: Option<String>,
42    },
43
44    #[error("Template render failed for '{template_name}'")]
45    RenderFailed {
46        template_name: String,
47        slug: Option<String>,
48        cause: String,
49    },
50
51    #[error("Content fetch failed for source '{source_name}'")]
52    FetchFailed { source_name: String, cause: String },
53
54    #[error("Configuration error: {message}")]
55    Config {
56        message: String,
57        path: Option<String>,
58    },
59
60    #[error("Page prerenderer '{page_type}' failed: {cause}")]
61    PagePrerendererFailed { page_type: String, cause: String },
62
63    #[error("I/O error: {0}")]
64    Io(#[from] std::io::Error),
65
66    #[error("{context}: {source}")]
67    IoContext {
68        context: String,
69        #[source]
70        source: std::io::Error,
71    },
72
73    #[error("YAML error: {0}")]
74    Yaml(#[from] serde_yaml::Error),
75
76    #[error("JSON error: {0}")]
77    Json(#[from] serde_json::Error),
78
79    #[error("Failed to read content config {}: {source}", path.display())]
80    ContentConfigRead {
81        path: PathBuf,
82        #[source]
83        source: std::io::Error,
84    },
85
86    #[error("Failed to parse content config {}: {source}", path.display())]
87    ContentConfigParse {
88        path: PathBuf,
89        #[source]
90        source: serde_yaml::Error,
91    },
92
93    #[error("Failed to load web config: {0}")]
94    WebConfig(#[from] systemprompt_models::WebConfigError),
95
96    #[error("Failed to load global config: {0}")]
97    GlobalConfig(#[from] systemprompt_models::errors::ConfigError),
98
99    #[error("{context}: {source}")]
100    Content {
101        context: String,
102        #[source]
103        source: systemprompt_content::ContentError,
104    },
105
106    #[error("Template registry error: {0}")]
107    Template(#[from] systemprompt_templates::TemplateError),
108
109    #[error("Extension discovery failed: {0}")]
110    ExtensionDiscovery(#[from] systemprompt_extension::LoaderError),
111}
112
113pub type GeneratorResult<T> = Result<T, PublishError>;
114
115impl PublishError {
116    pub fn missing_field(field: impl Into<String>, slug: impl Into<String>) -> Self {
117        let field_str = field.into();
118        Self::MissingField {
119            suggestion: suggest_fix_for_field(&field_str),
120            field: field_str,
121            slug: slug.into(),
122            source_path: None,
123        }
124    }
125
126    pub fn missing_field_with_path(
127        field: impl Into<String>,
128        slug: impl Into<String>,
129        path: PathBuf,
130    ) -> Self {
131        let field_str = field.into();
132        Self::MissingField {
133            suggestion: suggest_fix_for_field(&field_str),
134            field: field_str,
135            slug: slug.into(),
136            source_path: Some(path),
137        }
138    }
139
140    pub fn template_not_found(
141        content_type: impl Into<String>,
142        slug: impl Into<String>,
143        available: Vec<String>,
144    ) -> Self {
145        Self::TemplateNotFound {
146            content_type: content_type.into(),
147            slug: slug.into(),
148            available_templates: available,
149        }
150    }
151
152    pub fn provider_failed(provider_id: impl Into<String>, cause: impl Into<String>) -> Self {
153        Self::ProviderFailed {
154            provider_id: provider_id.into(),
155            cause: cause.into(),
156            suggestion: None,
157        }
158    }
159
160    pub fn render_failed(
161        template_name: impl Into<String>,
162        slug: Option<String>,
163        cause: impl Into<String>,
164    ) -> Self {
165        Self::RenderFailed {
166            template_name: template_name.into(),
167            slug,
168            cause: cause.into(),
169        }
170    }
171
172    pub fn fetch_failed(source_name: impl Into<String>, cause: impl Into<String>) -> Self {
173        Self::FetchFailed {
174            source_name: source_name.into(),
175            cause: cause.into(),
176        }
177    }
178
179    pub fn config(message: impl Into<String>) -> Self {
180        Self::Config {
181            message: message.into(),
182            path: None,
183        }
184    }
185
186    pub fn page_prerenderer_failed(page_type: impl Into<String>, cause: impl Into<String>) -> Self {
187        Self::PagePrerendererFailed {
188            page_type: page_type.into(),
189            cause: cause.into(),
190        }
191    }
192
193    pub fn io_context(context: impl Into<String>, source: std::io::Error) -> Self {
194        Self::IoContext {
195            context: context.into(),
196            source,
197        }
198    }
199
200    pub fn content(
201        context: impl Into<String>,
202        source: impl Into<systemprompt_content::ContentError>,
203    ) -> Self {
204        Self::Content {
205            context: context.into(),
206            source: source.into(),
207        }
208    }
209
210    pub fn location(&self) -> Option<String> {
211        match self {
212            Self::MissingField { source_path, .. } => {
213                source_path.as_ref().map(|p| p.display().to_string())
214            },
215            Self::Config { path, .. } => path.clone(),
216            _ => None,
217        }
218    }
219
220    pub fn suggestion_string(&self) -> Option<String> {
221        match self {
222            Self::MissingField { suggestion, .. } | Self::ProviderFailed { suggestion, .. } => {
223                suggestion.clone()
224            },
225            Self::TemplateNotFound {
226                available_templates,
227                content_type,
228                ..
229            } => {
230                if available_templates.is_empty() {
231                    Some("Add templates to the templates directory".to_owned())
232                } else {
233                    Some(format!(
234                        "Change content type from '{}' to one of: {}",
235                        content_type,
236                        available_templates.join(", ")
237                    ))
238                }
239            },
240            _ => None,
241        }
242    }
243
244    pub fn cause_string(&self) -> Option<String> {
245        match self {
246            Self::ProviderFailed { cause, .. }
247            | Self::RenderFailed { cause, .. }
248            | Self::FetchFailed { cause, .. }
249            | Self::PagePrerendererFailed { cause, .. } => Some(cause.clone()),
250            _ => None,
251        }
252    }
253}