Creating and using modules in Rails

Heriberto PerézNov 20, 2012

Let's talk about using Ruby Modules in Rails applications.

I want to apply this tutorial to one refactoring task, where two pieces of code look very similar.

controllers/actions_controller.rb

if @retrospective.can_delete_action?
  @action.destroy
  @action.pusher_destroy(params[:socket_id])
  respond_with @action, location: retrospectives_url
else
  render text: 'Forbidden', status: 403
end

controllers/items_controller.rb

if @retrospective.can_delete_item?
  @item.destroy
  @item.pusher_destroy(params[:socket_id])
  respond_with @item, location: retrospectives_url
else
 render text: 'Forbidden', status: 403
end

We can attack this common problem using Ruby Modules. In said module, we can remove all references to specific resources and make it generic, because we want to use it in any controller.

First of all, we need to create our module. The basic syntax to create a module is:

Open and close a module
module MyModule
   def a_module_method
   end
end

For my example, I'll be placing our modules in the “lib” directory, within a subfolder called “modules”; after that our module looks like:

lib/modules/resource_destroyer.rb

  module Modules
    class ResourceDestroyer

      def self.destroy_with_pusher(resource, retrospective, socket_id)
        if retrospective.send("can_delete_#{resource.class.name.underscore}?".to_sym)
         resource.destroy
         resource.pusher_destroy(socket_id)
         resource
       end
     end
   end
 end
Let's discuss the code above:

It won't matter the class of the resource that we send to this method, by using send, we'll be able to ask if the current user is allowed to delete the resource using the magic of metaprogramming.

This is how our Retrospective presenter looks like:

  class RetrospectivePresenter < SimpleDelegator  
    def can_delete_action_item?
      # Code that determines if a user can delete the action item
    end

    def can_delete_story?
      # Code that determines if a user can delete the story
    end
  end

What we did in our module is, determine the method that we need to call by using the resource's class name:

resource = Story.new

"can_delete_#{resource.class.name.underscore}?" # => can_delete_story?

resource = ActionItem.new

"can_delete_#{resource.class.name.underscore}?" # => can_delete_action_item?

Now, let's continue with our test in Rspec

The first thing to do is create our spec.

spec/lib/modules/resource_destroyer_spec.rb

Add the following code:

require ‘spec_helper’

describe Modules::ResourceDestroyer do
  subject{Modules::ResourceDestroyer}
end

At this point, if you execute your test, it should pass.

Describe that we're testing our “destroywithpusher” method

 describe :destroy_with_pusher do
 end

Now, we need to create two contexts:

First context when the user can delete resource.

Second context when the use can’t delete resource.

  context 'when user can delete resources' do
  end

  context 'When user can not delete resources' do
  end

Let’s start with the first context and the method that we want to test.

  context 'when user can delete resources' do
  before do
    retrospective.should_receive(:send).with(:can_delete_resource?),and_return true
    retrospective.should_receive(:destroy)
    retrospective.should_receive(:pusher_destroy).with(socket_id)
  end
  it 'destroy a resource with the specified params' do
    subject.destroy_with_pusher(resource, retrospective, socket_id). should eq(resource)
   end
 end

At this point, if we execute the test, it will fail because we did not declare our helper methods(let), now let’s continue with the helper methods declaration:

  let(:resource) { mock(‘Resource’, class: stub(Class, name:  ‘Resource’)) }
  let(:retrospective) { mock(Retrospective) }
  let(:socket_id){ “1” }

Now let's continue with the other context, when the user can’t delete a resource for whatever reason, in general we need to confirm that this method should return false:

  context 'When user can not delete resources' do
    before do
      retrospective.should_receive(:send).with(:can_delete_resource?).and_return false
    end

    it 'does not invoke destroy on resource nor its pusher socket' do
      subject.destroy_with_pusher(resource, retrospective, socket_id). should be_false
    end
  end
Finally, here is the complete spec
require 'spec_helper'

describe Modules::ResourceDestroyer do
  subject { Modules::ResourceDestroyer }

  describe :destroy_with_pusher do
    let(:resource) { mock('Resource', class: stub(Class, name: 'Resource')) }
    let(:retrospective) { mock(Retrospective) }
    let(:socket_id) { "1" }

    context 'when user can delete resource' do
      before do
        retrospective.should_receive(:send).with(:can_delete_resource?).and_return true
        resource.should_receive(:destroy)
        resource.should_receive(:pusher_destroy).with(socket_id)
      end

      it 'destroys a resource with its pusher socket' do
        subject.destroy_with_pusher(resource, retrospective, socket_id).should eq(resource)
      end
    end

    context 'when user can not delete resources' do
      before do
        retrospective.should_receive(:send).with(:can_delete_resource?).and_return false
      end

      it 'does not invoke destroy on resource nor its pusher socket' do
        subject.destroy_with_pusher(resource, retrospective, socket_id).should be_false
      end
    end
  end
end

How to use modules in Rails 3?

To use our modules, we need to load the module files, for example, we can use a Rails initializer, because they are loaded and executed at the moment the application is started and all the modules files that we specify will be loaded with our application files.

Another way to include our modules files is editing and configuring the config/application.rb file, we can uncomment the following line:

 config.autoload_paths += %W(#{config.root}/lib)

After we’ve done all the instructions above, we now can use the Module's methods our controller like this:

  Modules::ResourceDestroyer.destroy_with_pusher(@action_item, @retrospective, params[:socket_id]).

But if we need to share that method with other controllers we can always add the following method into application_controller.rb

 def resource_destroyer(resource, restrospective, socked_id)
   if Modules::ResourceDestroyer.destroy_with_pusher(resource, restrospective, socked_id)
     respond_with(resource, location: retrospectives_url)
   else
     render text: 'Forbidden', status: 403
   end
 end

Now we can use the method in any controller that inherits from application_controller.

def destroy
  resource_destroyer(@story, @retrospective, params[:socket_id])
end

More Ruby Module examples.

We will continue creating new modules, the next module that we will create should satisfy the next specifications.

For developers, it is very important to debug our applications, in this case we want to use Rails.logger, but with some custom functionality that we want to share among all of our controllers.

The basic idea behind this solution is, to create a module called “Logger” with a method called “log”, that will receive two params, and the log method will print the result:

#
# spec/lib/modules/logger_spec.rb
#

require 'spec_helper'

describe Modules::Logger do
  describe "#log" do
    let(:title) { "SomeClass"}
    let(:message) { "SomeContent"}

    specify do
      subject.log title, message
    end
  end
end

The code to make it pass:

#
# lib/modules/logger.rb
#

module Modules
  module Logger
    extend self

    def log(title, message)
      Rails.logger.debug "\033[38;5;148mSTART #{self.class} - #{title}\033[39m"
      Rails.logger.debug message
      Rails.logger.debug "\033[38;5;148mEND #{self.class}\033[39m"
    end
  end
end

Now, whenever we need to print some debugging string and we need to know the class and method that called it, we can just:

 class SomeClass
   include Modules::Logger

   def any_method(params)
      some_result = MyModel.find(:all)
      log __method__, some_result
      some_result
    end  
 end

Well, that's it for now, thanks for reading this article! Any questions or suggestions are very welcome and appreciated. You can follow me @heridev or email me at heriberto.perez@crowdint.com. Regards.

blog comments powered byDisqus