JRL Projects About

Namespacing in Rails 5

August 2, 2017
Updated: December 18, 2018

Google Plus Twitter Hacker News Reddit LinkedIn
Article Header Image
I

f you've created a large Rails project, you may be encountering an organizational issue. Models, controllers, and other files live in the same folder, and can be difficult to tell apart. There may even exist multiple types of seemingly equivalent classes (e.g. an event participant vs a business participant) that require different logic and cannot live in the same namespace. Namespacing allows MVC files to be grouped into subfolders, greatly simplifying organizational complexity.

What is Namespacing?

Before we get into implementation details, we need to first differentiate some related routing concepts/keywords: namespacing, nesting, and scoping.

Route Description URI Controller
Namespace Organize under a folder /parent/child/* parent/child#action
Nested Logical children of resources /parent/:parent_id/child/* child#action
Scope Customizable /parent/child/* child#action

Scoped and Namespaced routes are very similar, scope allows greater customization. The table shows default values, but scope can be customized to change URI pattern (path), controller (module), and path helper names (as).

Nested routes are a bit different. A nested route can be a member, a collection, or another resource. Only a collection does not require a specific parent ID in the URI route.

They are all closely related. Using these different routing keywords, you can create identical routes, helpers, and folder structure, they just provide different ways to get there.

Setup

Assume we have Event and User resources, where Users can be Participants or Workers in these events. First off, we need to generate these resources: rails g scaffold Event and rails g scaffold User. The scaffold command will create model, migration, controller, and views for the resources. Next, create the namespaced participant controller: rails g controller Events::Participants. For convenience sake, it will be responsible for creating any event role and thus we will avoid creating a Worker controller.

Routing

Let's define a custom, namespaced route in config/routes.rb to add or remove a participant from an event:

1
2
3
4
5
6
7
Rails.application.routes.draw do
  namespace :events do
      put :add_or_rm_role, to: 'participants#add_or_rm_role'
  end
  resources :events
  resources :users #Recommended to use Devise or another library instead of writing your own authentication system
end

Running rake routes will show:

1
2
3
4
               Prefix Verb   URI Pattern                      Controller#Action
events_add_or_rm_role PUT    /events/add_or_rm_role(.:format) events/participants#add_or_rm_role
           edit_event GET    /events/:id/edit(.:format)       events#edit  #<<<< event resource route
               #other event/user resource routes...

Notice the order of the routes. If event has a string ID, and the custom route 'add_or_rm_role' occurs after resources :events, add_or_rm_role will be interpreted as an event ID.

Migration

Let's setup the back-end migrations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
create_table :events do |t|
  t.string  :name, null: false
end
create_table :users do |t|
  t.string  :name, null: false
end

create_table :event_workers do |t|
  t.belongs_to :user
  t.belongs_to :event

  t.string :job #add some custom attributes to this join table for fun
end

create_table :event_participants do |t|
  t.belongs_to :user
  t.belongs_to :event

  t.boolean :user_showed_up, default: false
end

Model

We want to specify the associations between these models. Since join tables have custom attributes, and there are multiple user-event associations, we will use has_many instead of has_many_through.

1
2
3
4
class Event < ApplicationRecord #filename: event.rb
  has_many :workers, dependent: :destroy
  has_many :participants, dependent: :destroy
end
1
2
3
4
class User < ApplicationRecord #filename: user.rb
  has_many :workers, dependent: :destroy
  has_many :participants, dependent: :destroy
end

The following models are event namespaced, and thus should go into the /models/event/* folder.

1
2
3
4
class Event::Worker < ApplicationRecord #filename: event/worker.rb
  belongs_to :event
  belongs_to :user
end
1
2
3
4
class Event::Participant < ApplicationRecord #filename: event/participant.rb
  belongs_to :event
  belongs_to :user
end

Notice the Event::MODEL_NAME? That indicates to rails that this model is namespaced, and is similar to what must be done in the controllers.

Controller

Of course events_controller.rb and user_controller.rb will be created, but the participant controller is much more interesting. Create a new file, /app/controllers/events/participants_controller.rb. This controller will implement our custom route.

The logic is a bit application-specific here. The important thing to note is that we are creating our own strong parameters, and that the route points to the correct action for this namespaced route.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Events::ParticipantsController < ApplicationController
  def add_or_rm_role
    #choose which model we want to add
    model = nil
    event = Event.find(event_role_params[:event_id])
    case event_role_params[:role]
      when "worker"
        model = Event::Worker
      when "participant"
        model = Event::Participant
    end

    #add/remove the DB tuple
    mod_role(model, event_role_params[:is_add], event.id, current_user.id) #current_user must be defined via authentication application

    #redirect user and refresh the page
    redirect_to event, notice: 'Changes have been saved!'
  end

  private
    # Never trust parameters from the scary internet, only allow the white list through.
    def event_role_params
      params.permit(:role, :is_add, :event_id, :user_id)
    end

    def mod_role(model, is_add, evt_id, usr_id)
      if ActiveModel::Type::Boolean.new.cast(is_add)
        model.create(event_id: evt_id, user_id: usr_id)
      else
        model.find_by(event_id: evt_id, user_id: usr_id).destroy
      end
    end
end

Views

We need a way for the user to call our new action. This is pretty simple using link_to. Let's insert this link in our views/events/show.html.erb file.

1
2
3
4
5
6
7
8
9
10
11
<%
current_user_is_participating = @event.particpants.find_by(user_id: current_user.id) != nil
if current_user_is_participating %%>
  <%= link_to "Remove myself from event",
    events_add_or_rm_role_path(role: Event::Participant.event_role_types[:participant], is_add: false, event_id: @event.id),
    method: :put %%>
<% else %%>
  <%= link_to "Sign Up For Event",
    events_add_or_rm_role_path(role: Event::Participant.event_role_types[:participant], is_add: true, event_id: @event.id),
    method: :put %%>
<% end %%>

events_add_or_rm_role_path is the helper from our Routing section. The custom params defined in the controller are arguments to this helper function. @event will be defined in the events#show action, and user_signed_in & current_user need to be defined in your authentication schema. If you want link_to to use an ajax command, remove the redirect_to in the controller action and add remote: true to the link_to's.

We also need to display all of our participating users on the event#show page

1
2
3
4
<p>Participants:</p>
<% for participant in @event.participants %%>
  <%= participant.user.attributes %%>
<% end %%>

Caveats

Sometimes namespacing can cause issues. When setting this up, I kept running into the error: NameError (uninitialized constant Participant). I tracked it down to the CanCanCan authorization gem's load_and_authorize_resource call. This call was not smart enough to see the model had been namespaced, and needed to know about it explicitly: load_and_authorize_resource class: "Event::Participant", I imagine there are other gems/scenarios out there that act the same.

Alternatives

There are other ways this specific scenario could have been setup. Scope is an alternative to the namespace routing, and is a bit cleaner:

1
2
3
4
5
resources :events do
  scope module: :events do
    put :add_or_rm_role, to: 'participants#add_or_rm_role'
  end
end

Which rake routes would resolve to

1
2
              Prefix Verb   URI Pattern                                Controller#Action
event_add_or_rm_role PUT    /events/:event_id/add_or_rm_role(.:format) events/participants#add_or_rm_role

Since this action is role agnostic, maybe a nested event route would be better.

1
2
3
4
5
resources :events do
  member do
    put :add_or_rm_role
  end
end

Notice that rake routes now points to the events controller instead of the namespaced participants controller. This prevents mucking around in namespaces, or even creating a new controller, but it can quickly cause the events controller to blow up.

1
2
              Prefix Verb   URI Pattern                          Controller#Action
add_or_rm_role_event PUT    /events/:id/add_or_rm_role(.:format) events#add_or_rm_role

From there, you'd have to add the action to the events controller, modify the strong parameters, and change the path helper in the link_to functions. The last option is probably the easiest to read and understand, but then I wouldn't have been able to write about namespacing!