There's enough for everyone

गते गते पारगते पारसंगते बोधि स्वाहा गते गते पारगते पारसंगते बोधि स्वाहा

Action Module

A little while ago I was ranting about rails controllers, and a bit after that about @ivars in views. More recently I realised you can do class ActionHandler < Module; end and I’ve been wondering what one could usefully do with that.


This morning I had yet another scenario where I was adding three extra methods to a controller so that one action method could be nicer. None of the other actions would ever use those 3 methods. And as usual there were @ants in my @pants, causing the proverbial itch which, according to legend, gives rise to open source.

“Separate controller” says my Refactor Pedant who sits on my right shoulder (my left shoulder being occupied by my Cowboy Coder).

And I thought “Hmm. Well, why not just put them in a module?”

And presently the following code emerged:

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
34
35
36
class ApplicationController < ActionController::Base
  class ActionHandler < Module
    # Get the controller, and call methods on that.
    # In other words, all rails controller macros
    # are accessible for methods defined inside
    # this module.
    # Awsuuum!
    def method_missing(meth, *args, &blk)
      controller_class = Module.nesting[1]
      controller_class.send meth, *args, &blk
    end
  end

  def self.handle( action, &action_module_block )
    action_module =
    if block_given?
      # as a block to this method
      ActionHandler.new &action_module_block
    else
      # it's defined in it's own module
      instance_eval action.to_s.camelize
    end

    # Set up the controller action so rails will call it.
    # Dynamically include the relevant module to handle the call.
    # Assume that module implements "handle"
    define_method action do
      singleton_class.include action_module
      handle
    end

    def handle
      raise NotImplementedError, "You didn't implement 'handle' for #{params[:action]}"
    end
  end
end
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
34
35
36
class ProjectsController < ApplicationController
  # warning: ruby-2.1 syntax
  helper_method def project
    @project ||= Project[params[:id]]
  end

  # handle GET /projects/:id/owner
  handle :owner do
    helper_method def projects_for( owner )
      # this makes no sense in this limited context
      # just take my word that it does Useful Stuff
      grinder.filter[:persons__id] = owner.pk
      grinder.transform projects
    end

    # you can even redefine this if you like
    helper_method def owner
      @owner ||= project.owner
    end

    helper_method def owners
      @owners ||= roller.dataset.grind filter
    end

    private

    # this does all the normal rails action method stuff
    def handle
      filter.extract!{|k,v| k == :year || k == :budget_gte}
    end

    def grinder
      @grinder ||= Grinder.new filter
    end
  end
end

So in a nutshell, you define the action method in the controller using the handle :owner call, which does two things (What!? A method that does TWO things!?!? EEEeeeek. Run awaaaaay…):

  1. defines the owner method so that rails will have a method to call and a default view to render
  2. when the action method is called, include the defined methods in the current instance of the controller.

So now, in the view, you can refer to project and owner instead of @project and @owner. Yes, the accessor for @owner will overwrite the owner action method. Eww and Aahh and uuuuh! Well whaddya expect when you have to work around framework stinkiness!?

In short, a way to get rid of name clashes in the controller, get rid of @ivars in views, and use that wacky inheriting from Module thing. Not bad, eh?

And it would be easy to iterate through all public methods defined in the handler block and add them as helper_methods. Making them automatically available in the view.

Comments