use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
use crate::{Class, Error, ErrorExt, ResourceExhaustionKind};
#[derive(Clone, Default, PartialEq)]
pub struct Metadata(BTreeMap<Cow<'static, str>, MetadataValue>);
impl Metadata {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn contains_key(&self, key: &str) -> bool {
self.0.contains_key(key)
}
pub fn get(&self, key: &str) -> Option<&MetadataValue> {
self.0.get(key)
}
pub fn get_mut(&mut self, key: &str) -> Option<&mut MetadataValue> {
self.0.get_mut(key)
}
pub fn insert(
&mut self,
key: impl Into<Cow<'static, str>>,
value: impl Into<MetadataValue>,
) -> Option<MetadataValue> {
self.0.insert(key.into(), value.into())
}
pub fn remove(&mut self, key: &str) -> Option<MetadataValue> {
self.0.remove(key)
}
pub fn clear(&mut self) {
self.0.clear();
}
pub fn iter(
&self,
) -> impl DoubleEndedIterator<Item = (&Cow<'static, str>, &MetadataValue)> + ExactSizeIterator + '_ {
self.0.iter()
}
pub fn iter_mut(
&mut self,
) -> impl DoubleEndedIterator<Item = (&Cow<'static, str>, &mut MetadataValue)> + ExactSizeIterator + '_ {
self.0.iter_mut()
}
}
impl std::ops::Index<&str> for Metadata {
type Output = MetadataValue;
fn index(&self, key: &str) -> &Self::Output {
&self.0[key]
}
}
impl FromIterator<(Cow<'static, str>, MetadataValue)> for Metadata {
fn from_iter<T: IntoIterator<Item = (Cow<'static, str>, MetadataValue)>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl<const N: usize> From<[(Cow<'static, str>, MetadataValue); N]> for Metadata {
fn from(values: [(Cow<'static, str>, MetadataValue); N]) -> Self {
values.into_iter().collect()
}
}
impl Extend<(Cow<'static, str>, MetadataValue)> for Metadata {
fn extend<T: IntoIterator<Item = (Cow<'static, str>, MetadataValue)>>(&mut self, iter: T) {
self.0.extend(iter);
}
}
impl fmt::Debug for Metadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = f.debug_map();
for (key, value) in self.iter() {
debug.entry(&DebugKey(key), value);
}
debug.finish()
}
}
struct DebugKey<'a>(&'a str);
impl fmt::Debug for DebugKey<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.0.is_empty()
&& self
.0
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
{
f.write_str(self.0)
} else {
fmt::Debug::fmt(self.0, f)
}
}
}
pub struct Message {
pub message: Cow<'static, str>,
pub class: Option<Class>,
pub values: Metadata,
}
impl Message {
pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
Self {
message: message.into(),
class: None,
values: Metadata::new(),
}
}
pub fn with_class(mut self, class: Class) -> Self {
self.class = Some(class);
self
}
}
impl Message {
pub fn corrupted(self) -> Self {
self.with_class(Class::Corruption)
}
#[track_caller]
pub fn corrupted_error(self) -> Error {
self.corrupted().raise()
}
pub fn validation(self) -> Self {
self.with_class(Class::Validation)
}
#[track_caller]
pub fn validation_error(self) -> Error {
self.validation().raise()
}
pub fn not_found(self) -> Self {
self.with_class(Class::NotFound)
}
#[track_caller]
pub fn not_found_error(self) -> Error {
self.not_found().raise()
}
pub fn retryable(self) -> Self {
self.with_class(Class::Retryable)
}
#[track_caller]
pub fn retryable_error(self) -> Error {
self.retryable().raise()
}
pub fn cancelled(self) -> Self {
self.with_class(Class::Cancelled)
}
#[track_caller]
pub fn cancelled_error(self) -> Error {
self.cancelled().raise()
}
pub fn permission_denied(self) -> Self {
self.with_class(Class::PermissionDenied)
}
#[track_caller]
pub fn permission_denied_error(self) -> Error {
self.permission_denied().raise()
}
pub fn unauthenticated(self) -> Self {
self.with_class(Class::Unauthenticated)
}
#[track_caller]
pub fn unauthenticated_error(self) -> Error {
self.unauthenticated().raise()
}
pub fn conflict(self) -> Self {
self.with_class(Class::Conflict)
}
#[track_caller]
pub fn conflict_error(self) -> Error {
self.conflict().raise()
}
pub fn unsupported(self) -> Self {
self.with_class(Class::Unsupported)
}
#[track_caller]
pub fn unsupported_error(self) -> Error {
self.unsupported().raise()
}
pub fn resource_exhaustion(self, kind: ResourceExhaustionKind) -> Self {
self.with_class(Class::ResourceExhaustion(kind))
}
#[track_caller]
pub fn resource_exhaustion_error(self, kind: ResourceExhaustionKind) -> Error {
self.resource_exhaustion(kind).raise()
}
pub fn allocation_limit(self) -> Self {
self.resource_exhaustion(ResourceExhaustionKind::AllocationLimit)
}
#[track_caller]
pub fn allocation_limit_error(self) -> Error {
self.allocation_limit().raise()
}
pub fn allocation_failure(self) -> Self {
self.resource_exhaustion(ResourceExhaustionKind::AllocationFailure)
}
#[track_caller]
pub fn allocation_failure_error(self) -> Error {
self.allocation_failure().raise()
}
}
impl Message {
pub fn with_input(self, input: impl Into<MetadataValue>) -> Self {
self.with("input", input)
}
pub fn with_command_status(self, command: &std::process::Command, status: std::process::ExitStatus) -> Self {
self.with_program(command.get_program()).with_exit_status(status)
}
pub fn with_program(self, program: impl AsRef<std::ffi::OsStr>) -> Self {
self.with("program", std::path::Path::new(program.as_ref()))
}
pub fn with_exit_status(mut self, status: std::process::ExitStatus) -> Self {
self = self.with("exit_status", status.to_string());
if let Some(code) = status.code() {
self = self.with("exit_code", code);
} else {
self.values.remove("exit_code");
}
self
}
pub fn with_command_output(self, command: &std::process::Command, output: std::process::Output) -> Self {
self.with_command_status(command, output.status)
.with("stdout", output.stdout)
.with("stderr", output.stderr)
}
pub fn with(mut self, key: impl Into<Cow<'static, str>>, value: impl Into<MetadataValue>) -> Self {
self.values.insert(key, value);
self
}
}
impl fmt::Debug for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = f.debug_struct("Message");
debug.field("message", &self.message);
if let Some(class) = self.class {
debug.field("class", &format_args!("{class:?}"));
}
if !self.values.is_empty() {
debug.field("values", &format_args!("{:?}", self.values));
}
debug.finish()
}
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)?;
for (key, value) in self.values.iter() {
write!(f, ", {:?}={value}", DebugKey(key))?;
}
Ok(())
}
}
impl std::error::Error for Message {}
impl From<Cow<'static, str>> for Message {
fn from(message: Cow<'static, str>) -> Self {
Self::new(message)
}
}
impl From<String> for Message {
fn from(message: String) -> Self {
Self::new(message)
}
}
impl From<&'static str> for Message {
fn from(message: &'static str) -> Self {
Self::new(message)
}
}
pub fn validation(message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).validation()
}
pub fn corruption(message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).corrupted()
}
pub fn not_found(message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).not_found()
}
pub fn retryable(message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).retryable()
}
pub fn cancelled(message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).cancelled()
}
pub fn permission_denied(message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).permission_denied()
}
pub fn unauthenticated(message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).unauthenticated()
}
pub fn conflict(message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).conflict()
}
pub fn unsupported(message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).unsupported()
}
pub fn resource_exhaustion(kind: ResourceExhaustionKind, message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).resource_exhaustion(kind)
}
pub fn allocation_limit(message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).allocation_limit()
}
pub fn allocation_failure(message: impl Into<Cow<'static, str>>) -> Message {
Message::new(message).allocation_failure()
}
#[derive(Clone, PartialEq)]
#[non_exhaustive]
pub enum MetadataValue {
Bool(bool),
I64(i64),
U64(u64),
F64(f64),
String(String),
Bytes(Vec<u8>),
Path(PathBuf),
}
impl fmt::Debug for MetadataValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MetadataValue::Bool(value) => write!(f, "Bool({value:?})"),
MetadataValue::I64(value) => write!(f, "I64({value:?})"),
MetadataValue::U64(value) => write!(f, "U64({value:?})"),
MetadataValue::F64(value) => write!(f, "F64({value:?})"),
MetadataValue::String(value) => write!(f, "String({value:?})"),
MetadataValue::Bytes(value) => {
f.write_str("Bytes(")?;
fmt::Debug::fmt(&DebugBytes(value), f)?;
f.write_str(")")
}
MetadataValue::Path(value) => write!(f, "Path({value:?})"),
}
}
}
impl fmt::Display for MetadataValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MetadataValue::Bool(value) => fmt::Display::fmt(value, f),
MetadataValue::I64(value) => fmt::Display::fmt(value, f),
MetadataValue::U64(value) => fmt::Display::fmt(value, f),
MetadataValue::F64(value) => fmt::Display::fmt(value, f),
MetadataValue::String(value) => fmt::Debug::fmt(value, f),
MetadataValue::Bytes(value) => fmt::Debug::fmt(&DebugBytes(value), f),
MetadataValue::Path(value) => fmt::Debug::fmt(value, f),
}
}
}
macro_rules! from {
($variant:ident: $($ty:ty),+ $(,)?) => {
$(impl From<$ty> for MetadataValue {
fn from(value: $ty) -> Self {
Self::$variant(value.into())
}
})+
};
}
from!(Bool: bool);
from!(I64: i8, i16, i32, i64);
from!(U64: u8, u16, u32, u64);
from!(F64: f32, f64);
from!(String: String, &str);
from!(Bytes: Vec<u8>, &[u8]);
#[cfg(feature = "bstr")]
from!(Bytes: bstr::BString);
from!(Path: PathBuf, &std::path::Path);
#[cfg(feature = "bstr")]
impl From<&bstr::BStr> for MetadataValue {
fn from(value: &bstr::BStr) -> Self {
Self::Bytes(value.to_vec())
}
}
impl From<usize> for MetadataValue {
fn from(value: usize) -> Self {
Self::U64(value as u64)
}
}
impl From<isize> for MetadataValue {
fn from(value: isize) -> Self {
Self::I64(value as i64)
}
}
struct DebugBytes<'a>(&'a [u8]);
impl fmt::Debug for DebugBytes<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("\"")?;
let mut bytes = self.0;
while !bytes.is_empty() {
let (text, invalid) = match std::str::from_utf8(bytes) {
Ok(text) => (text, &[][..]),
Err(err) => {
let (valid, rest) = bytes.split_at(err.valid_up_to());
let text = std::str::from_utf8(valid).map_err(|_| fmt::Error)?;
(text, &rest[..err.error_len().unwrap_or(rest.len())])
}
};
for ch in text.chars() {
match ch {
'\0' => f.write_str("\\0")?,
'\x01'..='\x7f' => write!(f, "{}", (ch as u8).escape_ascii())?,
_ => write!(f, "{}", ch.escape_debug())?,
}
}
for byte in invalid {
write!(f, "\\x{byte:02x}")?;
}
bytes = &bytes[text.len() + invalid.len()..];
}
f.write_str("\"")
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "bstr")]
#[test]
fn bstr_inputs_convert_to_byte_metadata() {
let input = b"ref\xff";
let owned = bstr::BString::from(input.as_slice());
let allocation = owned.as_ptr();
let value = super::Message::new("invalid input")
.with_input(owned)
.values
.remove("input");
let Some(super::MetadataValue::Bytes(bytes)) = value else {
panic!("owned byte strings must become byte metadata");
};
assert_eq!(bytes, input, "owned input retains every byte");
assert_eq!(bytes.as_ptr(), allocation, "owned conversion reuses the allocation");
assert_eq!(
super::Message::new("invalid input")
.with_input(bstr::BStr::new(input))
.values["input"],
super::MetadataValue::Bytes(bytes),
"borrowed byte strings convert directly without losing invalid UTF-8"
);
}
#[test]
fn byte_metadata_preserves_and_formats_input() {
let input = b"hello\0\n\"'\\\xff\xf0\x9f";
let value = super::MetadataValue::from(input.as_slice());
let super::MetadataValue::Bytes(bytes) = &value else {
panic!("byte input must remain byte metadata");
};
assert_eq!(bytes.as_slice(), input, "metadata retains the exact input bytes");
assert_eq!(
format!("{value}"),
r#""hello\0\n\"\'\\\xff\xf0\x9f""#,
"display escapes control characters and truncated UTF-8 without data loss"
);
}
#[cfg(feature = "bstr")]
#[test]
fn dependency_free_byte_formatting_matches_bstr() {
for bytes in [
Vec::new(),
(0..=u8::MAX).collect(),
"你好\u{fffd}\u{200d}\n\0\"'\\".as_bytes().to_vec(),
b"valid\xf0\x9f\x92\xa9\xff\xe2\x82text\xc0\xaf\xed\xa0\x80\xf0\x9f".to_vec(),
] {
assert_eq!(
format!("{:?}", super::DebugBytes(&bytes)),
format!("{:?}", bstr::BStr::new(&bytes)),
"dependency-free formatting preserves UTF-8 and escapes invalid bytes like bstr"
);
}
}
}