DEV Community

Cover image for My Tiny Rust Utils, Part 2: helpers.rs
icsboyx
icsboyx

Posted on

My Tiny Rust Utils, Part 2: helpers.rs

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()))
    }
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

This is almost embarrassingly tiny.

Still, I like it.

It lets me write:

Some(subscription_request.json_value()?)
Enter fullscreen mode Exit fullscreen mode

instead of:

Some(serde_json::to_value(subscription_request)?)
Enter fullscreen mode Exit fullscreen mode

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(),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)