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)
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 levelinfo
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:
- 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.
- 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 namedpayload
, which is annotated withJson
. This suggests that the payload is expected to be in JSON format.Extension(db)
: Another parameter nameddb
is taken as an extension, indicating some external dependency, and it is expected to be of typesled::Db
.
- 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("/")
}
}
}
- 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.
- 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 aString
parameter namedid
extracted from the path.Extension(db): Extension<sled::Db>
: It takes a parameter nameddb
of typesled::Db
as an extension to the request.
- Database Lookup:
db.get(&id).unwrap()
: It retrieves a value from thesled::Db
database associated with the providedid
. The use ofunwrap()
means it assumes the result is alwaysSome
, and it will panic if it’sNone
.
- 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 vectorurl
to aString
and logs it. Then, it returns aRedirect::to(&url)
.None => { ... }
: If no URL is found, it logs that information and returns a redirect to “/” (the root path).
- Logging:
info!("URL found: {:#?}", url)
: This logs the found URL using theinfo!
macro. The format suggests that a logging framework or macro system (possibly from thelog
crate) is in use.
- 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.