#[cfg(feature = "std")]
use std::{
format,
string::{String, ToString},
vec::Vec,
};
#[cfg(not(feature = "std"))]
use alloc::{
format,
string::{String, ToString},
vec,
vec::Vec,
};
use super::types::{CodeSnippet, ErrorDoc};
use crate::traits::Role;
#[derive(Debug, Clone, PartialEq)]
pub enum ParseError {
InvalidFormat,
InvalidSequence(String),
}
impl core::fmt::Display for ParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ParseError::InvalidFormat => {
write!(
f,
"Invalid error code format, expected: SEVERITY.COMPONENT.PRIMARY.SEQUENCE"
)
}
ParseError::InvalidSequence(val) => {
write!(f, "Invalid sequence number: {}", val)
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParseError {}
#[derive(Debug, Clone)]
struct ErrorCodeParts {
severity: String,
component: String,
primary: String,
sequence: u16,
}
fn parse_error_code(code: &str) -> Result<ErrorCodeParts, ParseError> {
let parts: Vec<&str> = code.split('.').collect();
if parts.len() != 4 {
return Err(ParseError::InvalidFormat);
}
let sequence = parts[3]
.parse::<u16>()
.map_err(|_| ParseError::InvalidSequence(parts[3].to_string()))?;
Ok(ErrorCodeParts {
severity: parts[0].to_string(),
component: parts[1].to_string(),
primary: parts[2].to_string(),
sequence,
})
}
#[derive(Debug)]
pub struct ErrorDocBuilder {
code: String,
severity: String,
component: String,
primary: String,
sequence: u16,
description: String,
message: String,
fields: Vec<String>,
introduced: Option<String>,
deprecated: Option<String>,
docs_url: Option<String>,
namespace: Option<String>,
hints_gated: Vec<(String, String)>,
tags_gated: Vec<(String, String)>,
related_codes_gated: Vec<(String, String)>,
see_also_gated: Vec<(String, String)>,
role: Option<String>,
code_snippets: Vec<CodeSnippet>,
version: Option<String>,
}
impl ErrorDocBuilder {
pub fn from_code(code: impl Into<String>) -> Result<Self, ParseError> {
let code_str = code.into();
let parts = parse_error_code(&code_str)?;
Ok(Self {
code: code_str,
severity: parts.severity,
component: parts.component,
primary: parts.primary,
sequence: parts.sequence,
description: String::new(),
message: String::new(),
fields: Vec::new(),
namespace: None,
introduced: None,
deprecated: None,
docs_url: None,
hints_gated: Vec::new(),
tags_gated: Vec::new(),
related_codes_gated: Vec::new(),
see_also_gated: Vec::new(),
role: None,
code_snippets: Vec::new(),
version: None,
})
}
pub fn from_components(
severity: impl Into<String>,
component: impl Into<String>,
primary: impl Into<String>,
sequence: u16,
) -> Self {
let severity_str = severity.into();
let component_str = component.into();
let primary_str = primary.into();
let code = format!(
"{}.{}.{}.{:03}",
severity_str, component_str, primary_str, sequence
);
Self {
code,
severity: severity_str,
component: component_str,
primary: primary_str,
sequence,
description: String::new(),
message: String::new(),
fields: Vec::new(),
namespace: None,
introduced: None,
deprecated: None,
docs_url: None,
hints_gated: Vec::new(),
tags_gated: Vec::new(),
related_codes_gated: Vec::new(),
see_also_gated: Vec::new(),
role: None,
code_snippets: Vec::new(),
version: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
pub fn with_fields(mut self, fields: Vec<String>) -> Self {
self.fields = fields;
self
}
pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
self.namespace = Some(namespace.into());
self
}
pub fn with_introduced(mut self, version: impl Into<String>) -> Self {
self.introduced = Some(version.into());
self
}
pub fn with_deprecated(mut self, version: impl Into<String>) -> Self {
self.deprecated = Some(version.into());
self
}
pub fn with_docs_url(mut self, url: impl Into<String>) -> Self {
self.docs_url = Some(url.into());
self
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn with_hints_gated(mut self, hints: Vec<(String, String)>) -> Self {
self.hints_gated = hints;
self
}
pub fn add_hint_gated(mut self, hint: impl Into<String>, role: impl Into<String>) -> Self {
self.hints_gated.push((hint.into(), role.into()));
self
}
pub fn with_tags_gated(mut self, tags: Vec<(String, String)>) -> Self {
self.tags_gated = tags;
self
}
pub fn add_tag_gated(mut self, tag: impl Into<String>, role: impl Into<String>) -> Self {
self.tags_gated.push((tag.into(), role.into()));
self
}
pub fn with_related_codes_gated(mut self, codes: Vec<(String, String)>) -> Self {
self.related_codes_gated = codes;
self
}
pub fn add_related_code_gated(
mut self,
code: impl Into<String>,
role: impl Into<String>,
) -> Self {
self.related_codes_gated.push((code.into(), role.into()));
self
}
pub fn with_see_also_gated(mut self, refs: Vec<(String, String)>) -> Self {
self.see_also_gated = refs;
self
}
pub fn add_see_also_gated(
mut self,
reference: impl Into<String>,
role: impl Into<String>,
) -> Self {
self.see_also_gated.push((reference.into(), role.into()));
self
}
pub fn with_role(mut self, role: Role) -> Self {
self.role = Some(format!("{:?}", role));
self
}
pub fn with_code_snippets(mut self, snippets: Vec<CodeSnippet>) -> Self {
self.code_snippets = snippets;
self
}
pub fn add_code_snippet(mut self, snippet: CodeSnippet) -> Self {
self.code_snippets.push(snippet);
self
}
pub fn build(self) -> ErrorDoc {
#[cfg(feature = "runtime-hash")]
let hash: Option<String> = {
use waddling_errors_hash::compute_wdp_hash;
Some(compute_wdp_hash(&self.code))
};
#[cfg(feature = "runtime-hash")]
let namespace_hash: Option<String> = self.namespace.as_ref().map(|ns| {
use waddling_errors_hash::compute_wdp_namespace_hash;
compute_wdp_namespace_hash(ns)
});
let mut error_doc = ErrorDoc::new(
self.code,
self.severity,
self.component,
self.primary,
self.sequence,
self.description,
self.message,
self.fields,
self.namespace,
#[cfg(feature = "runtime-hash")]
namespace_hash,
self.introduced.or(self.version),
self.deprecated,
self.docs_url,
self.role,
#[cfg(feature = "runtime-hash")]
hash,
self.code_snippets,
);
error_doc.hints_gated = self.hints_gated;
error_doc.tags_gated = self.tags_gated;
error_doc.related_codes_gated = self.related_codes_gated;
error_doc.see_also_gated = self.see_also_gated;
error_doc
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::{vec, vec::Vec};
#[test]
fn test_parse_valid_code() {
let parts = parse_error_code("E.AUTH.TOKEN.001").unwrap();
assert_eq!(parts.severity, "E");
assert_eq!(parts.component, "AUTH");
assert_eq!(parts.primary, "TOKEN");
assert_eq!(parts.sequence, 1);
}
#[test]
fn test_parse_invalid_format() {
assert!(matches!(
parse_error_code("E.AUTH.TOKEN"),
Err(ParseError::InvalidFormat)
));
}
#[test]
fn test_parse_invalid_sequence() {
assert!(matches!(
parse_error_code("E.AUTH.TOKEN.ABC"),
Err(ParseError::InvalidSequence(_))
));
}
#[test]
fn test_builder_from_code() {
let error = ErrorDocBuilder::from_code("E.AUTH.TOKEN.001")
.unwrap()
.with_description("Token missing")
.add_hint_gated("Check header", "Public")
.build();
assert_eq!(error.code, "E.AUTH.TOKEN.001");
assert_eq!(error.severity, "E");
assert_eq!(error.component, "AUTH");
assert_eq!(error.primary, "TOKEN");
assert_eq!(error.sequence, 1);
assert_eq!(error.description, "Token missing");
assert_eq!(error.hints_gated.len(), 1);
assert_eq!(error.hints_gated[0].0, "Check header");
assert_eq!(error.hints_gated[0].1, "Public");
}
#[test]
fn test_builder_from_components() {
let error = ErrorDocBuilder::from_components("E", "AUTH", "TOKEN", 1)
.with_description("Token missing")
.build();
assert_eq!(error.code, "E.AUTH.TOKEN.001");
assert_eq!(error.description, "Token missing");
}
#[test]
fn test_builder_with_gated_fields() {
let error = ErrorDocBuilder::from_code("E.AUTH.TOKEN.001")
.unwrap()
.add_hint_gated("Public hint", "Public")
.add_hint_gated("Internal hint", "Internal")
.add_tag_gated("security", "Public")
.build();
assert_eq!(error.hints_gated.len(), 2);
assert_eq!(error.tags_gated.len(), 1);
}
#[test]
fn test_builder_with_role() {
let error = ErrorDocBuilder::from_code("E.AUTH.TOKEN.001")
.unwrap()
.with_role(Role::Public)
.build();
assert_eq!(error.role, Some("Public".to_string()));
}
#[test]
#[cfg(feature = "runtime-hash")]
fn test_hash_auto_generated() {
let error = ErrorDocBuilder::from_code("E.AUTH.TOKEN.001")
.unwrap()
.build();
assert!(error.hash.is_some());
assert_eq!(error.hash.unwrap().len(), 5);
}
}