Skip to content

Group related classes together. No more silos. A solution to rails dystopia.

License

Notifications You must be signed in to change notification settings

NullVoxPopuli/drawers

Repository files navigation

Drawers

Group related classes together. No more silos.

Gem Version Build Status Code Climate Test Coverage Dependency Status

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 the UserService
  • :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 the resource_type is derived from, sans the resource_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.