use serde::Serialize;
use crate::error_code::ErrorCode;
use crate::field_name::FieldName;
#[derive(Debug)]
pub struct SerializeError {
pub message: String,
}
impl std::fmt::Display for SerializeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "serialize error: {}", self.message)
}
}
impl std::error::Error for SerializeError {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct StderrError {
pub error: &'static str,
pub code: ErrorCode,
#[serde(skip_serializing_if = "Option::is_none")]
pub field: Option<FieldName>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cause: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub recovery: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subcode: Option<&'static str>,
}
#[cfg(test)]
thread_local! {
static FORCE_SERIALIZE_FAIL: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
pub struct ForceSerializeFailGuard;
#[cfg(test)]
impl ForceSerializeFailGuard {
pub fn arm() -> Self {
FORCE_SERIALIZE_FAIL.with(|f| f.set(true));
Self
}
}
#[cfg(test)]
impl Drop for ForceSerializeFailGuard {
fn drop(&mut self) {
FORCE_SERIALIZE_FAIL.with(|f| f.set(false));
}
}
impl StderrError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
error: code.error_slug(),
code,
field: None,
message: message.into(),
cause: None,
recovery: Vec::new(),
docs: None,
subcode: None,
}
}
pub fn field(mut self, field: impl Into<FieldName>) -> Self {
self.field = Some(field.into());
self
}
pub fn cause(mut self, cause: impl Into<String>) -> Self {
self.cause = Some(cause.into());
self
}
pub fn recovery(mut self, step: impl Into<String>) -> Self {
self.recovery.push(step.into());
self
}
pub fn docs(mut self, docs: impl Into<String>) -> Self {
self.docs = Some(docs.into());
self
}
pub fn subcode(mut self, subcode: &'static str) -> Self {
self.subcode = Some(subcode);
self
}
pub fn to_json_string(&self) -> Result<String, SerializeError> {
#[cfg(test)]
{
if FORCE_SERIALIZE_FAIL.with(std::cell::Cell::get) {
return Err(SerializeError {
message: "forced".into(),
});
}
}
serde_json::to_string(self).map_err(|e| SerializeError {
message: e.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn omits_empty_optional_fields() {
let err = StderrError::new(ErrorCode::ParseError, "bad json")
.cause("trailing comma")
.recovery("Ensure input is valid JSON");
let value: serde_json::Value =
serde_json::from_str(&err.to_json_string().expect("serialize")).expect("valid JSON");
assert_eq!(value["error"], "parse");
assert_eq!(value["code"], "PARSE_ERROR");
assert!(value.get("field").is_none());
assert!(value.get("docs").is_none());
assert!(value.get("subcode").is_none());
assert_eq!(value["cause"], "trailing comma");
assert_eq!(
value["recovery"],
serde_json::json!(["Ensure input is valid JSON"])
);
}
#[test]
#[serial]
fn serialize_error_forced_fail() {
let _guard = ForceSerializeFailGuard::arm();
let err = StderrError::new(ErrorCode::ParseError, "x");
assert!(err.to_json_string().is_err());
}
}