Components from the Tokyo stack have been a frequent topic in the Ferris Talk. Among them is Axum, a framework for developing web APIs. Axum is in November released in version 0.6. An important change is the improvement of type safety regarding the shared state. So far, the recommendation has been that Extension-Middleware and the Extension-Extractor to useto split the state.


In this column, the two Rust experts Rainer Stropek and Stefan Baumgartner would like to take turns reporting regularly on innovations and background information in the Rust area. It helps teams already using Rust stay current. Beginners get deeper insights into how Rust works through the Ferris Talks.

However, these components did not ensure the consistency of the types at compile time. If someone forgot the middleware or provided the wrong types, the extractor would throw a runtime error. In Axum 0.6 the state extractor was introducedwhich eliminates this weakness.

This episode of the Ferris Talk shows what improved state handling in Axum looks like, beyond trivial use cases. The examples demonstrate how to interact with Rust traits and the mocking framework mockall Have unit tests implemented for the Axum API, where mock objects replace underlying layers like database access during testing.

The following example first illustrates the Axum innovations and then shows the decoupling of the layers with Rust traits step by step. The code snippets embedded in the text only cover the code parts that are relevant to the topic. All executable code can be found on GitHub.

The sample code is a prototype of a simple web API for managing superheroes with Rust and Axum. It is initially intended to implement the search for heroes by their names. Other API functions would follow the same scheme. A limitation of the prototype is that it does not actually access a database. Nevertheless, the code should contain all precautions to use asynchronous database access, for example to add to the sqlx crate.

It is important to keep web API code and database access code separate. This is essential in the example for using mock objects and unit tests, but would also make sense for a regular application. The following code shows the data access layer of the prototype. See the comments for implementation details.

/// The model for our API. We are maintaining heroes.
#[derive(Serialize)]
pub struct Hero {
  pub id: &'static str,
  pub name: &'static str,
}

/// Represents an error that happened during data access.
enum DataAccessError {
  NotFound,
  #[allow(dead_code)]
  TechnicalError,
  #[allow(dead_code)]
  OtherError,
}

/// Dummy implementation for our repository
/// 
/// In real life, this repository would access a 
/// database with persisted heroes. However, for
/// the sake of simplicity, we just simulate 
/// database access by using an in-memory
/// collection of heroes.
struct HeroesRepository();

impl HeroesRepository {
  async fn get_by_name(&self, name: &str) 
    -> Result<Vec<Hero>, DataAccessError> {
    // We don't use a database in this example,
    // but simulate one.
    // Our data is just a const array of heroes:
    const HEROES: [Hero; 2] = [
      Hero {
        id: "1",
        name: "Wonder Woman",
      },
      Hero {
        id: "2",
        name: "Deadpool",
      },
    ];

    // Simulate database access by doing 
    // an async wait operation.
    time::sleep(Duration::from_millis(100)).await;

    // As we do not have a real database, we just 
    // filter the heroes array in memory. Note
    // that we simulate the DB behaviour of `LIKE`
    // and the % wildcard at the end of the name 
    // filter by using the `starts_with` method.
    let found_heroes = HEROES
      .into_iter()
      .filter(|hero| {
        if let Some(stripped_name) = 
          name.strip_suffix('%') {
          // We have a % at the end of the name 
          // filter. This means that we have to use
          // the `starts_with` method to simulate
          // the `LIKE` behaviour of the DB.
          hero.name.starts_with(stripped_name)
        } 
        else {
          // No % at the end, so we can use 
          // the `==` operator.
          hero.name == name
        }
      })
      .collect::<Vec<Hero>>();

    if found_heroes.is_empty() {
      // We did not find any heroes. This means 
      // that we have to return a "not found" error.
      Err(DataAccessError::NotFound)
    } 
    else {
      // We found some heroes. Return them.
      Ok(found_heroes)
    }
  }
}

It is particularly important to note that the method get_by_name as return type Result<vec, DataAccessError> used, i.e. error states can be reported back to the caller of the method. It will be up to the API handler function to convert these error conditions into appropriate HTTP response messages. This will later play an important role in automated testing.

To home page

California18

Welcome to California18, your number one source for Breaking News from the World. We’re dedicated to giving you the very best of News.

Leave a Reply