Ruby project structure and configuration
Some languages do a great job of enforcing module structures that match namespaces. Ruby is not one of those languages.
This usually isn’t a problem though. Conventionally, if a constant is named Foo it is probably defined in a file named foo.rb. If a constant is named Foo::Bar, it probably lives in a file called foo/bar.rb. You can achieve this by manually enforcing the pattern, or using tools which manage file loading for you. I like convenience-enforced conventions, it should be easier to do the right thing.
When I started using Ruby I was surrounded by too many folks who waved their hands while muttering on about “rails magic” said things like “convention over configuration” without explanation. I never found that to be an effective form of pedagogy. I prefer to learn and understand the components in play.
Here’s why you never need to call require in a Rails project, and how you can too!
Zeitwerk::Loader
Zeitwerk (Ruby) is great. It removes the need to require by:
- translating constants to file names with
CamelCasetosnake_case.rbdefault - giving an escape hatch of “inflections”, so acronyms like
CLIwill load fromcli.rbinstead of requiring you useCli.
Once configured to look in ./lib, a file lib/foo.rb containing a constant Foo will always load correctly just by referencing the constant Foo. Wild.
Side note - Zeitwerk has excellent hot-reload capabilities. They are worth learning about, but aren’t relevant here.
Bundler.require
For a given Ruby project you’ll probably have a Gemfile. Here you specify which dependencies your project uses. You should read the docs, but the tldr is that you can use it for third party dependencies from github or rubgems, requiring projects from relative directories, and locking to specific dependencies.
While Ruby’s Core classes do not need to be required, Standard Library classes do. It may not be necessary, but I like to add them to the Gemfile as well.
Calling Bundler.require will load all dependencies from your Gemfile, preventing you from explicitly needing to require any of them directly. (If this isn’t the behavior you want for a particular dependency you can always opt out using require: false in the Gemfile.)
Bundler works well side-by-side with Zeitwerk, between the two you shouldn’t have to call require.
configuring the config - ./conf.rb
Using these together is pretty lightweight. This is what the scaffolding for a bare bones project might look like:
in ./conf.rb
require "bundler" # load bundler, no zeitwerk yet
Bundler.require # load gems
# configure zeitwerk (no require, bundler required it)
loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/lib/")
require './lib/inflections'
Inflections.add_inflections(loader.inflector)
loader.setup
in Gemfile
source "https://rubygems.org"
gem "bundler"
gem "zeitwerk"
in lib/inflections.rb
module Inflections
def self.add_inflections(inflector)
inflector.inflect(all)
end
def self.all
{
"cli" => "CLI",
"api" => "API",
}
end
end
then your actual project code get’s its appropriately named file ./lib/foo.rb
class Foo
def bar
puts "now that's what I call podracing!"
end
end
That’s it. Now you have auto loading with convenience-enforced namespace conventions.
./main.rb
require './conf'
Foo.new.bar
# outputs "now that's what I call podracing!"
Putting it together - Shell scripts and exec
Maybe that’s not quite it. The whole point is to not call require all the time. You probably have more than a single main.rb entry point to your project. If any script or main function needed require ./conf.rb (and only if it is loaded as the main function) this would defeat the purpose. So for most of my ruby projects I’ll’ add a wrapper script to ruby which loads the conf. From there things work just as I’d expect.
Here’s an example of what this can look like:
in ./rb (with permissions -rwxr--r--)
#!/usr/bin/env bash
exec ruby -r./conf "$@"
A pedantic explanation, each point wildly glossed over but deserving it’s own 500 words:
-rwxr--r--appropriately executable, I usechmod u+x ./cmdto granting few enough permissions#!If the program is a file beginning with #!, the remainder of the first line specifies an interpreter for the programexecreplaces the shell with the given commandruby -r./conffrom the docs (-rlibrary require the library before executing your script)"$@"forward positional parameters without word splitting in bash- Ruby also has the ability to
execlike bash, but I use this pattern everywhere so I stick with bash for consistency
I’ll assumed you didn’t read that. Just remember to chmod u+x ./rb
Look mom, no hands require!
./rb -e "Foo.new.bar" outputs now that's what I call podracing!
Want an equivalent of rails console in the project you’re working on? It looks like this:
./rb -e binding.irb
Do you excel at forgetting what flag ruby needs to run code? Great, me too. Go ahead and stuff that in a file (echo binding.irb > console.rb). Running code is the best documentation.
Any file is a main file, load order be damned!
./rb console.rb
irb(main):001> Foo.new.bar
# now that's what I call podracing!
=> nil
irb(main):002>
That’s it, no magic. Just ruby.