/automatic_namespaces

Modify Rails autoloading rules to treat all files in a package directory as belonging to a given namespace

Primary LanguageRubyMIT LicenseMIT

AutomaticNamespaces

This gem eases some pain related to strong namespaces in a modular monolith.

By default, Rails 7 autoloader (Zeitwerk) requires a separate "namespace" directory above a namespaced class. For example:

app
├── models
│   ├── component1
│   │   ├── class1.rb       # contains Component1::Class1

When building a modular monolith using packages (packwerk + packs-rails), this pattern creates a lot of extra noise in the directory structure:

packs
├── component1
│   ├── app
│   │   ├── controllers
│   │   │   ├── component1
│   │   │   │   ├── model1_controller.rb  # contains Component1::Model1Controller
│   │   │   │   ├── model2_controller.rb  # contains Component1::Model2Controller
│   │   ├── models
│   │   │   ├── component1
│   │   │   │   ├── model1.rb             # contains Component1::Model1
│   │   │   │   ├── model2.rb             # contains Component1::Model2
│   │   ├── public
│   │   │   ├── component1
│   │   │   │   ├── public_type1.rb       # contains Component1::PublicType1
│   │   │   │   ├── public_type2.rb       # contains Component1::PublicType2
│   │   ├── services
│   │   │   ├── component1
│   │   │   │   ├── service1.rb           # contains Component1::Service1
│   │   │   │   ├── service2.rb           # contains Component1::Service2

And that's only for a single pack! As your modular monolith grows, you'll likely have dozens (maybe you'll have hundreds) of packs. That's a lot of "namespace" directories that aren't adding a lot of value. You already know the namespace of those classes in a strongly namespaced pack -- it's the pack name -- can Zeitwerk know it, too?

This gem configures the Rails 7 autoloader so that most subdirectories under your strongly namespaced component's app directory are automatically associated with the namespace.

Installation

Install the gem and add to the application's Gemfile by executing:

$ bundle add automatic_namespaces

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install automatic_namespaces

Usage

Given the package.yml of a strongly namespaced pack:

enforce_dependencies: true
enforce_privacy: true
public_path: app/public/
dependencies:
 - "packs/components/core_ui"
 - "packs/components/core_ext"
 - "packs/components/us_states"
 - "packs/subsystems/products"
 - "packs/subsystems/inventory"
metadata:

modify the metadata to opt into automatic namespacing:

metadata:
 automatic_pack_namespace: true

Now restructure your files to remove the extra namespace directories under app. Note that some directories do not generally contain namespaced classes. These are exempted from automatic_namespaces:

  • assets
  • helpers
  • javascript
  • views

Additional directories can be excluded by adding them to the automatic_pack_namespace_exclusions key in your package.yml file. This is useful if you require files in your pack that sit outside of your packs namespace:

metadata:
  automatic_pack_namespace: true
  automatic_pack_namespace_exclusions:
    - app/policies # Exclude pundit policies
    - app/admin # Exclude active admin definition files

If your package / namespace name requires ActiveSupport inflections, you will probably need to tell automatic_namespaces what the correct namespace name should be in that package:

# packs/shoes_ui/package.yml
metadata:
  automatic_pack_namespace: true
  namespace_override: ShoesUI

This is necessary because automatic_namespaces works by modifying the autoloader paths, which has to happen during Rails application initialization; but the inflector is not available for use then.

Development

After checking out the repo, run bundle install to install dependencies. Then, run rspec to run the tests.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Credits

Thanks to a handful of individuals who directly contributed to the creation of this gem:

  • Caleb Woods - For feeling the pain and helping to spike an approach during a project kickoff at RoleModel Software
  • Alex Evanczuk - For being willing to collaborate on the project, helping to refine the approach, and even mentoring me on how to create my first gem for sharing with others.
  • Xavier Noria - For his suggestions on how to patch Zeitwerk to make this work at all, and for suggestions on how to keep hot reloading working after Rails autopaths were missing all pack directories.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/gap777/automatic_namespaces. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the AutomaticNamespaces project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.