I was working on a project recently where I had already scaffolded out a new Rails app with rails new
and had begun committing a good chunk of application code to it. The app was small, but processes lots of data in real time and so had some special considerations around data retention and throughput. After a few weeks, I found out the requirements had changed, and the assumptions I had made at the beginning of the project no longer held.
Rather than sticking with a full Rails stack, I was asked if we could run the code transiently, essentially as a standalone script, without needing a database or persistence layer.
It took some experimentation, but I can say now that I understand Rails a lot better as a framework, and it’s not very hard to get a lot of what Rails offers even in simple scripts.
How It Worked Out
This effort was my organization’s first foray into deploying a pure Ruby script to production. We typically work within Rails and have an entire ecosystem of supporting internal gems that we can quickly insert to get the functionality we need.
To that end, I wanted to make sure the script I was architecting felt “Railsy” enough for new developers to the project to be able to drop right in. A few aspects were critical, which I’ve listed below and how they turned out.
Code Autoloading
Rails (currently) uses the zeitwerk
gem to autoload all your classes.
If you’ve worked in a Rails project before and remember back to it’s “convention over configuration” motto, a lot of what Rails offers is due to this gem. For instance, need a new Thing
model backed by ActiveRecord for your things
table? Plop it into app/models/thing.rb
, define the class as class Thing < ApplicationRecord
, and zeitwerk
will know where to find it and lazy load it when it’s needed.
You can put this into multi-file Ruby applications as well – it’s very simple. You just tell it which files to load for you, and you’re off! It can even handle code reloading like the reload!
command in a Rails console.
loader = Zeitwerk::Loader.new
loader.push_dir(...)
loader.enable_reloading # you need to opt-in before setup
loader.setup
...
loader.reload
Environment Management
One thing we use a lot is environment-specific code, for one reason or another. Rails makes this super easy to check an environment by calling Rails.env.<environment>?
.
Only want your code to run in prod? Put it behind a conditional!
if Rails.env.production?
test_in_prod
end
Implementing this in a pure Ruby script isn’t exactly hard – Ruby is just checking for the presence of ENV["RAILS_ENV"]
with the name of the environment you’re querying. To make it work as nicely as Rails… That’s a bit different.
I defined my own Application
class, with an Environment
subclass (similar to Rails’ EnvironmentInquirer
).
That class is gleefully weird – it inherits from String
through StringInquirer
, and it dynamically adds all the environment query methods. My implementation was not quite as involved, but it allowed for something like:
# Rails version
Rails.env.development?
# My version
Application.env.development?
Cherry-Picking Active*
Dependencies
One of the key features of Rails is its DSL for working with data.
It’s actually multiple DSLs housed inside the rails
meta-gem which are meant to be cherry-picked for only what you need. For instance, if you want to work with models and data validations just like you would in full Rails, you can just install activemodel
and get that for free. Like the date/time helpers you get in an out-of-the-box Rails install? Pick activesupport
. And the list goes on.
Plus, each of these smaller pieces can be further broken down to truly specialize what you need. For me, I only needed model validations from activemodel
because I wanted it to behave like ActiveRecord objects that my coworkers are all very familiar with. In my case, I can just layer in what’s needed:
class Thing
include ActiveModel::Validations
include ActiveModel::API
attr_accessor: :id, :name, :type, :description
end
thing = Thing.new(id: 1, name: "Ty", type: "something", description: "test")
(Astute readers will note that the EnvironmentInquirer
class is a part of ActiveSupport, so I should have just included it that way!)
Hopefully this sheds some light on how Rails is organized as a gem and maybe a bit of how it works under the hood. It was interesting to get to deep dive into Rails itself, and I definitely feel like I come way with a lot of knowledge when I get to do things like this.
That’s all for now. Thanks for reading!
- ← Previous: Launching Dark (Postmortem)
- Next: Thoughts on "Developers Should Deploy Their Own Code" →