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!