Sunday, April 28, 2013

Controllers ... again

I realized there was a problem with the controllers the other day, and I've been trying to figure out a way of fixing it. Technically, I have two very similar problems.

First is that users would probably like to be able to control which methods in a module are actions. Currently, if a function exists in a module, then it gets applied to the request and run. I could use the private / public status to control which methods are visible, but it's not terribly elegant.

The other problem is that there's no way to restrict HTTP verbs for an action. Currently only GET is implemented anyway, but that's ratcheting ever higher on my list of things to fix. Once that happens, I'll have to figure out some way of being able to restrict an action to a specific verb.

Interesting side question: does it make any sense to be able to set multiple verbs on an action? I suppose that makes sense from a REST type perspective - you'd one to decide what action to take based on what verb was passed in. Okay, so...  multiple verbs.

There are quite a few different ways to approach this. First, you could just add a constant to the module that contains a list of actions. The framework would only call functions that are in that list.

That is what's known as "ugly as hell".

Plan B: define a DSL like Sinatra does:

module MyApp
  module MyController
    actionize!

    get 'foo' do
      # controller action
    end
  end
end

The actionize! call is there for a very specific reason. Since we don't have a base class to derive from, we have to hack our way in from Ruby's Module class. Adding 'get' and 'post' functions to every module would be a bad and possibly hazardous thing to do, so we add a single unique method that adds the required functions to this specific module instance.

So that works, it looks pretty clean, and it has the added benefit of being a pattern that many people use and like. (i.e. Sinatra)  It's also extensible, in that we can add options after the action name, the same way that Sinatra does. It also handles multiple verbs on an action cleanly, since you're not actually defining a function per say.

The downside is that you have to understand the paradigm in order to build a controller. It's built around the language.

Plan C - The last way I thought do to it was more annotations-based:

module MyApp
  module MyController
    actionize!

    action :verb => POST
    def foo
      # controller action
    end
  end
end

What I like about this plan is that it's using generic Ruby methods. We could default the verb to GET and much of the boilerplate goes away. The options we could pass in for plan B could also be passed in here. We could do interesting things like look at the function parameters and pull those parameters out of the params hash - syntactic sugar, but nice to have. It seems like some fun things could be done here to make these actions easier to work with.

One downside is that since it's a ruby method we can't really overload it. We could pass an array of verbs in to the action, but now we'd have to put a big ugly switch inside the function to handle each verb. It's also an extra line that is not grammatically attached to the function definition - it could get lost or deleted and the code would still parse.

I think the Sinatra DSL plan is probably the best plan. It's simpler and easier to see what is being built. I'll keep thinking on it, but that is probably the route I'll end up taking.

No comments:

Post a Comment