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