#![cfg_attr(unstable_never_type, feature(never_type))]
#![cfg_attr(unstable_try_trait_v2, feature(try_trait_v2))]
use std::ops::{ControlFlow, FromResidual, Try};
use tracing::Level;
use try_v2::{Extract, Transform};
use crate::TracingConfig::{OnOutput, OnResidual};
pub trait Trace<T: Try> {
fn or_error(self, name: &'static str) -> Traced<T>;
fn and_error(self, name: &'static str) -> Traced<T>;
fn or_warn(self, name: &'static str) -> Traced<T>;
fn and_warn(self, name: &'static str) -> Traced<T>;
fn or_info(self, name: &'static str) -> Traced<T>;
fn and_info(self, name: &'static str) -> Traced<T>;
fn or_debug(self, name: &'static str) -> Traced<T>;
fn and_debug(self, name: &'static str) -> Traced<T>;
fn or_trace(self, name: &'static str) -> Traced<T>;
fn and_trace(self, name: &'static str) -> Traced<T>;
}
impl<T: Try> Trace<T> for T {
fn or_error(self, name: &'static str) -> Traced<T> {
Traced {
inner: self,
event: TracingConfig::OnResidual(Event {
level: Level::ERROR,
message: name,
}),
}
}
fn and_error(self, name: &'static str) -> Traced<T> {
Traced {
inner: self,
event: TracingConfig::OnOutput(Event {
level: Level::ERROR,
message: name,
}),
}
}
fn or_warn(self, name: &'static str) -> Traced<T> {
Traced {
inner: self,
event: TracingConfig::OnResidual(Event {
level: Level::WARN,
message: name,
}),
}
}
fn and_warn(self, name: &'static str) -> Traced<T> {
Traced {
inner: self,
event: TracingConfig::OnOutput(Event {
level: Level::WARN,
message: name,
}),
}
}
fn or_info(self, name: &'static str) -> Traced<T> {
Traced {
inner: self,
event: TracingConfig::OnResidual(Event {
level: Level::INFO,
message: name,
}),
}
}
fn and_info(self, name: &'static str) -> Traced<T> {
Traced {
inner: self,
event: TracingConfig::OnOutput(Event {
level: Level::INFO,
message: name,
}),
}
}
fn or_debug(self, name: &'static str) -> Traced<T> {
Traced {
inner: self,
event: TracingConfig::OnResidual(Event {
level: Level::DEBUG,
message: name,
}),
}
}
fn and_debug(self, name: &'static str) -> Traced<T> {
Traced {
inner: self,
event: TracingConfig::OnOutput(Event {
level: Level::DEBUG,
message: name,
}),
}
}
fn or_trace(self, name: &'static str) -> Traced<T> {
Traced {
inner: self,
event: TracingConfig::OnResidual(Event {
level: Level::TRACE,
message: name,
}),
}
}
fn and_trace(self, name: &'static str) -> Traced<T> {
Traced {
inner: self,
event: TracingConfig::OnOutput(Event {
level: Level::TRACE,
message: name,
}),
}
}
}
#[must_use = "The wrapped type may be an `Err`, `None`, or other `Residual` which requires handling."]
pub struct Traced<T>
where
T: Try,
{
inner: T,
event: TracingConfig,
}
impl<T, O> Extract<O> for Traced<T> where T: Try<Output = O> {}
impl<T, O> Transform<O> for Traced<T> where T: Try<Output = O> {}
impl<T: Try> Try for Traced<T> {
type Output = T::Output;
type Residual = T::Residual;
fn from_output(output: Self::Output) -> Self {
Self {
inner: Try::from_output(output),
event: Default::default(),
}
}
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
match self.inner.branch() {
ControlFlow::Continue(output) => {
self.event.emit_output();
ControlFlow::Continue(output)
}
ControlFlow::Break(residual) => {
self.event.emit_residual();
ControlFlow::Break(residual)
}
}
}
}
impl<T: Try> FromResidual for Traced<T> {
fn from_residual(residual: <Self as Try>::Residual) -> Self {
Self {
inner: FromResidual::from_residual(residual),
event: Default::default(),
}
}
}
pub type TracedResult<T, E> = Traced<Result<T, E>>;
pub type TracedOption<T> = Traced<Option<T>>;
impl<T, E> TracedResult<T, E> {
pub fn err(self) -> Option<E> {
match self.branch() {
ControlFlow::Continue(_) => None,
ControlFlow::Break(Result::Err(err)) => Some(err),
}
}
pub fn ok(self) -> Option<T> {
self.output()
}
}
#[derive(Debug, Clone, Copy)]
struct Event {
level: Level,
message: &'static str,
}
impl Event {
fn emit(&self) {
match self.level {
Level::ERROR => tracing::error!("{}", self.message),
Level::WARN => tracing::warn!("{}", self.message),
Level::INFO => tracing::info!("{}", self.message),
Level::DEBUG => tracing::debug!("{}", self.message),
Level::TRACE => tracing::trace!("{}", self.message),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
enum TracingConfig {
OnOutput(Event),
OnResidual(Event),
#[default]
None,
}
impl TracingConfig {
fn emit_output(&self) {
if let OnOutput(event) = self {
event.emit();
}
}
fn emit_residual(&self) {
if let OnResidual(event) = self {
event.emit();
}
}
}
#[cfg(test)]
mod tests {
use std::io;
use tracing_test::traced_test;
use super::*;
#[traced_test]
#[test]
fn or_error_ok() {
fn no_error() -> io::Result<()> {
io::Result::Ok(()).or_error("should not log")?;
Ok(())
}
assert!(no_error().is_ok());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_error_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).or_error("error message")?;
Ok(())
}
assert!(err().is_err());
assert!(logs_contain("error message"));
}
#[traced_test]
#[test]
fn and_error_ok() {
fn ok() -> io::Result<()> {
io::Result::Ok(()).and_error("error on ok")?;
Ok(())
}
assert!(ok().is_ok());
assert!(logs_contain("error on ok"));
}
#[traced_test]
#[test]
fn and_error_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).and_error("should not log")?;
Ok(())
}
assert!(err().is_err());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_warn_ok() {
fn no_error() -> io::Result<()> {
io::Result::Ok(()).or_warn("stuff")?;
Ok(())
}
assert!(no_error().is_ok());
assert!(!logs_contain("stuff"));
}
#[traced_test]
#[test]
fn or_warn_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).or_warn("stuff")?;
Ok(())
}
assert!(err().is_err());
assert!(!logs_contain("name"));
assert!(logs_contain("stuff"));
}
#[traced_test]
#[test]
fn and_warn_ok() {
fn ok() -> io::Result<()> {
io::Result::Ok(()).and_warn("ok warn")?;
Ok(())
}
assert!(ok().is_ok());
assert!(logs_contain("ok warn"));
}
#[traced_test]
#[test]
fn and_warn_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).and_warn("should not log")?;
Ok(())
}
assert!(err().is_err());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_debug_ok() {
fn no_error() -> io::Result<()> {
io::Result::Ok(()).or_debug("should not log")?;
Ok(())
}
assert!(no_error().is_ok());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_debug_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).or_debug("debug message")?;
Ok(())
}
assert!(err().is_err());
assert!(logs_contain("debug message"));
}
#[traced_test]
#[test]
fn and_debug_ok() {
fn ok() -> io::Result<()> {
io::Result::Ok(()).and_debug("debug on ok")?;
Ok(())
}
assert!(ok().is_ok());
assert!(logs_contain("debug on ok"));
}
#[traced_test]
#[test]
fn and_debug_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).and_debug("should not log")?;
Ok(())
}
assert!(err().is_err());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_trace_ok() {
fn no_error() -> io::Result<()> {
io::Result::Ok(()).or_trace("should not log")?;
Ok(())
}
assert!(no_error().is_ok());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_trace_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).or_trace("trace message")?;
Ok(())
}
assert!(err().is_err());
assert!(logs_contain("trace message"));
}
#[traced_test]
#[test]
fn and_trace_ok() {
fn ok() -> io::Result<()> {
io::Result::Ok(()).and_trace("trace on ok")?;
Ok(())
}
assert!(ok().is_ok());
assert!(logs_contain("trace on ok"));
}
#[traced_test]
#[test]
fn and_trace_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).and_trace("should not log")?;
Ok(())
}
assert!(err().is_err());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn ok_or_ok() {
let good = io::Result::Ok(5).or_warn("should not log");
let v = good.ok();
assert_eq!(v, Some(5));
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn ok_and_ok() {
let good = io::Result::Ok(5).and_warn("should log");
let v = good.ok();
assert_eq!(v, Some(5));
assert!(logs_contain("should log"));
}
#[traced_test]
#[test]
fn err_or_err() {
let err: TracedResult<i32, _> = Err(io::Error::other("oops")).or_warn("should log");
let v = err.err();
assert_eq!(v.map(|e| e.to_string()), Some("oops".to_string()));
assert!(logs_contain("should log"));
}
#[traced_test]
#[test]
fn err_and_err() {
let err: TracedResult<i32, _> = Err(io::Error::other("oops")).and_warn("should not log");
let v = err.err();
assert_eq!(v.map(|e| e.to_string()), Some("oops".to_string()));
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn err_conversion() {
fn convert() -> Result<(), u32> {
let err: Result<(), u16> = Err(5);
err.or_error("conversion")?;
Ok(())
}
assert!(convert().is_err());
assert!(logs_contain("conversion"));
}
#[traced_test]
#[test]
fn option() {
fn opt() -> Option<usize> {
let n = Some(5).or_warn("wibble")?;
Some(n)
}
assert_eq!(opt(), Some(5));
assert!(!logs_contain("wibble"));
}
#[traced_test]
#[test]
fn none_and_then() {
let _: Option<usize> = Option::<usize>::None
.or_warn("wibble")
.and_then(|x| Some(x + 1));
assert!(logs_contain("wibble"));
}
#[traced_test]
#[test]
fn some_and_then() {
let n = Some(4).or_warn("wibble").and_then(|x| Some(x + 1));
assert_eq!(n, Some(5));
assert!(!logs_contain("wibble"));
}
#[traced_test]
#[test]
fn chain() {
fn chained() -> Option<usize> {
let n = Some(4).or_warn("empty").and_debug("full")?;
Some(n + 1)
}
assert_eq!(chained(), Some(5));
assert!(logs_contain("full"));
assert!(!logs_contain("empty"));
}
}