#![forbid(unsafe_code)]
#![warn(missing_docs)]
#[cfg(feature = "obfuscate")]
#[doc(hidden)]
pub mod __private {
pub use obfstr::obfstring;
}
use std::{borrow::Cow, fmt, ops::Deref};
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Message(Cow<'static, str>);
impl Message {
#[must_use]
pub fn from_static(value: &'static str) -> Self {
Self(Cow::Borrowed(value))
}
#[must_use]
pub fn from_string(value: String) -> Self {
Self(Cow::Owned(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
#[must_use]
pub fn into_string(self) -> String {
self.0.into_owned()
}
}
impl Default for Message {
fn default() -> Self {
Self(Cow::Borrowed(""))
}
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Debug for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self.as_str(), f)
}
}
impl AsRef<str> for Message {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Deref for Message {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl PartialEq<&str> for Message {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<Message> for &str {
fn eq(&self, other: &Message) -> bool {
*self == other.as_str()
}
}
impl From<&'static str> for Message {
fn from(value: &'static str) -> Self {
Self::from_static(value)
}
}
impl From<String> for Message {
fn from(value: String) -> Self {
Self::from_string(value)
}
}
impl From<Message> for String {
fn from(value: Message) -> Self {
value.into_string()
}
}
#[cfg(feature = "obfuscate")]
#[macro_export]
macro_rules! message {
($literal:literal) => {
$crate::Message::from_string($crate::__private::obfstring!($literal))
};
}
#[cfg(not(feature = "obfuscate"))]
#[macro_export]
macro_rules! message {
($literal:literal) => {
$crate::Message::from_static($literal)
};
}
#[cfg(feature = "obfuscate")]
#[macro_export]
macro_rules! message_string {
($literal:literal) => {
$crate::message!($literal).into_string()
};
}
#[cfg(not(feature = "obfuscate"))]
#[macro_export]
macro_rules! message_string {
($literal:literal) => {
$crate::message!($literal).into_string()
};
}
#[must_use]
pub fn detail(value: impl Into<String>) -> String {
#[cfg(debug_assertions)]
{
value.into()
}
#[cfg(not(debug_assertions))]
{
let _ = value;
String::new()
}
}
#[must_use]
pub fn display(value: impl std::fmt::Display) -> String {
#[cfg(debug_assertions)]
{
value.to_string()
}
#[cfg(not(debug_assertions))]
{
let _ = value;
String::new()
}
}
#[macro_export]
macro_rules! detail {
($($arg:tt)*) => {{
#[cfg(debug_assertions)]
{
format!($($arg)*)
}
#[cfg(not(debug_assertions))]
{
::std::string::String::new()
}
}};
}
pub trait ErrorCode {
fn code(&self) -> &'static str;
}
pub trait PublicError: ErrorCode {
fn public_message(&self) -> Message;
}
#[macro_export]
macro_rules! impl_redacted_debug {
($ty:ty) => {
#[cfg(not(debug_assertions))]
impl ::std::fmt::Debug for $ty {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
::std::fmt::Display::fmt(self, f)
}
}
};
}
#[cfg(test)]
mod tests {
use super::Message;
use std::fmt;
#[test]
fn message_returns_public_literal() {
assert_eq!(crate::message!("request failed"), "request failed");
}
#[test]
fn message_string_returns_owned_public_literal() {
assert_eq!(
crate::message_string!("request failed"),
String::from("request failed")
);
}
#[test]
fn message_default_is_empty() {
assert_eq!(Message::default(), "");
}
#[test]
fn message_from_static_and_string() {
let from_static: Message = "literal".into();
let from_owned: Message = String::from("literal").into();
assert_eq!(from_static, from_owned);
assert_eq!(from_static.as_str(), "literal");
}
#[test]
fn detail_is_available_only_in_debug_builds() {
let value = super::detail("secret");
#[cfg(debug_assertions)]
assert_eq!(value, "secret");
#[cfg(not(debug_assertions))]
assert!(value.is_empty());
}
#[test]
fn display_is_available_only_in_debug_builds() {
let value = super::display("secret");
#[cfg(debug_assertions)]
assert_eq!(value, "secret");
#[cfg(not(debug_assertions))]
assert!(value.is_empty());
}
#[test]
fn detail_macro_is_available_only_in_debug_builds() {
let value = crate::detail!("secret {}", 42);
#[cfg(debug_assertions)]
assert_eq!(value, "secret 42");
#[cfg(not(debug_assertions))]
assert!(value.is_empty());
}
#[cfg_attr(debug_assertions, derive(Debug))]
#[allow(dead_code)]
struct SampleError(String);
impl fmt::Display for SampleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(debug_assertions)]
{
write!(f, "{} {}", crate::message!("sample failed:"), self.0)
}
#[cfg(not(debug_assertions))]
{
write!(f, "{}", crate::message!("sample failed"))
}
}
}
crate::impl_redacted_debug!(SampleError);
impl super::ErrorCode for SampleError {
fn code(&self) -> &'static str {
"sample.failed"
}
}
impl super::PublicError for SampleError {
fn public_message(&self) -> Message {
crate::message!("sample failed")
}
}
#[test]
fn release_debug_delegates_to_display() {
let err = SampleError("secret".to_owned());
let debug = format!("{err:?}");
#[cfg(debug_assertions)]
assert!(debug.contains("secret"));
#[cfg(not(debug_assertions))]
assert_eq!(debug, "sample failed");
}
#[test]
fn public_error_exposes_stable_code_and_message() {
let err = SampleError("secret".to_owned());
assert_eq!(super::ErrorCode::code(&err), "sample.failed");
assert_eq!(super::PublicError::public_message(&err), "sample failed");
}
}