Rust Feature Flags

Table of Contents

Rust Feature Flags

Background

Recently while working on a project, I stumbled upon an annoying situation. The API for a test and production client were different. The production client was immutable while the test client was mutable. They also had different methods to implement the same functionality.

I needed a way to use the same code for both clients.

Road to victory

Enum Variants

So I created an enum with two variants, Test and Production.

enum Client {
    Test(TestClient),
    Production(ProductionClient),
}

And the usage was like this:

async fn boo(client: &Client) {
    match client {
        Client::Test(client) => {
            client.do_something_1();
        }
        Client::Production(client) => {
            client.do_something_2();
        }
    }
}

Good start, but this didn’t work since TestClient needed to be mutable. Ok, let’s pass in a mutable reference.

async fn boo(client: &mut Client) {
    match client {
        Client::Test(client) => {
            client.do_something_1();
        }
        Client::Production(client) => {
            client.do_something_2();
        }
    }
}

Arc<Mutex>

That works, almost. I was using this across multiple server handlers, background workers etc. Ok let’s slap an Arc<Mutex<Client>> on it.

async fn boo(client: Arc<Mutex<Client>>) {
    let client = client.lock().await;
    match client {
        Client::Test(client) => {
            client.do_something_1();
        }
        Client::Production(client) => {
            client.do_something_2();
        }
    }
}

This works, but now every request to the client is sequential, for test I don’t mind. But for production I need concurrency, especially since the production client immutable, there is no justification for this.

#[cfg] macro

So now I tried using #[cfg(test)].

#[cfg(test)]
async fn boo(client: Arc<Mutex<TestClient>>) {
    let client = client.lock().await;
    client.do_something_1();
}

#[cfg(not(test))]
async fn boo(client: Arc<ProductionClient>) {
    client.do_something_2();
}

This was good, until I tried running my integration tests. In rust , integration don’t run in #[cfg(test)] mode. So back to the drawing board.

Feature Flags

So instead of using the standard #[cfg(test)], I used feature flags.

[features]
test-client = []

And the code changed to:

#[cfg(feature = "test-client")]
async fn boo(client: Arc<Mutex<TestClient>>) {
    let client = client.lock().await;
    client.do_something_1();
}

#[cfg(not(feature = "test-client"))]
async fn boo(client: Arc<ProductionClient>) {
    client.do_something_2();
}

So in regular mode, test-client feature is not enabled. And I just needed to add --features test-client to my cargo test commands. While this looks dirty since I have duplicate code, if I would use the same method it would still need two different internal implementations with a match clause due to differences in clients.

References