Dissecting a Rust Program | Axum & Sled

I’ve chosen a project on GitHub to pick apart and study to learn some new code, crates and ideas.

The project is : https://github.com/kyoheiu/url-shortener-axum-sled

I chose it because I want to learn more about Sled as it’s recommended to be used with BDK (Bitcoin Development Kit)

study_rust_program

First, let’s have a look at Cargo.toml and see if there are any crates that we haven’t used before:

Webdock – Fast Cloud VPS Linux Hosting

Cargo.toml

[dependencies]
axum = {version = "0.5.15", features = ["macros"]}
env_logger = "0.9.0"
http = "0.2.8"
log = "0.4.17"
nanoid = "0.4.0"
serde = {version = "1.0.144", features = ["derive"]}
serde_json = "1.0.85"
sled = "0.34.7"
tokio = {version = "1.21.0", features = ["full"]}

nanoid is one that I’ve not seen before, it is a third-party crate that provides functionality for generating unique, concise IDs. Use nanoid::nanoid!(8) to generate an 8-character UUID.

Overview of the Rust code

What does the code do?

I cloned the repo, installed it, and with no Readme, I checked the code, notably the port number, 8080, and the endpoints.

Here’s the cURL command:

curl -X POST -H "Content-Type: application/json" -d '{"url": "https://example.com"}' 'http://localhost:8080/shorten'
V9uRQ8Fl
❯ RUST_LOG=info cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `/home/rag/Documents/rust/url-shortener-axum-sled/target/debug/url-shortener`
[2023-11-30T16:28:42Z INFO  url_shortener] Server started.
[2023-11-30T16:28:42Z INFO  url_shortener] Database started.
[2023-11-30T16:28:50Z INFO  url_shortener::router] Payload { url: "https://example.com" }
[2023-11-30T16:28:50Z INFO  url_shortener::router] key: oYy3jmU2, value: [104, 116, 116, 112, 115, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109]

How does it work?

.
├── Cargo.lock
├── Cargo.toml
├── my_db
│   ├── blobs
│   ├── conf
│   ├── db
│   └── snap.0000000000000114
├── sample.json
├── src
│   ├── main.rs
│   ├── my_db
│   └── router.rs
└── target
    ├── CACHEDIR.TAG
    └── debug

6 directories, 9 files
mod router;

use axum::{
    routing::{get, post},
    Extension, Router,
};
use log::info;
use router::*;

