I’ve spent a scary amount of time with #Ruby and #RubyOnRails (nearly 2 decades at this point) and given the recent developments in the community and circumstances in general vigorously gesturing at everything I though I'd explore what's out there.

I decided to try #Rust for web dev. I have to say that I dubbed my toes regularly into Rust but never used it extensively and never learned it properly. It seems I reread the Rust Book like every three years and I still can’t fully grasp it. I did Advent of Code this year with Rust. Finished all the tasks. So I felt I might try as well something else.

Anyway, I did a quick search. I got to https://www.arewewebyet.org/ — it says “Yes! And it's freaking fast!” so I was encouraged. I looked at what it recommends and off I went.

So here's a rare thread. I’m doing a series of posts because I want to vent a bit and "be wrong on the internet" in hope that someone’s gonna set the record straight and I will learn something. A thread will hopefully make it easier for the hivemind to address individual points.

PLEASE, REPLY TO THESE POSTS.

Anyway, here we go. 🧵

Are we web yet? Yes, and it's freaking fast!

AreWeWebYet gives insight on whether you can build your latest web-project on top of Rust

Are We Web Yet?

I had to start with something. I picked #Axum. It’s one of the most popular frameworks and it's actively developed. So good enough for start.

First I have to address the “framework” here. IT terms of scope Axum is closer to Sinatra than Rails. And closer from the opposite side. It's somewhere between Rack and Sinatra.

The central part of Axum is the router and API for request-response mapping into types.

So in Ruby pretty much every web framework has some sort of Request and Response objects. They completely encapsulate both concepts and you have to stick your fingers deep into Request object to get anything you may want.

You still can do that in Axum but generally your “actions” (in Rails terminology) are just functions. And arguments to those functions pieces of the request that you need to process the request. It can be a piece of the request path (like your post id from /posts/42), or cookies, or any specific header, or parsed json request body, or form data, etc. So where in Rails you get request (or at bests params) in Axum you get very specific and typed pieces of information.

Your handler may look like this:

fn create_post(Form(post_form): Form<PostForm>, cookies: CookieJar) -> Result<(), StatusCode> {
//…
}

Now, Rails docs are great. Specifically the Guides. You can learn every piece of Rails from them. There are no guides for Axum. There are API docs. They're very different to Rails docs. It will take some time learning to read the docs. But the main learning material seems to be examples. Examples are indispensable. But they're sort of terrible to learn all the possible types the handler function can take.

