Loading Ruby files automatically with Zeitwerk
Introduction
Hello everyone!
As I was playing around with the Ruby programming language when building a Rails application I came across Zeitwerk, a library that loads all your ruby files automatically and I wanted to write about it.
The main idea behind the library is that your projects needs to follow a certain file structure and then the lib
will load all the ruby files, eliminating the necessity to write require_relative 'x'. This is nice because
it keeps the code structured in an opinionated way and saves some keystrokes from writing import files.
It did confuse me at first when I browsed the source code of Gitlab and I didn’t see any imports, and that is because they use Rails and Zeitwerk.
Usage
To use the library your project needs to follow the following folder structure:
lib/my_gem.rb -> MyGem
lib/my_gem/foo.rb -> MyGem::Foo
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
You can also fine tune it by collapsing directories, ignoring directories or adding inflections.
(eg: unicredit.rb to UniCredit, without inflections it would be lowercase C)
Once you have that structure, you can install it by typing bundle add zeitwerk and then all you need to do is write
some boilerplate code to set it up.
Application
If you’re writing an application then you can call the library in your main file, specifying which directories Zeitwerk should load.
require "zeitwerk"
# Note: there is no require_relative "huffman" because Zeitwerk loaded it automatically.
def main
huffman = Huffman.new("huffman.md")
content = huffman.encode
print content.left
end
if __FILE__ == $0
# Only three lines of code needed
loader = Zeitwerk::Loader.new
loader.push_dir(File.expand_path("./lib"))
loader.setup
main
end
The structure for this application project looks like this:
├── Gemfile
├── Gemfile.lock
├── huffman.md
├── lib
│ ├── huffman
│ │ └── base_exception.rb
│ ├── huffman.rb
│ └── tree
│ ├── base_node.rb
│ ├── internal_node.rb
│ └── leaf_node.rb
├── main.rb # The main file, entrypoint
├── Rakefile.rb
└── test
└── huffman_test.rb
That is basically it.
Gem (Library)
When writing a library gem, you can use Zeitwerk::Loader.for_gem. In my ExtrasDeCont gem I have the following entrypoint:
# frozen_string_literal: true
require 'zeitwerk'
loader = Zeitwerk::Loader.for_gem
loader.inflector.inflect(
"unicredit" => "UniCredit"
)
loader.setup
# The ExtrasDeCont module contains utilities for parsing bank statements.
module ExtrasDeCont
# Map of supported banks (symbol → rule class)
BANK_RULES = {
brd: ExtrasDeCont::Rules::Brd,
ing: ExtrasDeCont::Rules::Ing,
revolut: ExtrasDeCont::Rules::Revolut,
unicredit: ExtrasDeCont::Rules::UniCredit
}.freeze
class << self
# Parses a PDF bank statement and returns structured transactions.
#
# @param file [String, Pathname, IO] path to the PDF file or an IO-like object
# @param bank [Symbol] the bank identifier (:unicredit, :revolut, etc.)
# @return [Array<ExtrasDeCont::Transaction>]
# @raise [ArgumentError] if the bank is not supported
def parse(file, bank:)
rule_class = BANK_RULES[bank]
raise ArgumentError, "Unsupported bank: #{bank}. Supported banks: #{BANK_RULES.keys.join(", ")}" unless rule_class
p = ExtrasDeCont::Parser.new(file)
p.parse_with(rule_class.new)
end
end
end
loader.eager_load
Note that I added this inflection in order to keep unicredit.rb refer to UniCredit.
loader.inflector.inflect(
"unicredit" => "UniCredit"
)
Additionally, the file structure of the gem looks like this:
.
├── AGENTS.md
├── bin
│ └── example.rb
├── changelog.md
├── diagram.md
├── entities.json
├── extras_de_cont.gemspec
├── Gemfile
├── Gemfile.lock
├── lib
│ ├── extras_de_cont
│ │ ├── parser.rb
│ │ ├── rules
│ │ │ ├── base.rb
│ │ │ ├── brd.rb
│ │ │ ├── ing.rb
│ │ │ ├── revolut.rb
│ │ │ └── unicredit.rb
│ │ └── transaction.rb
│ └── extras_de_cont.rb
├── LICENSE
├── mempalace.yaml
├── opencode.json
├── pkg
│ ├── extras_de_cont-1.0.2.gem
│ ├── extras_de_cont-1.1.0.gem
│ ├── extras_de_cont-1.2.0.gem
│ ├── extras_de_cont-1.3.0.gem
│ └── extras_de_cont-1.4.0.gem
├── Rakefile
├── README.md
├── sig
│ └── extras_de_cont
│ ├── extras_de_cont.rbs
│ ├── parser.rbs
│ ├── rules
│ │ ├── base.rbs
│ │ └── brd.rbs
│ └── transaction.rbs
└── test
└── extras_de_cont
└── rules
├── brd_rule_test.rb
├── ing_rule_test.rb
├── revolut_rule_test.rb
└── unicredit_rule_test.rb
12 directories, 35 files
Conclusion
Zeitwerk is a nice ruby gem that removes the need of requiring files manually. If you browse some Ruby code and all
the require_relative are missing then the projects probably uses Zeitwerk.
In order for Zeitwerk to work you will need to have a certain directory structure, but Zeitwerk is highly customizable.
Thanks for reading!