Drawers
Group related classes together. No more silos.
What is this about?
With large rails application, the default architecture can result in a resource's related files being very spread out through the overall project structure. For example, lets say you have 50 controllers, serializers, policies, and operations. That's four different top level folders that spread out all the related objects. It makes sense do it this way, as it makes rails' autoloading programmatically easy.
This gem provides a way to re-structure your app so that like-objects are grouped together.
All this gem does is add some new autoloading / path resolution logic. This gem does not provide any service/operation/policy/etc functionality.
All of this is optional, and can be slowly migrated to over time. Adding this gem does not force you to change your app.
The new structure
app/
├── channels/
├── models/
│ ├── data/
│ │ ├── post.rb
│ │ └── comment.rb
│ └── graph_data.rb
├── jobs/
├── mailers/
│ └── notification_mailer.rb
└── resources/
├── posts/
│ ├── forms/
│ │ └── new_post_form.rb
│ ├── controller.rb # or posts_controller.rb
│ ├── operations.rb # or post_operations.rb
│ ├── policy.rb # or post_policy.rb
│ └── serializer.rb # or post_serializer.rb
└── comments/
├── controller.rb
├── serializer.rb
└── views/
├── index.html.erb
└── create.html.erb
Does this new structure mean you have to change the class names of all your classes? Nope. In the above example file structure, app/resources/posts/controller.rb
still defines class PostsController < ApplicationController
Checkout the sample rails app in the tests directory.
The Convention
Say, for example, you have any class/module defined as:
module Api # {namespace
module V3 # namespace}
module UserServices # {resource_name}{resource_type}
module Authentication # {class_path
class OAuth2 # class_path/file_name}
end
end
end
end
end
As long as some part of the fully qualified class name (in this example: Api::V3::UserServices::Authentication::OAuth2
) contains any of the defined keywords, the file will be found at app/resources/api/v3/users/services/authentication/oauth2.rb
.
The pattern for this is: app/resources/:namespace/:resource_name/:resource_type/:class_path
where:
:namespace
is the namespace/parents of theUserService
:resource_type
is a suffix that may be inferred by checking of the inclusion of the defined keywords (linked above):resource_name
is the same module/class as what theresource_type
is derived from, sans theresource_type
:class_path
is the remaining namespaces and eventually the class that the target file defines.
So... what if you have a set of classes that don't fit the pattern exactly? You can leave those files where they are currently, or move them to app/resources
, if it makes sense to do so. Feel free to open an issue / PR if you feel the list of resource types needs updating.
Usage
gem 'drawers'
Including the gem in your gemfile enables the new structure.
A note for ActiveModelSerializers
ActiveModelSerializers, by default, does not consider your controller's namespace when searching for searializers.
To address that problem, you'll need to add this to the serializer lookup chain
# config/initializers/active_model_serializers.rb
ActiveModelSerializers.config.serializer_lookup_chain.unshift(
lambda do |resource_class, _, namespace|
"#{namespace.name}::#{resource_class.name}Serializer" if namespace
end
)
Note: as of 2016-11-04, only this branch of AMS supports a configurable lookup chain
Note: as of 2016-11-16, the master
(>= v0.10.3) branch of AMS supports configurable lookup chain.
Migrating
Each part of your app can be migrated gradually (either manually or automatically).
In order to automatically migrate resources, just run:
rake rmu:migrate_resource[Post]
This will move all unnamespaced classes that contain any of the supported resource suffixes to the app/resources/posts
directory.
Configuration
# (Rails.root)/config/initializers/drawers.rb
Drawers.directory = 'pods'
Sets the folder for the new structure to be in the app/pods
directory if you want the new structure separate from the main app files.
Co-Location of Tests / Specs
There are some mixed feelings about co-location of specs, so (by default), this monkey patch is not included.
# config/initializers/gems/rails.rb
module Rails
class Engine
# https://github.com/rails/rails/blob/5-1-stable/railties/lib/rails/engine.rb#L472-L479
# https://github.com/rails/rails/blob/4-2-stable/railties/lib/rails/engine.rb#L468
def eager_load!
config.eager_load_paths.each do |load_path|
matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/
# They key is the !(spec) added to the glob pattern.
# You may need to modify this if your tests don't end with spec
Dir.glob("#{load_path}/**/*!(spec).rb").sort.each do |file|
require_dependency file.sub(matcher, '\1')
end
end
end
end
end
Your test suite command would then need to change to include the app
directory.
rspec app/ spec/
And you'll still want to use the spec folder for spec_helper.rb
, factories, and other support things.
Contributing
Feel free to open an issue, or fork and make a pull request.
All discussion is welcome :-)
The gem name 'Drawers' was provided by @bartboy011. Thanks @bartboy011!
The previous name of this gem was Rails Module Unification -- which, while a homage to its inspiration, Ember's Module Unification app architecture, it's quite a mouthful, and doesn't exactly make for a good gem name.