Elefren release v0.14.0

Today, hot on the heels of v0.13.0, I've released elefren 0.14.0. I normally would have waited longer between releases, but today I discovered a bug in elefren & mammut that caused a runtime failure when 3 specific API calls were made

The Bug

I noticed the bug when I went to pull down a list of the people I was following. I was doing something like:

let client = Mastodon::from(data);
client.following()?;

I saw following in the docs and just assumed it would pull down the following list for the authenticated user. So I was surprised when I got an error response from the server! Since there really isn't much to this call, or any way to configure the request, I figured maybe the API was out-of-date, or there was something wrong with my server. When I looked in the code, I found out that it was trying to call this API endpoint: /api/v1/accounts/{}/following. Did you notice the {} in there? This is a placeholder that normally would get filled in with a call to format!, so maybe the method is retrieving the account id of the authenticated user and using that to fill in the account id parameter?

Turns out, nope, it just takes that string, verbatim, and makes an HTTP call to the server. Of course, there is no account with ID {}, so the call fails.

Digging Deeper

As it turns out, there was a small bug in the macro that was generating these methods. The macro call is here:

paged_routes_with_id! {
    (get) followers: "accounts/{}/followers" => Account,
    (get) following: "accounts/{}/following" => Account,
    (get) reblogged_by: "statuses/{}/reblogged_by" => Account,
    (get) favourited_by: "statuses/{}/favourited_by" => Account,
}

As you can see, we are generating 4 methods with this macro call. The macro definition is supposed to generate methods that look like this:

pub fn followers(&self, id: &str) -> Result<Page<Account, H>> {
    // implementation here
}

In fact, for followers it did generate that. However, the other 3 methods ended up like this:

pub fn following(&self) -> Result<Account> {
    // implementation here
}

This is obviously incorrect, and is the reason that client.following() compiles fine, when the method should have been requiring an id: &str.

The Solution

The solution eventually presented itself as I was staring at the macro definition:

macro_rules! paged_routes_with_id {

    (($method:ident) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
        fn $name(&self, id: &str) -> Result<Page<$ret, H>> {
            // implementation
        }

        route!{$($rest)*}
    }
}

Do you see it? The match arm pulls out the first (get) followers: "accounts/{}/followers" => Account pattern, matching the rest of the endpoints with the $rest:tt pattern, then generates fn followers, then calls another macro to expand the $rest. However, it does not call paged_routes_with_id!{$($rest)*}, it calls route!{$($rest)*}. route is another macro that is used to generate methods, but it generates them as, you guessed it, fn method_name(&self) -> Result<Entity>, which is exactly what we were seeing with the following method.

So after changing route! to paged_routes_with_id! at the end of that macro expansion, and adding an empty match arm, the problem was fixed and the methods started working 👍.

So Why 0.14.0 and not 0.13.1?

After I fixed this bug and opened a PR for it, I realized I was going to have to release this as 0.14.0 and not 0.13.1. Rust crates usually follow strict semver when it comes to versioning, and elefren is no different. Even though this was a bugfix, it did result in a change to the public API. It may not matter too much, since anyone that was relying on the old API for these methods was relying on broken code, but still, the public API changed. As much as it pained me to do it, I released it as 0.14.0.

If you've gotten this far, thanks for reading, and be sure to follow me @balrogboogie@ceilidh.space!