I had to actually go and lear the magic that makes this typed handlers witchcraft work to understand what I can plug in there. (It's explicit type coercion and a little macro.) I'm glad I understand it now but I have to point out that I spend much longer trying to figure that out reading the docs and still didn't until I read the code. It's not a good learning experience.

#Ruby #RubyOnRails #Rust

Now, that was request. For responses it's basically the same, in a way. Your handled has to return something that can be converted into a response. It’s literally impl IntoResponse.

Axum provides implementations for a few things like tuples of relevant pieces of information. You can return a bag or response pieces and it will glue them together into a proper thing.

It can be a redirect, or a status code and HTML string (HTML(“my html”), or a JSON string (JSON(serialized_json)). You can add headers in there. It can be a Result so you can propagate errors around.

It's very neat when it clicks but it's very hard to find out what actual types have that trait implemented for. So you have to gerp examples and dive deep in to the framework code to see if the trait is implemented for what you want to return or you have to come up with your own type and implement the trait for it.

It's not a good learning experience.

#Axum #Ruby #RubyOnRails #Rust

Since we're talking about responses let’s address the elephant in the room: HTML.

In Ruby pretty much every framework support some form of templating. Even barebones Sinatra gives you ERB out of the box and pretty much any other template engine is only one line in the gemfile away.

Axum doesn't provide any templating. You’ll have to bring your own.

I picked #Askama as it was the first templating engine on Are We Web Yet.

It's a dialect of Jinja. But maybe not 100% compatible. No matter.

One thing you learn fast is that in Rust everything is a type. So your template has to have an actual template and a struct that provides data for the template. You can't just put random code in your templates. On one hand it’s great as you can't by accident have n+1 queries in your template. On the other, to change something on the page you might to edit three files: the actual template, the struct for the template, and the handler to populate the struct.

There's no support for layouts. Well, there's kinda. Through template inheritance. It’s where you extend your layout by overriding some blocks in it. It's much more awkward than in Ruby. In Ruby layouts are conceptually separate. In Askama your template struct has to provide all the data for the template and all its parent templates. I don't even know if it's possible to render the same template in different layouts. The thing that is extremely easy in Ruby.

There's also no support for partials. At best you can insert renders of other templates. But that's awkward for many of the same reasons (mostly around providing data for the templates).

One major downside is that Askama uses proc macro to compile the templates to Rust. It makes it extremely fast but debugging templates is a nightmare. It gives you an error that something is missing or has a wrong type but doesn't tell you what it is or where in the template it is. Even with enabled proc macro backtraces it gives you a snippet of generated Rust code and not the template. Even the barebones ERB will give you the exact position in the template in case of an error.

So yeah, lots of frustration.

#Axum #Ruby #RubyOnRails #Rust

BTW, there's no form builder in Axum.

I’m not very fond of Rails form builder. It's bloated. It hides a lot of HTML features. But if you need a basic form it's just magical.

Axum also can only parse flat forms. By flat I mean, that only one level of struct nesting. Nothing like nested attributes in Axum. You have to bring your own crate for that.

#Axum #Ruby #RubyOnRails #Rust

Axum also doesn't provide any DB layer. You’ll have to bring your own. (Notice the theme?)

I’ve chosen Diesel.

At the surface it's kinda similar to ActiveRecord. You see familiar terms like migrations and associations. But in reality it's very far from AR.

Again, I'm not exactly a fan of ActiveRecord but in comparison experience working with it is all sparkly unicorns and rainbows.

Let's take migrations, for example. Migrations in Diesel are raw SQL. Which is fine is you're forking on an app that will only use a single db (like your typical startup or something will settle on, say, Postgres for the main db). It’s much more awkward when you're working on an app that people might want to deploy in different environments. Like a self-hosted app that can have 1-2 users might want to use SQLite for simplicity, and same app can be used for a company of 300 and might be better served by pg (for reliability, concurrency, backups, etc.). With Diesel you’d have to write migrations for every supported db backend. In Rails you use a simple API that abstracts that away for the most part. You still can use very specific db-dependent feature, in the same migration.

Diesel provides an option to generate migrations from a schema definition. I think it's neat. You write the schema you need and Diesel would figure out what needs to be changed and puts that in a migration. The issue is that the DSL is extremely limited. You can't even define indexes with it.

Relations in Diesel are very limited as well. It's very basic “get associated records”. Transitive associations are very awkward compared to AR.

Anther snag is in Rust everything is a type. On one hand Diesel ensures that db is in the right state to work with the types in your code. So you can be sure there won't be some weird miscommunication between the app and the db that will lose your data. On the other, you can't be loose with your requests. In Rails you can requests partial rows and you still get the same models. So you can optimise your queries much easier (but you have to be careful). In Diesel every shape of the returned row has to be a specific type. You can't randomly add or remove columns from the query.

So what’s conceptually a single model in AR can be a whole bunch of structs in Diesel. And you have to pay attention where you use which.

#Axum #Ruby #RubyOnRails #Rust

@pointlessone First of all I would like to highlight that this feedback is very valuable. It would be great if you can fill feature requests for maybe the two most relevant items at the diesel github repository, so that we could discuss how to resolve them in details.
@pointlessone
That written I also would like to highlight that you are comparing a well established and rather large project (active record) with a number of comparable "younger" projects that are essentially run by single persons. Sure, you as a user expect that everything is there, but the hard truth often is: It needs someone that cares for a certain feature and that is willing to implement and maintain it or at least fund that work.

@weiznich I understand the sentiment, but—please don't take it personally—it doesn't hold.

Sure, AR is much older and much more effort went into it. But let's take a look at ROM. It started in 2012, 3 years before Diesel. But it wasn't being worked on since around 2020 so even less time went into it. It too was developed by mostly 2 people. But it seems to be much more feature rich. It's got much fancier relations. It's even got cross-db relations. It can use plain files (YAML, JSON, CSV, etc.), relational DBs, and NoSQL DBs all together, transparently. It's much rougher than AR, sure. Especially in the ecosystem department as AR has a whole bunch of third-party extensions. But it's still feels much more powerful than Diesel.

I'm not trying to bash on you or Diesel. Just pointing out that the effort argument doesn't hold that well.

It's true that I had expectation going in. It's also true that they most likely were uncalibrated.

@pointlessone The point I wanted to make still stands as far as I see. The example you mad are two people working full time on a framework. For diesel it’s more like one person working part time (like a day per week) on the project. That’s quite an important distinction in my opinion.

Also I would like to point out that people work on what they personally need: For me that’s less convenient top level methods but often low level methods and performance optimisation.

@weiznich It is an important distinction but it’s not the case with ROM. It was and still is your typical evenings and weekends open source. Anyway.
@weiznich I fully admit that I don’t have a good mental model for Diesel yet. And I understand that frustration comes from mismatched expectations and that is my doing. I try different models and see which fits or if I need a new one altogether. However, frustration is still there. Even if it’s mostly not Diesel’s fault.