#[tokio::main]
async fn main() {
    env_logger::init();
    info!("Server started.");

    let db: sled::Db = sled::open("my_db").unwrap();
    info!("Database started.");

    let app = Router::new()
        .route("/", get(health))
        .route("/shorten", post(shorten))
        .route("/redirect/:id", get(redirect))
        .layer(Extension(db));

    axum::Server::bind(&"0.0.0.0:8080".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Observations:

The route handlers are in a separate file “route.rs” which keeps main nice and small and neat

See line 8 above, the asterisk allows use of all public functions in router.rs

use router::*;

Axum’s “Extension” is being used so we can talk to a database.

Logging is included in the code, as we’ve already seen from the output above.

Sled is the database, note it uses “open” rather than “new” ?

   let db: sled::Db = sled::open("my_db").unwrap();

Routes

use axum::{debug_handler, extract::Json, extract::Path, response::Redirect, Extension};
use log::info;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct Payload {
    url: String,
}

#[debug_handler]
pub async fn health() -> String {
    "Hello, developer.".to_string()
}

#[debug_handler]
pub async fn shorten(Json(payload): Json<Payload>, Extension(db): Extension<sled::Db>) -> String {
    info!("{:?}", payload);
    let mut uuid = nanoid::nanoid!(8);
    while db.contains_key(&uuid).unwrap() {
        uuid = nanoid::nanoid!(8);
    }
    let url_as_bytes = payload.url.as_bytes();
    db.insert(&uuid, url_as_bytes).unwrap();
    info!("key: {}, value: {:?}", uuid, url_as_bytes);
    assert_eq!(&db.get(uuid.as_bytes()).unwrap().unwrap(), url_as_bytes);
    uuid
}

#[debug_handler]
pub async fn redirect(Path(id): Path<String>, Extension(db): Extension<sled::Db>) -> Redirect {
    match &db.get(&id).unwrap() {
        Some(url) => {
            let url = String::from_utf8(url.to_vec()).unwrap();
            info!("URL found: {:#?}", url);
            Redirect::to(&url)
        }
        None => {
            info!("URL not found.");
            Redirect::to("/")
        }
    }
}

Webdock – Fast Cloud VPS Linux Hosting

Use axum, log, serde

axum::{debug_handler, extract::Json, extract::Path, response::Redirect, Extension}:

  • axum: This is the main crate for the Axum web framework in Rust.
  • debug_handler: used for handling errors or debugging information in the context of the Axum framework.
  • extract::Json: used for extracting JSON data from an HTTP request. Axum provides extractors that help to parse and handle incoming data from HTTP requests.
  • extract::Path: used for extracting path parameters from an HTTP request. Path parameters are typically used to capture variable parts of the URL.
  • response::Redirect: used to send an HTTP redirect response. Redirect responses instruct the client’s browser to navigate to a different URL.
  • Extension: for storing and retrieving additional data associated with a request or response.

log::info:

  • log: provides a flexible logging infrastructure.
  • info: This is a logging macro that logs an informational message. The level info is typically used for non-error messages to provide information about the application’s state or events.

serde::Deserialize:

  • serde: This is a popular Rust crate for serializing and deserializing data. It helps with converting between Rust data structures and JSON.
  • Deserialize: This is a trait from serde that is implemented for types that can be deserialized from a certain format, such as JSON.
use axum::{debug_handler, extract::Json, extract::Path, response::Redirect, Extension};
use log::info;
use serde::Deserialize;

Debug Handler

Generates better error messages when applied handler functions.

Payload

#[derive(Debug, Deserialize)]
pub struct Payload {
    url: String,
}

Functions

fn shorten

#[debug_handler]
pub async fn shorten(Json(payload): Json<Payload>, Extension(db): Extension<sled::Db>) -> String {
    info!("{:?}", payload);
    let mut uuid = nanoid::nanoid!(8);
    while db.contains_key(&uuid).unwrap() {
        uuid = nanoid::nanoid!(8);
    }
    let url_as_bytes = payload.url.as_bytes();
    db.insert(&uuid, url_as_bytes).unwrap();
    info!("key: {}, value: {:?}", uuid, url_as_bytes);
    assert_eq!(&db.get(uuid.as_bytes()).unwrap().unwrap(), url_as_bytes);
    uuid
}

This code appears to define an asynchronous function named shorten that serves as a URL shortening endpoint. Let’s break it down step by step:

  1. Attributes:
   #[debug_handler]

This indicates that the function is annotated with a custom attribute debug_handler. Custom attributes are typically used for metadata or code generation purposes.

  1. Function Signature:
   pub async fn shorten(Json(payload): Json<Payload>, Extension(db): Extension<sled::Db>) -> String {
  • pub async: This function is marked as public and asynchronous.
  • Json(payload): The function takes a single parameter named payload, which is annotated with Json. This suggests that the payload is expected to be in JSON format.
  • Extension(db): Another parameter named db is taken as an extension, indicating some external dependency, and it is expected to be of type sled::Db.
  1. Function Body:
   info!("{:?}", payload);

Logs the payload to some logging infrastructure. The info! macro is likely part of a logging framework.

   let mut uuid = nanoid::nanoid!(8);
   while db.contains_key(&uuid).unwrap() {
       uuid = nanoid::nanoid!(8);
   }

Generates a random 8-character UUID using the nanoid crate and ensures that it doesn’t collide with existing keys in the database. This is done by repeatedly generating a new UUID until a unique one is found.

   let url_as_bytes = payload.url.as_bytes();
   db.insert(&uuid, url_as_bytes).unwrap();

Converts the URL from the payload into bytes and inserts it into the database with the generated UUID as the key.

   info!("key: {}, value: {:?}", uuid, url_as_bytes);

Logs the key-value pair that was just inserted into the database.

   assert_eq!(&db.get(uuid.as_bytes()).unwrap().unwrap(), url_as_bytes);

Performs an assertion to ensure that the value retrieved from the database matches the URL bytes that were just inserted. If they don’t match, this assertion will fail.

   uuid

Finally, the UUID is returned as a String, presumably as the shortened URL.

In summary, this code defines an asynchronous function for URL shortening. It generates a unique short key (UUID), inserts the original URL into a database, logs the key-value pair, asserts that the retrieval from the database matches the inserted value, and then returns the shortened URL. The code assumes certain external dependencies like the nanoid crate for generating UUIDs and a database (sled::Db).

fn redirect

#[debug_handler]
pub async fn redirect(Path(id): Path<String>, Extension(db): Extension<sled::Db>) -> Redirect {
    match &db.get(&id).unwrap() {
        Some(url) => {
            let url = String::from_utf8(url.to_vec()).unwrap();
            info!("URL found: {:#?}", url);
            Redirect::to(&url)
        }
        None => {
            info!("URL not found.");
            Redirect::to("/")
        }
    }
}
  1. Attributes:
  • #[debug_handler]: This seems to be a custom attribute, possibly specific to the application or a web framework you’re using. Attributes in Rust are used to add metadata or annotations to items like functions, structs, or modules.
  1. Function Signature:
  • pub async fn redirect(Path(id): Path<String>, Extension(db): Extension<sled::Db>) -> Redirect:
    • async fn: This function is asynchronous, meaning it can perform non-blocking operations.
    • Path(id): Path<String>: It takes a String parameter named id extracted from the path.
    • Extension(db): Extension<sled::Db>: It takes a parameter named db of type sled::Db as an extension to the request.
  1. Database Lookup:
  • db.get(&id).unwrap(): It retrieves a value from the sled::Db database associated with the provided id. The use of unwrap() means it assumes the result is always Some, and it will panic if it’s None.
  1. Matching on Result:
  • match &db.get(&id).unwrap() { ... }: It matches on the result of the database lookup.
  • Some(url) => { ... }: If a URL is found in the database, it proceeds with the block inside, where it converts the byte vector url to a String and logs it. Then, it returns a Redirect::to(&url).
  • None => { ... }: If no URL is found, it logs that information and returns a redirect to “/” (the root path).
  1. Logging:
  • info!("URL found: {:#?}", url): This logs the found URL using the info! macro. The format suggests that a logging framework or macro system (possibly from the log crate) is in use.
  1. Redirecting:
  • Redirect::to("/"): If the URL is not found, it returns a redirect to the root path (“/”).

This code is a route handler for a web application using Axum, and it handles redirecting based on the id path parameter and a sled::Db extension. Make sure to handle errors more gracefully in a production setting and consider using proper error handling instead of unwrapping.