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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.

1
2
3
4
5
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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).

1
2
3
4
5
6
7
8
9
10
11
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.

1
2
3
4
5
6
7
8
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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.

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:

1
2
3
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.