Skip to main content

anyform/
error.rs

1//! Error types for anyform.
2
3use axum::response::{IntoResponse, Response};
4use http::StatusCode;
5use std::collections::HashMap;
6
7/// Core error type for anyform.
8#[derive(Debug, Clone, thiserror::Error)]
9pub enum FormError {
10    #[error("Form not found: {0}")]
11    NotFound(String),
12
13    #[error("Step not found: {0}")]
14    StepNotFound(String),
15
16    #[error("Field not found: {0}")]
17    FieldNotFound(String),
18
19    #[error("Validation failed")]
20    ValidationFailed(ValidationErrors),
21
22    #[error("Validation failed")]
23    StepValidationFailed(StepValidationErrors),
24
25    #[error("Database error: {0}")]
26    Database(String),
27
28    #[error("Invalid field type: {0}")]
29    InvalidFieldType(String),
30
31    #[error("Condition evaluation failed: {0}")]
32    ConditionError(String),
33
34    #[error("File upload error: {0}")]
35    FileUpload(String),
36
37    #[error("Invalid form data: {0}")]
38    InvalidData(String),
39
40    #[error("Form is deleted")]
41    FormDeleted,
42
43    #[error("Submission not found: {0}")]
44    SubmissionNotFound(String),
45}
46
47impl FormError {
48    /// Returns the HTTP status code for this error.
49    #[must_use]
50    pub fn status_code(&self) -> StatusCode {
51        match self {
52            Self::NotFound(_)
53            | Self::StepNotFound(_)
54            | Self::FieldNotFound(_)
55            | Self::SubmissionNotFound(_) => StatusCode::NOT_FOUND,
56            Self::ValidationFailed(_)
57            | Self::StepValidationFailed(_)
58            | Self::InvalidFieldType(_)
59            | Self::InvalidData(_) => StatusCode::BAD_REQUEST,
60            Self::FormDeleted => StatusCode::GONE,
61            Self::Database(_) | Self::ConditionError(_) => StatusCode::INTERNAL_SERVER_ERROR,
62            Self::FileUpload(_) => StatusCode::BAD_REQUEST,
63        }
64    }
65
66    /// Returns the error code string for this error.
67    #[must_use]
68    pub fn error_code(&self) -> &'static str {
69        match self {
70            Self::NotFound(_) => "FORM_NOT_FOUND",
71            Self::StepNotFound(_) => "STEP_NOT_FOUND",
72            Self::FieldNotFound(_) => "FIELD_NOT_FOUND",
73            Self::ValidationFailed(_) | Self::StepValidationFailed(_) => "VALIDATION_FAILED",
74            Self::Database(_) => "DATABASE_ERROR",
75            Self::InvalidFieldType(_) => "INVALID_FIELD_TYPE",
76            Self::ConditionError(_) => "CONDITION_ERROR",
77            Self::FileUpload(_) => "FILE_UPLOAD_ERROR",
78            Self::InvalidData(_) => "INVALID_DATA",
79            Self::FormDeleted => "FORM_DELETED",
80            Self::SubmissionNotFound(_) => "SUBMISSION_NOT_FOUND",
81        }
82    }
83}
84
85impl IntoResponse for FormError {
86    fn into_response(self) -> Response {
87        let api_response: crate::response::ApiResponse<()> = self.into();
88        api_response.into_response()
89    }
90}
91
92impl From<FormError> for crate::response::ApiResponse<()> {
93    fn from(err: FormError) -> Self {
94        match &err {
95            FormError::ValidationFailed(errors) => {
96                crate::response::ApiResponse::validation_failed(errors.clone())
97            }
98            FormError::StepValidationFailed(errors) => {
99                crate::response::ApiResponse::step_validation_failed(errors.clone())
100            }
101            _ => crate::response::ApiResponse::error(
102                err.error_code(),
103                err.to_string(),
104                err.status_code(),
105            ),
106        }
107    }
108}
109
110impl From<sea_orm::DbErr> for FormError {
111    fn from(err: sea_orm::DbErr) -> Self {
112        Self::Database(err.to_string())
113    }
114}
115
116impl From<evalexpr::EvalexprError> for FormError {
117    fn from(err: evalexpr::EvalexprError) -> Self {
118        Self::ConditionError(err.to_string())
119    }
120}
121
122/// Trait for converting FormError to your API response type.
123///
124/// Implement this trait on your own response type to integrate
125/// anyform errors with your API envelope pattern.
126///
127/// # Example
128///
129/// ```rust,ignore
130/// use anyform::{FormError, IntoApiError};
131///
132/// impl IntoApiError for ApiResponse<()> {
133///     type Response = Self;
134///
135///     fn into_api_error(error: FormError) -> Self::Response {
136///         ApiResponse {
137///             data: None,
138///             success: false,
139///             status: error.status_code().as_u16(),
140///             error: Some(error.to_string()),
141///             ..Default::default()
142///         }
143///     }
144/// }
145/// ```
146pub trait IntoApiError {
147    type Response: IntoResponse;
148
149    fn into_api_error(error: FormError) -> Self::Response;
150}
151
152/// Default implementation that returns the FormError directly.
153impl IntoApiError for FormError {
154    type Response = Self;
155
156    fn into_api_error(error: FormError) -> Self::Response {
157        error
158    }
159}
160
161/// Validation errors mapped by field name.
162#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
163pub struct ValidationErrors {
164    /// Errors keyed by field name.
165    pub errors: HashMap<String, Vec<String>>,
166}
167
168/// Validation errors grouped by step.
169///
170/// For multi-step forms, errors are organized by step ID, allowing clients
171/// to display errors contextually within each step.
172#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
173pub struct StepValidationErrors {
174    /// Errors keyed by step ID, then by field name.
175    pub steps: HashMap<String, HashMap<String, Vec<String>>>,
176}
177
178impl StepValidationErrors {
179    /// Creates new empty step validation errors.
180    #[must_use]
181    pub fn new() -> Self {
182        Self::default()
183    }
184
185    /// Returns true if there are no validation errors.
186    #[must_use]
187    pub fn is_empty(&self) -> bool {
188        self.steps.is_empty() || self.steps.values().all(|fields| fields.is_empty())
189    }
190
191    /// Returns the total number of fields with errors across all steps.
192    #[must_use]
193    pub fn field_count(&self) -> usize {
194        self.steps.values().map(HashMap::len).sum()
195    }
196
197    /// Returns the total number of error messages across all fields.
198    #[must_use]
199    pub fn error_count(&self) -> usize {
200        self.steps
201            .values()
202            .flat_map(|fields| fields.values())
203            .map(Vec::len)
204            .sum()
205    }
206
207    /// Adds an error for a field within a step.
208    pub fn add(
209        &mut self,
210        step_id: impl Into<String>,
211        field: impl Into<String>,
212        message: impl Into<String>,
213    ) {
214        self.steps
215            .entry(step_id.into())
216            .or_default()
217            .entry(field.into())
218            .or_default()
219            .push(message.into());
220    }
221
222    /// Gets errors for a specific step.
223    #[must_use]
224    pub fn get_step(&self, step_id: &str) -> Option<&HashMap<String, Vec<String>>> {
225        self.steps.get(step_id)
226    }
227
228    /// Gets errors for a specific field within a step.
229    #[must_use]
230    pub fn get_field(&self, step_id: &str, field: &str) -> Option<&Vec<String>> {
231        self.steps.get(step_id).and_then(|s| s.get(field))
232    }
233
234    /// Converts to flat `ValidationErrors`, losing step grouping.
235    #[must_use]
236    pub fn flatten(&self) -> ValidationErrors {
237        let mut errors = ValidationErrors::new();
238        for fields in self.steps.values() {
239            for (field, messages) in fields {
240                for message in messages {
241                    errors.add(field.clone(), message.clone());
242                }
243            }
244        }
245        errors
246    }
247}
248
249impl ValidationErrors {
250    /// Creates a new empty `ValidationErrors`.
251    #[must_use]
252    pub fn new() -> Self {
253        Self::default()
254    }
255
256    /// Returns true if there are no validation errors.
257    #[must_use]
258    pub fn is_empty(&self) -> bool {
259        self.errors.is_empty()
260    }
261
262    /// Returns the number of fields with errors.
263    #[must_use]
264    pub fn len(&self) -> usize {
265        self.errors.len()
266    }
267
268    /// Adds an error for a field.
269    pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
270        self.errors
271            .entry(field.into())
272            .or_default()
273            .push(message.into());
274    }
275
276    /// Gets errors for a specific field.
277    #[must_use]
278    pub fn get(&self, field: &str) -> Option<&Vec<String>> {
279        self.errors.get(field)
280    }
281
282    /// Merges another `ValidationErrors` into this one.
283    pub fn merge(&mut self, other: Self) {
284        for (field, messages) in other.errors {
285            self.errors.entry(field).or_default().extend(messages);
286        }
287    }
288}
289
290impl std::fmt::Display for ValidationErrors {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        let count = self.errors.values().map(Vec::len).sum::<usize>();
293        write!(f, "{count} validation error(s)")
294    }
295}
296
297impl std::fmt::Display for StepValidationErrors {
298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299        let count = self.error_count();
300        let step_count = self.steps.len();
301        write!(f, "{count} validation error(s) in {step_count} step(s)")
302    }
303}