Rust Feature Flags
Table of Contents
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.