I'm a software engineer in San Francisco, currently working on a platform to streamline release management for VPCs: Bottlerocket.

Posts

  1. Another Rust macro use case: concise newtypes
  2. A Rust macros use case: Tightly-coupled API definitions for the client and server

Another Rust macro use case: concise newtypes

A footgun I hit recently in my current Rust project is if I have a few arguments of the same type for a function, I sometimes can mix up the order.

pub fn mint_session(
    user_id: uuid::Uuid,
    org_id: uuid::Uuid,
) -> Session {
    ...
}

let session = mint_session(org_id, user_id); // compiles

There’s no keyword arguments in Rust, and creating a struct for each of these functions seems unnecessarily verbose.

I could just create a type alias, but that actually doesn’t provide any type safety!

type UserId = uuid::Uuid;
type OrgId = uuid::Uuid;

pub fn mint_session(
    user_id: UserId,
    org_id: OrgId,
) -> Session {
    ...
}

let session = mint_session(org_id, user_id); // compiles!

The proper way to solve this is to actually use the newtype idiom:

pub struct UserId(uuid::Uuid);
pub struct OrgId(uuid::Uuid);

pub fn mint_session(
    user_id: UserId,
    org_id: OrgId,
) -> Session {
    ...
}

let session = mint_session(org_id, user_id); // doesn’t compile!

But when using this idiom, especially when using these types to interface with sqlx and serde, defining them can be pretty verbose and resulted in a lot of repeated code across my project, so I instead opted to create this macro:

macro_rules! typed_id {
    ($name:ident) => {
        #[derive(
            Debug,
            Clone,
            Copy,
            PartialEq,
            Eq,
            Hash,
            serde::Deserialize,
            serde::Serialize,
            sqlx::Type,
        )]
        #[sqlx(transparent)]
        pub struct $name(pub uuid::Uuid);

        impl From<uuid::Uuid> for $name {
            fn from(value: uuid::Uuid) -> Self {
                Self(value)
            }
        }

        impl From<$name> for uuid::Uuid {
            fn from(value: $name) -> Self {
                value.0
            }
        }
    };
}

And now I can just create these handy newtypes very concisely without having to unpack tuples or deconstruct when having to extract the underlying value:

typed_id!(UserId);
typed_id!(OrgId);

A Rust macros use case: Tightly-coupled API definitions for the client and server

I’m working on a Kubernetes operator and an API server for it to interface with. Both of these are crates in the same Rust workspace. The motivation for this was that I wanted to define all of the types used by the two services in one place, and keep the services tightly-coupled. When writing the operator, I wrote a protocol to define the HTTP requests the operator sends to the server:

pub trait ApiPath {
    type Request: serde::Serialize;
    type Response: serde::de::DeserializeOwned;

    const METHOD: Method;
    const PATH: &'static str;
}

So that I could define paths/endpoints like this:

pub async fn send<P: ApiPath>(&self, body: P::Request) -> Result<P::Response, reqwest::Error> {
        let url = format!("https://{}/{}", self.host, P::PATH);

        self.client
            .request(P::METHOD, url)
            .bearer_auth(&self.token)
            .json(&body)
            .send()
            .await?
            .error_for_status()?
            .json::<P::Response>()
            .await
}

This worked super well when implementing the operator. All I had to do was call this generic send function with a struct that implemented ApiPath, which reduced a lot of boilerplate code, and let me define the method, body type, response type and path all in one place for each endpoint.

When I started implementing the API server, which I used the axum crate for, I was having a hard time adding the routing/handling in an elegant way using the protocol I created for the operator. When defining a route in axum, you usually do something like this:

axum::Router::new().route("/poll", axum::routing::get(poll));

The issue here though is that I’m now defining the path and method for each endpoint in a different place. I could do something like this, but I don’t really like how I’m specifying the endpoint struct in two places:

axum::Router::new().route(Poll::PATH, method_to_handler_func(Poll::METHOD)(handler));

I was only vaguely familiar with macros and thought I’d see if this would be a good use case. After some iteration I got this:

macro_rules! into_route {
    ($path:ty, $handler:expr) => {
        axum::Router::new().route(<$path>::PATH, from_method(<$path>::METHOD, $handler))
    };
}
* from_method just matches the http::Method to the axum handler, e.g. axum::routing::get.

Now I can just define my router like this:

let operator_routes = axum::Router::new()
    .merge(into_route!(Poll, poll))
    .merge(into_route!(ListImages, list_images))

All I have to do is pass in the struct implementing the ApiPath protocol and the handler function! This was my first practical use of macros, and I thought it was interesting enough to share.