Tuesday 11th October 2022

When I started writing Velum, I didn’t really have any idea about what web frameworks were available, or what was good. So, somewhat at random, I chose Warp, since the examples looked fairly straightforward and it’s pretty popular.

Coming from Rails, and the almost infinitely malleable Ruby, working with Rust feels very restricted. Rust is incredibly pedantic, and there’s very little magic going on, so even seemingly trivial things like responding with different MIME types within a function depending on the input is surprisingly difficult. Sometimes it means re-thinking how your routes are architected, other times it means trying to decipher the occasionaly spartan documentation or trawling through Stack Overflow or Reddit for answers.

I persevered, though, and the fact that you’re reading this now is evidence of that. This blog is running on an engine I wrote myself, in Rust! I briefly mentioned in an earlier post about getting to a point where I’d no longer feel the need to tweak stuff, but I’m not there yet. One ‘tweak’ I wanted to make was to the core framework: replacing Warp with Axum.

Engine Swap

Much like swapping the engine of a car, changing the web framework of an application is a complex business. Lots of things work more or less the same, but the connectors are the wrong shape, or in the wrong place.

My primary motivation for this effort was a growing discomfort with Warp. It’s fine for simple things, and for sure some of this is my own inexperience with both it and Rust, but as the application grew, I started to face problems doing what I wanted that went beyond Rust’s rigid nature. Warp’s error messages, for one thing, start to get absurd when you have a large chain of route filters like this:

pub fn article_filter(codata: SharedData) -> impl Filter<
    Extract = impl warp::Reply,
    Error=warp::Rejection
> + Clone + 'static {
    let codata_filter = warp::any().map(move || codata.clone());
    let show = warp::path!("articles" / String)
        .and(warp::get())
        .and(warp::header::optional::<String>("Referer"))
        .and(warp::cookie::optional::<String>("theme"))
        .and(codata_filter.clone())
        .and_then(article_route);
    let create = warp::path!("articles")
        .and(warp::post())
        .and(warp::filters::body::bytes())
        .and(warp::body::content_length_limit(MAX_ARTICLE_LENGTH))
        .and(warp::cookie::optional::<String>("velum_session_id"))
        .and(codata_filter.clone())
        .and_then(create_article_route);
    let update = warp::path!("articles" / String)
        .and(warp::put())
        .and(warp::filters::body::bytes())
        .and(warp::body::content_length_limit(MAX_ARTICLE_LENGTH))
        .and(warp::cookie::optional::<String>("velum_session_id"))
        .and(codata_filter.clone())
        .and_then(update_article_route);
    let delete = warp::path!("articles" / String)
        .and(warp::delete())
        .and(warp::cookie::optional::<String>("velum_session_id"))
        .and(codata_filter.clone())
        .and_then(delete_article_route);

    let text = warp::path!("articles" / String / "text")
        .and(codata_filter)
        .and_then(article_text_route);

    show.or(create).or(update).or(delete).or(text)
}

It’s not an exact comparison, because a lot of the cookie and shared data stuff is done elsewhere, but this is the Axum equivalent:

 Router::new()
    .route("/",                   get(home_handler))
    .route("/articles/:page",     get(index_handler))
    .route("/articles",           post(create_article_handler))
    .route("/article/:slug",      put(update_article_handler))
    .route("/article/:slug",      get(article_handler))
    .route("/article/:slug",      delete(delete_article_handler))
    .route("/article/:slug/text", get(article_text_handler))
    .layer(Extension(shared_data))
    .layer(CookieManagerLayer::new())

(yes, I took the rewrite as an opportunity to correct the RESTfulness of the routes – articles/<slug> changes to article/<slug>, and it’s now articles/<page> instead of index/page)

Basically, Axum uses the function signature of a handler to see if it matches, and uses the function’s parameters to extract data from the request, while Warp chains a series of filters that do both route matching and request data extraction, then requires that the handler function signature match these requirements perfectly.

Like I said, some of my issues with Warp could just be inexperience, with Rust, with Warp, and with functional programming, but for now I just feel more comfortable with Axum. I do plan to continue learning about Warp from a series of YouTube videos by Jeremy Chone, even the first of which I found very useful, and it could be that I become sufficiently enlightened that I return to Warp, but for now I’ll continue to work with Axum.

Feature-incomplete

There’s still a lot of work to do on Velum. The admin page is still very basic – you can create, edit and delete articles, but there’s no way to manage stuff like images. I have some ideas in that regard, possibly involving something like React, Svelete or Vue, to really improve the admin experience. I also need to improve the way comments are managed, as at the moment they’re just appended to an in-memory list while being backed up to a JSONL file, with no means of managing them.

Then there’s the client side of things: JavaScript and CSS. Right now the blog uses a rather haphazard collection of JavaScript, some from my old Ghost blog and some written just for this one. Somewhere along the way I lost the syntax highlighting provided by Prism, too. This all needs fixing, ideally in such a way as to allow for seaparate files in development mode and a combined, minified file in release mode. The CSS is likewise rather cumbersome, as it’s just one big file per theme, and the theme system itself is currently much too hard-coded.

These and other issues yet remain, but I’m so far very pleased with the progress I’ve made, and look forward to getting Velum to a state where I’m comfortable promoting it on Reddit etc.