use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ErrorSeverity {
#[default]
Error,
Warning,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorMeta {
pub code: String,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default)]
pub severity: ErrorSeverity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
impl ErrorMeta {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
path: None,
severity: ErrorSeverity::Error,
help: None,
source: None,
}
}
pub fn warning(mut self) -> Self {
self.severity = ErrorSeverity::Warning;
self
}
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FormError {
pub code: String,
pub message: String,
pub path: String,
pub severity: ErrorSeverity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
impl FormError {
pub fn from_meta(meta: &ErrorMeta, resolved_path: &str) -> Self {
Self {
code: meta.code.clone(),
message: meta.message.clone(),
path: meta
.path
.clone()
.unwrap_or_else(|| resolved_path.to_string()),
severity: meta.severity,
help: meta.help.clone(),
source: meta.source.clone(),
}
}
pub fn new(
code: impl Into<String>,
message: impl Into<String>,
path: impl Into<String>,
) -> Self {
Self {
code: code.into(),
message: message.into(),
path: path.into(),
severity: ErrorSeverity::Error,
help: None,
source: None,
}
}
pub fn is_error(&self) -> bool {
self.severity == ErrorSeverity::Error
}
pub fn is_warning(&self) -> bool {
self.severity == ErrorSeverity::Warning
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_meta_builder() {
let meta = ErrorMeta::new("TEST_ERROR", "Test message")
.warning()
.with_help("Try this fix")
.with_source("test");
assert_eq!(meta.code, "TEST_ERROR");
assert_eq!(meta.severity, ErrorSeverity::Warning);
assert_eq!(meta.help, Some("Try this fix".to_string()));
assert_eq!(meta.source, Some("test".to_string()));
}
#[test]
fn test_form_error_from_meta() {
let meta = ErrorMeta::new("FIELD_REQUIRED", "Field is required")
.with_help("Please provide a value");
let error = FormError::from_meta(&meta, "/foo/bar");
assert_eq!(error.code, "FIELD_REQUIRED");
assert_eq!(error.path, "/foo/bar");
assert_eq!(error.severity, ErrorSeverity::Error);
}
#[test]
fn test_form_error_path_override() {
let meta = ErrorMeta::new("FIELD_REQUIRED", "Field is required").with_path("/custom/path");
let error = FormError::from_meta(&meta, "/foo/bar");
assert_eq!(error.path, "/custom/path");
}
#[test]
fn test_serde_roundtrip() {
let error = FormError::new("TEST", "Test", "/path");
let json = serde_json::to_string(&error).unwrap();
let parsed: FormError = serde_json::from_str(&json).unwrap();
assert_eq!(error, parsed);
}
}