VOOZH about

URL: https://dev.to/icsboyx/my-tiny-rust-utils-part-2-helpersrs-5el0

⇱ My Tiny Rust Utils, Part 2: helpers.rs - DEV Community


After logging, the next kind of repetition that starts bothering me is usually this:

  • adding context to errors
  • converting typed values into JSON request bodies

Neither of those is dramatic on its own.

But if I leave them totally inline, they spread everywhere.

That is why src/utils/helpers.rs exists.

WithLocation

This is the first helper:

pub trait WithLocation<T> {
 fn with_location(self) -> anyhow::Result<T>;
 fn with_location_msg(self, msg: &str) -> anyhow::Result<T>;
}

impl<T, E> WithLocation<T> for std::result::Result<T, E>
where
 E: Into<anyhow::Error>,
{
 #[track_caller]
 fn with_location(self) -> anyhow::Result<T> {
 let loc = Location::caller();

 self.map_err(Into::into)
 .with_context(|| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
 }

 #[track_caller]
 fn with_location_msg(self, msg: &str) -> anyhow::Result<T> {
 let loc = Location::caller();

 self.map_err(Into::into)
 .with_context(|| format!("{} at {}:{}:{}", msg, loc.file(), loc.line(), loc.column()))
 }
}

I like this because it captures something I personally care about when debugging:

not only what failed, but where I decided to wrap or reinterpret that failure.

That is often the more useful location for me.

Why I still keep it as a helper

Yes, I could just write .context(...) everywhere.

Sometimes I do.

But I know myself well enough: if the repetitive part feels slightly annoying, I will postpone it in one or two places, and then the code becomes uneven.

This helper lowers that friction just enough that I actually use it.

That is a recurring theme in this utils crate, honestly. I am not searching for elegance as much as I am trying to remove small excuses.

IntoJsonValue

The second helper is very small:

pub trait IntoJsonValue {
 fn json_value(self) -> serde_json::Result<Value>
 where
 Self: Sized + Serialize,
 {
 serde_json::to_value(self)
 }
}

impl<T> IntoJsonValue for T {}

This is almost embarrassingly tiny.

Still, I like it.

It lets me write:

Some(subscription_request.json_value()?)

instead of:

Some(serde_json::to_value(subscription_request)?)

That difference is not huge.

But I think a lot of utility code lives exactly in that zone: not essential, not magical, just a little nicer to use.

TrackError

The same file also has a very small custom error type:

#[derive(Debug)]
pub struct TrackError {
 pub location: &'static std::panic::Location<'static>,
 pub message: String,
}

impl TrackError {
 #[track_caller]
 pub fn new(message: impl Into<String>) -> Self {
 Self {
 location: std::panic::Location::caller(),
 message: message.into(),
 }
 }
}

I keep it around because sometimes I want something between:

  • a full error enum
  • and a generic error chain

Sometimes I just want a normal error value with a message and a caller location.

This gives me that, without pretending to be more important than it is.

Moving to Part 3

Once logs and helper traits are in place, the next problem is usually state.

Small apps still need to save things. Tokens, config-like values, maybe a bit of local persistence.

That is where save_load.rs comes in.