Rust async abstraction pattern

Submitted by Erik Wegner on
Aufmacherbild

The Rust programming language is getting much attention these days. Not only in my opinion it has so many advantages that learning this language pays off soon, even the learning curve is steep and some familiar patters need changes.

One of the recently missed pattern is interface like abstraction (which is available in Rust), but with async methods.

Given there is some kind of database or file system data retrieval method:

A simple implementation
struct FileImpl {
file_field: String,
}

impl FileImpl {
fn new(info: &str) -> FileImpl {
FileImpl {
file_field: info.to_owned()
}
}

async fn get_from_file(&self) -> String {
sleep(Duration::from_millis(10)).await;

format!("asyncfn_file {}", self.file_field)
}
}

During testing, this implementation may be hard to instantiate or this runtime dependency is not wanted at all. It needs to be abstracted.

Impossible at the moment (Rust 1.65)
trait MyAbstraction {
async fn f() {}
}

To mitigate that restriction, I found this implementation. It uses an enum, which is an excellent implementation within the Rust language: an enum can be used to describe all valid data states and you can rely on all dependent fields being there.

Abstraction with an enum
use std::time::Duration;

// Import tokio to do something async
use tokio::time::sleep;

/*
cfg(test) ensures, that part of source code is available
during tests but not during normal compile mode.
*/

// A struct for test purposes only
#[cfg(test)]
struct MemImpl {
memory_field: String,
}

// An implementation for test purposes only
#[cfg(test)]
impl MemImpl {
fn new(info: &str) -> MemImpl {
MemImpl {
memory_field: info.to_owned()
}
}

async fn get_from_mem(&self) -> String {
sleep(Duration::from_millis(10)).await;

format!("asyncfn_mem {}", self.memory_field)
}
}

// A struct to mimic any async resource
struct FileImpl {
file_field: String,
}

// An implementation for the async resource
impl FileImpl {

// Constructor
fn new(info: &str) -> FileImpl {
FileImpl {
file_field: info.to_owned()
}
}

// The async data retrieval
async fn get_from_file(&self) -> String {
sleep(Duration::from_millis(10)).await;

format!("asyncfn_file {}", self.file_field)
}
}

// The abstraction enum, acts like an interface in Java
enum MyAbstraction {
// The runtime dependency
FileAdapter(FileImpl),
#[cfg(test)]
// available only during tests through annotation
MemoryAdapter(MemImpl),
}

// The abstraction implementation
impl MyAbstraction {
// The delegation switch
async fn get(&self) -> String {
match self {
// Switch to runtime adapter
MyAbstraction::FileAdapter(fa) => fa.get_from_file().await,
#[cfg(test)]
// Switch to testing adapter, available only during tests through annotation
MyAbstraction::MemoryAdapter(ma) => ma.get_from_mem().await,
}
}
}

// The method to be tested
async fn use_interface(i: MyAbstraction) -> String {
i.get().await
}

// Use https://tokio.rs/ as async runtime
#[tokio::main]
async fn main() {
let fi = FileImpl::new("a file");
let a = MyAbstraction::FileAdapter(fi);
print!("Result: {}", use_interface(a).await);

// 👇 Does not work, because of cfg(test)
// let mi = MemImpl::new("a file");
// let a = MyAbstraction::MemoryAdapter(mi);
// print!("Result: {}", use_interface(a).await);
}

#[cfg(test)]
mod tests {
use crate::{MyAbstraction, use_interface, FileImpl, MemImpl};

// A test that relies on the expensive dependency
#[tokio::test]
async fn test_file() {
let fi = FileImpl::new("data from a file");
let a = MyAbstraction::FileAdapter(fi);

let result = use_interface(a).await;

assert_eq!(result, "asyncfn_file data from a file");
}

// A test that uses the lightweight memory adapter
#[tokio::test]
async fn test_mem() {
let mi = MemImpl::new("reads from memory");
let a = MyAbstraction::MemoryAdapter(mi);

let result = use_interface(a).await;

assert_eq!(result, "asyncfn_mem reads from memory");
}
}
Cargo.toml
[package]
name = "abstractionexample"
version = "1.0.0"
edition = "2021"

[dependencies]
tokio = { version = "1.0", features = ["full"] }

Update 2023-01-17:

There is the article Ferris Talk #13: Rust-Web-APIs und Mocking mit Axum using the crate async-trait to describe an easier way.

Associated categories