Rails application namespace

Imagine this — you're working on an application that imports structured text documents. It might be a blog engine that fetches author's previous posts from Medium. Let's assume these are the markdown files. You've exported them from the former blogging software and they're present on the local filesystem.

In order to make them available in the application, you may start with a simple code like below. The FileImport walks over directory of markdown files. Reads each of them, processing with Parser to recover their structure with associated metadata. Finally each of them becomes a persisted Entry in the application.

class FileImport
  def initialize
    @parser = Parser.new
  end

  def call(path)
    Dir.glob("#{path}/*.md") do |path|
      parsed_result = @parser.call(File.read(path))

      Entry.create(
        name: File.basename(path, ".md"),
        content: parsed_result.content,
        title: parsed_result.metadata.fetch(:title),
      )
    end
  end
end

So far so good. You even have a working unit test in case of future regressions. You've also heard a bit about mutation testing with mutant and decide to finally try this thing. It can eliminate lots of dead and redundant code that Copilot is generating.

ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require "mutant/minitest/coverage"

class FileImportTest < ActiveSupport::TestCase
  cover FileImport

  def file_import = FileImport.new
  def entry = sample_imported_entry

  def test_happy_path
    file_import.call(File.join(__dir__, "fixtures/posts"))

    assert_equal 1, Entry.count
    assert_equal entry, Entry.last
  end
end

You set things up, run the tests and instead a moment of joy it fails unexpectedly. The error message says:

Parser is not a module (TypeError)

What the heck?

What happened here was the name conflict among two different constants available in the application. On one hand we've defined our very own markdown Parser, as a class in the application. On the other hand, we've added a gem dependency to our application — mutant, which transitively depends on a parser gem. This gem defines Parser as a module. A const cannot be both a class and a module, thus the error we've experienced.

Once you figure out it is about name conflict, a quick and pragmatic solution is to rename our Parser. Perhaps MarkdownParser to be more specific. There, problem solved. At least it should work until we add, directly or indirectly, some kind of markdown_parser gem dependency... 😉

Can we do better?

Custom application namespace

Another approach to such name conflicts is having a specific application namespace. Then nest application code in that namespace, hoping that none of the dependencies will use that single const name — here it is KakaDudu.

module KakaDudu
  class Parser
    def call
      # ...
    end
  end
end

In fact, when we generate a new Rails app, it already gives us a top-level namespace. Look at your config/application.rb to check it out.

require_relative "boot"

require "rails/all"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module KakaDudu
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.1

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")
  end
end

That generated namespace is not immediately useful though. Adding the namespace to a Parser, living in app/lib/parser.rb would mean moving it to app/lib/kaka_dudu/parser.rb. This the expectation of the zeitwerk code loader, which modern Rails versions use. We'd have to follow that pattern for every existing entity — FileImport, Entry. And the future ones. And for controllers, helpers... the list goes on.

Could zeitwerk help us here?

Root namespace for directory

Luckily the gift of Xavier keeps [giving]((https://github.com/fxn/zeitwerk?tab=readme-ov-file#root-directories-and-root-namespaces). We can associate code loading root with a desired namespace. This instructs the loader what namespace is expected from a path which doesn't include one. The KakaDudu::Parser can live in app/lib/parser.rb. So can the others without nesting them under kaka_dudu/ in a filesystem tree.

module KakaDudu
  class Application < Rails::Application
    config.load_defaults 7.1

    Dir["#{root}/app/*"].each do |path|
      Rails.autoloaders.main.push_dir(path, namespace: KakaDudu)
    end
  end
end

I've recently learned this feature got even better in Rails. Kudos to Xavier once again!

But does it have any drawbacks when applied so widely to all app/ subdirectories?

Framework classes and namespaces

Rails is a framework built on string manipulation. It follows particular conventions to avoid excessive configuration. Adding a namespace to entities influenced by the framework changes their string representation. Let's examine this piece by piece what to expect — from models, controllers, views and helpers.

Model

I'd have expected that ActiveRecord models need an explicit table name — not to resolve it to name-spaced table kaka_dudu_entries. This wasn't the case and all worked well here without changes.

module KakaDudu
  class Entry < ApplicationRecord
      # self.table_name = "entries" # not needed
  end
end

Controller

Controllers cannot be considered outside routing layer. Adding a class namespace requires wrapping associated routes with a block — namespace or scope. While namespace would prefix routes with kaka_dudu/, using scope lands us where we want to be — with public routes unchanged.

module KakaDudu
  class RootController < ApplicationController
    def index
      headers["Link"] = %Q_<#{micropub_url}>; rel="micropub"_
    end
  end
end
Rails.application.routes.draw do
  get "up" => "rails/health#show", :as => :rails_health_check

  scope module: "kaka_dudu" do
    root "root#index"
  end
end

View

Controller names also affect view paths — where the action is supposed to find corresponding template to render. I had not yet found a solution other than nesting on the filesystem tree under a name-spaced directory. Same had to be done for layouts, although a bit differently — app/views/layouts is the root.

app/views/
├── kaka_dudu
│   └── root
│       └── index.html.erb
└── layouts
    └── kaka_dudu
        └── application.html.erb

Helper

Every application starts with the ApplicationHelper module. With coarse-grained approach, where all app/* residents get a namespace, this creates a conflict originating from the AbstractController::Helpers::Resolution module — precisely from the helper_modules_from_paths logic. In short — the app/**/*_helper.rb is scanned and the names of the helper modules are extracted. For a name-spaced KakaDudu::ApplicationHelper living in app/helpers/application_helper.rb that would be "application" as a prefix. Next this prefix goes through "#{prefix.camemlize}Helper".constantize.

As you can imagine, this blows with NameError: uninitialized constant ApplicationHelper error. We don't have a non-namespace helper anymore.

The solutions vary:

Conclusion

I'm pretty happy how little there is to be tweaked with modern Rails and an application namespace to go together. I can consciously live with its limitations and a few rough edges here and there.

Happy hacking!