As you might know from previous posts, I keep a Rails playground project around. That’s a small application with a bunch of models, controllers and accompanying tests. It allows me to quickly try out a new gem that was mentioned in a blog post, see the effects of a configuration flag, or quickly prototype other ideas.
Sometimes however, even that very simple Rails application is too big. For example, when I want to share the application with friends or the internet. A default Rails application consists of a bunch of files that span multiple directories. That’s no longer suitable for a GitHub Gist or a simple email. Also, telling people to look at a bunch of files, but ignoring others, isn’t too easy. What’s important, what can be ignored?
Rails is a great framework and it’s really damn easy to start a new project, but I sometimes miss the beauty of Sinatra applications, where everything is contained in a single file - easy to ready, easy to modify, easy to share.
Turns out, there’s a way how to put all of your Rails application code into a self-executing file. I first discovered this while browsing the Rails’ GitHub issues, where people would share a self-contained rails application in a single file. Later on I noticed that those bug report templates are even mentioned in the Rails Guides.
Let’s look at an example that even integrates a 3rd party gem, like factory_girl_rails
, and can even handle simple views. I’ll walk through it block by block, see the end of this post for the full snippet.
Inline Gemfile
The first building block we’re using is Bundler’s inline feature, which lets us specify our dependencies the same way as with a traditional Gemfile
, but without the need to create a separate file. Note that this won’t create a Gemfile.lock
, so you’d want to be rather strict when defining versions. By passing true
to the gemfile
block, we can even install gems automatically when we run the script.
begin
require "bundler/inline"
rescue LoadError => e
$stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler"
raise e
end
gemfile(true) do
source "https://rubygems.org"
gem "rails"
gem "pg"
gem "factory_girl_rails"
end
Next we’ll require the parts of Rails that we actually wanna use, active_record/railtie
and active_controller/railtie
, and configure ActiveRecord to use our local installation of postgres, with a database called railstestdb
.
require "active_record"
require "action_controller/railtie"
ActiveRecord::Base.establish_connection(adapter: "postgresql", database: "railstestdb")
ActiveRecord::Base.logger = Logger.new(STDOUT)
Let’s continue with our ActiveRecord migrations. For this app, we’ll need a book model, that’s associated to categories via a categorization join model. You can write your migrations as you would with individual files.
Migrations
ActiveRecord::Schema.define do
create_table :books, force: true do |t|
t.string :name
t.timestamps
end
create_table :categories, force: true do |t|
t.string :name
t.timestamps
end
create_table :categorizations, force: true do |t|
t.references :book
t.references :category
t.boolean :primary, default: false, null: false
t.timestamps
end
end
Models
Now we continue with the three models and their associations:
class Book < ActiveRecord::Base
has_many :categorizations
has_many :categories, through: :categorizations
end
class Category < ActiveRecord::Base
has_many :categorizations
has_many :books, through: :categorizations
def self.primaries
Category.joins(:categorizations).merge(Categorization.primaries)
end
end
class Categorization < ActiveRecord::Base
belongs_to :book
belongs_to :category
def self.primaries
where(primary: true)
end
end
Application
We’ll add a class TestApp
that inherits from Rails::Application
, which will draw our routes (just /primary_categories
, for now).
class TestApp < Rails::Application
secrets.secret_token = "secret_token"
secrets.secret_key_base = "secret_key_base"
config.logger = Logger.new($stdout)
Rails.logger = config.logger
routes.draw do
resources :primary_categories, only: :index
end
end
Controllers
Next we define a PrimaryCategoriesController
, that will return all categories which have been marked as primary
in a book-category association (via the Categorizations
join model).Note that we can even render templates with ERB syntax via render
with :inline
.
class PrimaryCategoriesController < ActionController::Base
include Rails.application.routes.url_helpers
def index
@primary_categories = Category.primaries
render inline: "# of primary categories: <%= @primary_categories.count %>"
end
end
Factories
In our gemfile
block we’ve references factory_girl
, so let’s define a factory for our book model that gives us two traits: a book with a primary category, and a book with a secondary category (the primary
flag is defined on the Categorization
join model). Also add two category factories.
FactoryGirl.define do
factory :book do
name "Thing Explainer: Complicated Stuff in Simple Words"
trait :with_primary_category do
after(:create) do |book, _|
book.categorizations << Categorization.create!(category: create(:science_category), book: book, primary: true)
end
end
trait :with_secondary_category do
after(:create) do |book, _|
book.categorizations << Categorization.create!(category: create(:fun_facts_category), book: book, primary: false)
end
end
end
factory :science_category, class: Category do
name "Science & Scientists"
end
factory :fun_facts_category, class: Category do
name "Trivia & Fun Facts"
end
end
Testing
Now onto the tests. We’ll require "minitest/autorun"
, so that our tests are invoked automatically when we run the script.First we test our Category.primaries
method, and ensure that it returns only categories that are marked as primary in an association with a book.
require "minitest/autorun"
class CategoryTest < Minitest::Test
def test_primary_categories
FactoryGirl.create(:book, :with_primary_category, :with_secondary_category)
assert_equal Category.primaries, [Category.find_by_name('Science & Scientists')]
end
end
Finally, we write a controller test that assures that we correctly return the number of primary categories for the primary_categories
endpoint.
class PrimaryCategoriesTest < Minitest::Test
include Rack::Test::Methods
def test_index
get "/primary_categories"
assert last_response.ok?
assert_equal last_response.body, "# of primary categories: 1"
end
private
def app
Rails.application
end
end
That’s all! You can find the complete snippet here. Simply download or clone it (via git), create the test database via createdb railstestdb
and invoke the script via ruby rails_single_file.rb
. That will take a few seconds, depending on which gems you already have on your local machine. After that, it will prepare the application, and run the tests:
Finished in 0.332263s, 6.0193 runs/s, 9.0290 assertions/s.
2 runs, 3 assertions, 0 failures, 0 errors, 0 skips
I really like this approach, and it will allow me to keep experiments around in separate files, making them easier accessible than browsing through the git history of my playground project.