Reading Time: 4 minutes
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:
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 “destroy_with_pusher” 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@magmalabs.io. Regards.