00:00:08.960
All right, so I had an amazing talk prepared. I was going to announce all kinds of really cool new things, and then David gave it. What a jerk! No, I'm going to talk today about how to take the Rails CLI and make it do your bidding.
00:00:16.039
Specifically, I'll discuss how we at MongoDB took the Rails CLI and used it to serve the needs of our users that the Rails CLI itself was not able to meet. I hope that by the end of this talk, you all can feel confident to try some of this yourself and see what you can do with the Rails CLI and the tools that help build it.
00:00:31.679
I also want to mention that I really had a lot of fun with this project that I will be describing here. As a Rails core alumnus, sometimes people think I know everything about Rails, but the fact is, I'm just as delighted as the rest of you to dig in and discover new things.
00:00:49.280
Since I was on Rails core, a lot of new things have been added, so it's always a journey of discovery. I hope none of you are afraid to make that journey at any time. Let's start with some vocabulary, so you know what I'm talking about when I mention certain terms. First of all, MongoDB is a database, but it is also the company that makes the database. It's the company that pays me to write Ruby code, and I am incredibly grateful for the opportunity to make a living contributing to the Ruby community.
00:01:23.560
How many of you are familiar with MongoDB, the database? Good! And how many of you have used it in anger? How many of you have used it with joy? No need to answer that question. So yes, a lot of you are familiar with it, which is fantastic. You know that it is a document database, and it is schema-less, which will become significant shortly. Next, Mongoid is one of the products we create at MongoDB.
00:02:08.959
I work on the Ruby driver for MongoDB, but I also work on Mongoid. Mongoid is our Object Document Mapper, or ODM, which is basically Active Record for MongoDB. Now, why do we have an Active Record for MongoDB? Well, it's because Active Record only works with SQL databases. If your database doesn't speak SQL, it will not work with Active Record. And tada! That's MongoDB! So we have our answer to Active Record, which is called Mongoid.
00:02:49.480
Mongoid is built on Active Model, so it can exhibit a similar interface to Active Record. In fact, we try to be as API compatible as possible to make it easier for people transitioning from Active Record to Mongoid. A lot of the syntax is similar, but naturally, there are differences. It's the gaps between what Active Record can do and what Mongoid can do that often trips us up. Now, let's talk about Rails. How many of you have heard of Rails? Just kidding, you all know Rails.
00:03:44.480
Now, this might shock you, but Rails is opinionated. I don't know if you guys knew this, but it's pretty opinionated. Usually, that is a wonderful thing, right? That’s why we love Rails: because it has opinions. We may not always agree with them, but knowing that Rails is in good hands means we can trust it to do its job very well. It's quite frustrating when it doesn't align with my opinions. One of Rails’ opinions is that it prefers relational databases that speak SQL. For instance, we have Active Record, and on top of Active Record, you've got Active Storage, Action Mailbox, Action Text—these frameworks built on top of Active Record inherit its opinions.
00:04:39.160
If your database doesn't speak SQL, it can't play in those sandboxes. MongoDB is not typically considered a relational database and is definitely not in the target audience for Active Record. Therefore, we feel some pain when trying to deal with certain features. It would be fantastic to tell our customers to start a new Rails app simply by using one line: 'use MongoDB'—bam, you're good. Unfortunately, MongoDB is not a valid option for the database parameter in the Rails CLI.
00:05:40.520
The Rails new generator specifically hardcodes a list of supported databases. It's opinionated, right? You can even have an SQL relational database with an adapter for Active Record, but if it's not in that hard-coded list, sorry, you can't use that database parameter when you start your app. This means we have to document a bunch of hoops that our users have to jump through to get started with Rails. First, they must create a new app while skipping Active Record. Technically, you can leave Active Record in, but if you're not using it, it's just extra memory and resources.
00:06:40.680
Next, they need to 'gem install mongoid,' then 'bundle install' to get Mongoid, and then generate the skeletal configuration file for Mongoid. If they're using some of our enterprise features, such as field-level encryption or queryable encryption, there are another three or four hoops they have to jump through. This is all very boilerplate, and it would be nice to wrap this up in a little bundle and hand it to our users with a command to run to make it work.
00:07:07.000
So, in trying to accomplish this, I've had two guiding principles that help prioritize and execute this correctly. First, Rails is absolutely not obligated to support everyone. One of the things we love about Rails is that it is opinionated and says no to features. If it weren't, many of us would not be very happy using it—it would feel cumbersome and clunky. So I am grateful that Rails core says no, even though I wish they wouldn’t say no to me personally.
00:08:10.760
The second principle is that MongoDB users deserve nice things too. Some of you may find yourselves in a similar position with your customers—they deserve nice things. Just because Rails says no does not mean they are second-class or unloved. Everyone is worthy of love. So, Rails is under no obligation to say yes, and MongoDB users also deserve nice features. This brings us to the question: how do we turn this situation into something workable?
00:08:47.840
We recognize that the Rails CLI is not going to do it for us, but we need to understand how the Rails CLI is implemented. Can we do the same thing? If you’re familiar with Thor, you'll know that it is an official tool in the Rails repository for building command-line applications. It is efficient and simple, providing a class called Thor, which we can inherit from; every public method becomes a command. For example, in a cloud storage application, your commands would be 'cloud:list', 'cloud:upload', etc.
00:09:29.440
A straightforward example of this in Rails would be the DB console and commands like 'rails console' or 'rails routes' that are implemented as Thor subclasses. Each method performs a specific task well without the need for complicated logic. For more complex tasks, there are Thor groups, which are combined into one monolithic command—a little bit like a recipe.
00:10:12.760
For instance, when generating a new controller with Rails, it creates files and inserts specified actions through various programmed actions. The 'rails new' command is the application generator we want to extend, though you never invoke it any other way. This raises the question: Can we extend it? The answer is yes. If I said no, this would have been a short and unfortunate talk.
00:11:03.280
In David's keynote, he mentioned that templates can be very helpful for generating the same class repeatedly. I discovered buried in the help text for 'rails new' is a line about a template—a Ruby file evaluated in the context of the application generator. This means any code you put inside that template has access to the DSL used to generate the application, which can drastically streamline the process.
00:12:00.520
For example, a hypothetical but functional template could be used to create a new Rails application with MongoDB via Mongoid. First, it would add the mongoid gem to the Gemfile, then run 'bundle install' to install it, and finally run the Mongoid config generator. These steps can now be summarized in one line, even if that line is somewhat user-hostile by requiring the explicit skipping of Active Record.
00:13:01.920
My first instinct was to wrap this up in a shell script. This naive solution checks the first argument; if it's 'new,' it adds our arguments, otherwise it just passes through everything to Rails. It worked surprisingly well! The drawback is what if someone wants to use both Active Record and MongoDB? They could install both gems, but with my shell script, we would be skipping Active Record by default, which would not support that use case. We would need a way to allow someone to point to Active Record.
00:14:25.740
What if we wanted to make the Mongoid namespace implicit? If someone wanted to create the config file, they shouldn't have to type 'mongoid:' in front of everything. This shell script could become cumbersome trying to handle that. MongoDB users also utilize encryption options, which means we need to add support for that too. Instead of complicating our shell script with all these additional features, let’s rewrite it in Ruby, which is more enjoyable to work with.
00:15:24.560
However, at this point of realization, I thought—why not just extend the Rails CLI directly? I was feeling motivated and excited, thinking it wouldn't be difficult, and thankfully it was easier than I initially feared. In our CLI file, we simply set up extensions and require the Rails CLI file, allowing Rails to handle the heavy lifting it is known for, which has been battle-tested.
00:16:34.360
The fun part is the extensions, and let's focus on one in particular: the app generator. We wanted to add some new command-line options and default settings, which involves redefining the existing parameters in the Rails CLI API. We could set defaults for the template path and the skip Active Record option. This means when someone runs our command, they'll automatically pick up my template, with the option to choose a different one if they wanted, and skipping Active Record can default to true.
00:17:22.640
We can add our own command-line options for encryption by including our new module in the app generator. This 'monkey patching' allows for these overridden options to work seamlessly. It felt like a hallelujah moment as everything started working correctly. Now, the command 'rails new --skip-active-record app_name' will give you a new app set up with Mongoid, and 'rails new --encryption app_name' will set everything up automatically and download all the necessary files.
00:18:33.480
The necessary logic must also be in your template. For instance, if you check for the encryption parameter, you can conditionally execute certain actions. Template usage isn't as widely known, making it an underappreciated feature of Rails that can provide neat solutions. Some finishing touches can make this even friendlier. For example, there’s the issue with 'rails db console'—it doesn’t work with MongoDB.
00:19:42.560
So, we want it to invoke the MongoDB console instead of the Rails DB console. One option is to monkey patch the existing DB console command, but this would lose connection to the Rails command. Alternatively, we could monkey patch the Rails CLI itself so that it looks for the Mongoid namespace first, allowing our implementation to be found first.
00:20:54.440
To implement this, we need to understand how Rails assembles its list of possible command options when invoking commands like 'rails db console'. The default list of command identifiers includes multiple entries with 'rails:' at the front. By intercepting this process, we can prepend our custom Mongoid identifiers to the list, ensuring that our implementation is found and executed first, allowing us to preserve compatibility.
00:22:19.200
To finalize, we create a new DB console command that boots the application, reads the configuration, and executes the MongoDB shell smoothly. Now when I type 'rails mongoid db console', it correctly finds and launches my version—the MongoDB console. This pattern allows me to leverage this ability for other commands as well, which I plan to do.
00:23:52.160
Another helpful enhancement would be to ensure that anywhere in a Rails project, typing 'rails something' finds the appropriate bin/rails executable. For my MongoDB commands, I wanted it to find 'bin/rails:mdb' as well. To facilitate this, I’ve adjusted the lookup to check for 'bin/rails:mdb' instead of just 'bin/rails', so I can maintain consistent behavior with my Mongoid setup.
00:25:18.080
The final template adds both the Mongoid and Rails Mongoid gems in the Gemfile. It checks for the encryption option and installs the required libraries. It emits the necessary Mongoid YAML initializer files, creates the bin/rails:mdb stub, and links bin/rails to bin/rails:mdb, ensuring users can still utilize the traditional Rails command, effortlessly integrating this new functionality with familiar commands.
00:27:14.640
You can see the current iteration of this project at MongoDB/mongoid-rails on GitHub. It's still a work in progress, yet we're getting closer to a version 1 release. We need to ensure compatibility with Rails 7.2, as I've primarily been testing against 7.1. With version 8 coming out, it’s essential to verify all functionalities work, but I’m confident in the robust tests we have in place. Overall, this project has been an incredible learning journey. Many features and different pieces of logic come together in Rails CLI, and diving into the code has offered a wealth of fun and insight. I encourage everyone to explore Thor, the Rails CLI, and appreciate the magic involved in its design. Thank you!
00:30:01.320
If you have any questions, I'd be happy to take them. How much time do we have?