# WIP — NOT PUBLISHED — API AND DOCS IN FLUX # Zeitwerk [![Build Status](https://travis-ci.com/fxn/zeitwerk.svg?branch=master)](https://travis-ci.com/fxn/zeitwerk) ## Introduction Zeitwerk is an efficient and thread-safe code loader for Ruby. Given a conventional file structure, Zeitwerk loads the project classes and modules on demand. You don't need to write `require` calls for your own files, rather, you can streamline your programming knowing that your classes and modules are available everywhere. This feature is efficient, thread-safe, and matches Ruby's semantics for constants. The library is designed so that each gem and application can have their own loader, independent of each other. Each loader has its own configuration, inflector, and optional logger. Zeitwerk is also able to reload code, which may be handy for web applications. Coordination is needed to reload in a thread-safe manner. The documentation below explains how to do this. Finally, in some production setups it may be interesting to be able to eager load all code upfront. Zeitwerk is able to do that too. ## Synopsis Main interface for gems: ```ruby # lib/my_gem.rb (main file) require "zeitwerk" Zeitwerk::Loader.for_gem.setup module MyGem # ... end ``` Main generic interface: ```ruby loader = Zeitwerk::Loader.new loader.push_dir(...) loader.setup ``` Zeitwerk is ready to right after the `setup` call. Later, you can reload if you want to: ```ruby loader.reload ``` and you can also eager load: ```ruby loader.eager_load ``` Broadcast `eager_load` to all loaders: ``` Zeitwerk::Loader.eager_load_all ``` ## Compatible file structure To have a file structure compatible with Zeitwerk, just name files and directories after the name of the classes and modules they define: ``` 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 ``` Every directory configured with `push_dir` acts as root namespace. There can be several of them. For example, given ```ruby loader.push_dir(Rails.root.join("app/models")) loader.push_dir(Rails.root.join("app/controllers")) ``` you get the mappings ``` app/models/user.rb -> User app/controllers/admin/users_controller.rb -> Admin::UsersController ``` ### Implicit namespaces Directories without a matching Ruby file get modules autovivified automatically by Zeitwerk. For example, in ``` app/controllers/admin/users_controller.rb -> Admin::UsersController ``` `Admin` is autovivified as a module on demand, you do not need to define an `Admin` class or module in an `admin.rb` file explicitly. ### Explicit namespaces Classes and modules that act as namespaces can also be explictly defined, though. For instance, consider ``` app/models/hotel.rb -> Hotel app/models/hotel/pricing -> Hotel::Pricing ``` Zeitwerk does not autovivify a `Hotel` module in that case. The file `app/models/hotel.rb` explictly defines `Hotel` and Zeitwerk loads it as needed before going for `Hotel::Pricing`. ## Synopsis The autoloading semantics provided by Zeitwerk match Ruby's. Zeitwerk bases autoloading on `Kernel#autoload`, which has builtin support in the interpreter in constant lookup algorithms and related APIs. Autoloadable files can be required. The idea is to _not_ use `require` at all with a gem managed by Zeitwerk, but support for this exists in case your users have `require` calls already in place and upgrade, for example. ## Use case: Gems To use Zeitwerk in a gem just throw these couple of lines in the main file: ```ruby # lib/my_gem.rb require "zeitwerk" Zeitwerk::Loader.for_gem.setup module MyGem # ... end ``` The loader of your gem is exclusive, it has its own configuration, inflector, and logger. ## Use case: Generic interface The generic interface to use Zeitwerk in a different context is ```ruby require "zeitwerk" loader = Zeitwerk::Loader.new loader.push_dir("...") loader.setup ``` A loader instantiated that way is independent of other loaders. It has its own configuration, inflector, and logger. The variable used to setup the loader can get out of scope. The instance won't be garbage collected because Zeitwerk keeps a registry of them. ## Inflection Each individual loader needs an inflector to figure out which constant path would a given file or directory map to. Zeitwerk ships with two basic inflectors. ### Zeitwerk::Inflector This is a super basic inflector that converts snake case to camel case preserving upper case letters: ``` user -> User users_controller -> UsersController html_parser -> HtmlParser HTML_parser -> HTMLParser ``` This is the default inflector. ### Zeitwerk::GemInflector The loader instantiated behind the scenes by `Zeitwerk::Loader.for_gem` gets assigned by default an inflector that is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`. ### Custom inflector The inflectors that ship with Zeitwerk are deterministic and simple. But you can configure your own: ```ruby # frozen_string_literal: true class MyInflector < Zeitwerk::Inflector def camelize(basename, _abspath) case basename when "api" "API" when "mysql_adapter" "MySQLAdapter" else super end end end ``` The first argument, `basename`, is a string with the basename of the file or directory to be inflected. In the case of a file, without extension. The inflector needs to return this basename inflected. Therefore, a simple constant name without colons. The second argument, `abspath`, is a string with the absolute path to the file or directory in case you need it to decide how to inflect the basename. Then, assign the inflector before calling `setup`: ``` loader = Zeitwerk::Loader.new loader.inflector = MyInflector.new loader.setup ``` ## Logging Zeitwerk is silent by default, but you can configure a callable as logger. In the case of gems, for example: ```ruby require "zeitwerk" loader = Zeitwerk::Loader.for_gem loader.logger = method(:puts) loader.setup ``` If there is a logger configured, the the loader is going to print traces when autoloads are set, files preloaded, files autoloaded, and modules autovivified from directories. If your project has namespaces, you'll notice in the traces Zeitwerk sets autoloads for _directories_. That's a technique used to be able to be lazy and avoid unnecessary tree walks. It sets autoloads one depth level at a time, and only on demand. ## Reloading In order to reload code, unload and setup: ```ruby loader.unload loader.setup ``` It is important to highlight that these are instance methods. Therefore, reloading the project managed by your autoloader does _not_ reload the code of other gems using Zeitwerk. Generally speaking, reloading is useful for services, servers, web applications, etc. Gems that implement regular libraries, so to speak, won't normally have a use case for reloading. ## Eager loading Zeitwerk instances are able to eager load their managed files: ```ruby loader.eager_load ``` You can opt-out of eager loading individual files or directories: ```ruby require "zeitwerk" loader = Zeitwerk::Loader.for_gem db_adapters = File.expand_path("my_gem/db_adapters", __dir__) cache_adapters = File.expand_path("my_gem/cache_adapters", __dir__) loader.do_not_eager_load(db_adapters, cache_adapters) loader.eager_load # won't go into the directories with db/cache adapters ``` Files and directories excluded from eager loading can still be autoloaded, so a technique like this is possible: ```ruby db_adapter = Object.const_get("MyGem::DbAdapters::#{config[:db_adapter]}") ``` You can even opt-out from eager loading entirely: ```ruby require "zeitwerk" loader = Zeitwerk::Loader.for_gem loader.do_not_eager_load(__dir__) loader.setup ``` If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all registered instances: ```ruby Zeitwerk::Loader.eager_load_all ``` In that case, exclusions are per autoloader, and so will apply to each of them accordingly. This may be handy in top-level services, like web applications. ## Preloading Zeitwerk instances are able to preload files and directories. ```ruby loader.preload("app/models/videogame.rb") loader.preload("app/models/book.rb") ``` The example above depicts several calls are supported, but `preload` accepts multiple arguments and arrays of strings as well. The call can happen after `setup` (preloads on the spot), or before `setup` (executes during setup). If you're using reloading, preloads run on each reload too. This is a feature specifically thought for STIs in Rails, preloading the leafs of an STI tree ensures all classes are known when doing a query. Instead of preloading, gems can issue regular requires in the main file: ```ruby require "zeitwerk" Zeitwerk::Loader.for_gem.setup module MyGem require "preload_me" end ``` ## Supported Ruby versions Zeitwerk works with MRI 2.4.4 and above. ## Motivation Since `require` has global side-effects, and there is no static way to verify that you have issued the `require` calls for code that your file depends on, in practice it is very easy to forget some. That introduces bugs that depend on the load order. Zeitwerk provides a way to forget about `require` in your own code, just name things following conventions and done. On the other hand, autoloading in Rails is based on `const_missing`, which lacks fundamental information like the nesting and the resolution algorithm that was being used. Because of that, Rails autoloading is not able to match Ruby's semantics and that introduces a series of gotchas. The original goal of this project was to bring a better autoloading mechanism for Rails 6. ## Thanks I'd like to thank [@matthewd](https://github.com/matthewd) for the discussions we've had about this topic in the past years, I learned a couple of tricks used in Zeitwerk from him. Also would like to thank [@Shopify](https://github.com/Shopify), [@rafaelfranca](https://github.com/rafaelfranca), and [@dylanahsmith](https://github.com/dylanahsmith), for sharing [this PoC](https://github.com/Shopify/autoload_reloader). The technique Zeitwerk uses to support explicit namespaces was copied from that project. ## License Released under the MIT License, Copyright (c) 2019–ω Xavier Noria.