How best to refactor action with too many conditionals? - ruby-on-rails

A colleague and I are working on a RoR project which receives text messages from the Twilio messaging service and then parses them to perform commands. To interface with Twilio, we have a controller action that Twilio POSTs to with a few parameters, like to, from, and message body.
The problem is that we have a lot of possible commands that the user can perform by texting in. Eventually, our code was starting to look like this:
# message_controller.rb
def process_message
words = params[:Body].upcase.split
if words[0] == 'FOO'
do_something()
render 'foo'
elsif words[0] == 'BAR' && params[:From] == 'some number'
.
. # 10 to 15 lines
.
else
render 'command_not_found'
end
end
We decided to refactor using the Interpreter pattern and parse the message body sort of like a language. We ended up with something like this in the controller:
# message_controller.rb
def process_message
interpreter = InitialInterpreter.new(message_params)
while !interpreter.is_terminal? do
interpreter = interpreter.next_interpreter()
end
render interpreter.template
end
def message_params
params.permit(:To, :From, :Body)
end
And we created models like this:
# initial_interpreter.rb, a non-terminal interpreter
attr_accessible :To, :From, :Body
def next_interpreter
if words[0] == 'FOO'
FooInterpreter.new
elsif
.
. # a couple of non-terminal interpreters
.
else
CommandNotFoundInterpreter.new
end
end
# command_not_found_interpreter.rb, a terminal interpreter
def is_terminal?
true
end
def template
'command_not_found'
end
But then we started thinking about it and realized that almost all our commands have no more than two expressions, so we didn't really need the while loop and refactored like this:
# message_controller.rb
def process_message
interpreter = InitialInterpreter.new(message_params)
render interpreter.template
end
# initial_interpreter.rb
def template
if words[0] == 'FOO'
interpreter = FooInterpreter.new
elsif
.
. # a couple of non-terminal interpreters
.
else
interpreter = CommandNotFoundInterpreter.new
end
interpreter.template
end
# command_not_found_interpreter.rb
def template
'command_not_found'
end
But this seems a bit too simple. Now, it's just classes wrapping method. Are we overthinking this? Does this pattern seem good or bad? I feel like we're doing something wrong, but I can't put my finger on it. What would be best way of dealing with this sort of case?
EDIT
I also should have mention that I'm wondering about efficiency too. This solution we're using doesn't seem very efficient, and this action may be receiving a lot of traffic when we go live. I wonder if there's anything rails or ruby specific that might provide a good solution.

In my opinion, your pattern seems fine. I've done something similar in the past and haven't had any bad experiences with it. If you really have a ton of cases, maybe the observer pattern would be okay here. Your InitialInterpreter can accept the message and dispatch it to all of it's observers (which would be your other interpreters). Each interpreter can choose to do something with the message or not based on regular expressions or string matching or whatever you choose.
In this pattern, interpreters contain the logic of when to interpret inside of themselves. The InitialInterpreter and your controller won't care what interpreter picks up the message as long as it's subscribed to the InitialInterpreter's events.
Another benefit to the observer pattern is that it allows you to easily add multiple interpreters for a single command if you need to do so in the future. Ruby has built in support for observers via the Observable module. I believe Rails has something for it as well but I've never personally used it.

Related

What is the use of ! in rails

What is the use of ! in rails?
Especially in this line: From HArtl tutorial
users = User.order(:created_at).take(6)
50.times do
content = Faker::Lorem.sentence(5)
user.each { |user| user.microposts.create!( content: content )}
end
Basically this is creating tweets/microposts for 6 users.
I am really wondering why need to use !
The important thing to remember is that in Ruby a trailing ! or ? are allowed on method names and become part of the method name, not a modifier added on. x and x! and x? are three completely different methods.
In Ruby the convention is to add ! to methods that make in-place modifications, that is they modify the object in fundamental ways. An example of this is String#gsub which returns a copy, and String#gsub! which modifies the string in-place.
In Rails this has been ported over to mean that as well as situations where the method will raise an exception on failure instead of returning nil. This is best illustrated here:
Record.find_by(id: 10) # => Can return nil if not found
Record.find_by!(id: 10) # => Can raise ActiveRecord::RecordNotFound
Note that this is not always the case, as methods like find will raise exceptions even without the !. It's purely an informational component built into the method name and does not guarantee that it will or won't raise exceptions.
Update:
The reason for using exceptions is to make flow-control easier. If you're constantly testing for nil, you end up with highly paranoid code that looks like this:
def update
if (user.save)
if (purchase.save)
if (email.sent?)
redirect_to(success_path)
else
render(template: 'invalid_email')
end
else
render(template: 'edit')
end
else
render(template: 'edit')
end
end
In other words, you always need to be looking over your shoulder to be sure nothing bad is happening. With exceptions it looks like this:
def update
user.save!
purchase.save!
email.send!
redirect_to(success_path)
rescue ActiveRecord::RecordNotFound
render(template: 'edit')
rescue SomeMailer::EmailNotSent
render(template: 'invalid_email')
end
Where you can see the flow is a lot easier to understand. It describes "exceptional situations" as being less likely to occur so they don't clutter up the main code.

event trigger system design in rails

i'm on the way of redesigning my activity feed, i already implemented the logic with redis and rails (wich works great by the way) but i'm still unsure how to create/trigger the events.
In my first approach i used observer, which had the downside of not having current_user available. and, using observer is a bad idea anyways :)
My preferred method would be to create/trigger the events in the controller, which should look sth like:
class UserController < LocationController
def invite
...
if user.save
trigger! UserInvitedEvent, {creator: current_user, ...}, :create
....
end
end
end
The trigger method should
create the UserInvitedEvent with some params. (:create can be default option)
could be deactivate (e.g. deactivate for testing)
could be executed with e.g. resque
i looked in some gems (fnordmetrics, ...) but i could not find a slick implementation for that.
I'd build something like the following:
# config/initializers/event_tracking.rb
modlue EventTracking
attr_accessor :enabled
def enable
#enabled = true
end
def disable
#enabled = false
end
module_function
def Track(event, options)
if EventTracking.enabled
event.classify.constantize.new(options)
end
end
end
include EventTracking
EventTracking.enable unless Rails.env.test?
The module_function hack let's us have the Track() function globally, and exports it to the global namespace, you (key thing is that the method is copied to the global scope, so it's effectively global, read more here: http://www.ruby-doc.org/core-1.9.3/Module.html#method-i-module_function)
Then we enable tracking for all modes except production, we call event.classify.constantize in Rails that should turn something like :user_invited_event into UserInvitedEvent, and offers the possibility of namespacing, for example Track(:'users/invited'). The semantics of this are defined by ActiveSupport's inflection module.
I think that should be a decent start to your tracking code I've been using that in a project with a lot of success until now!
With the (new) rails intrumentation and ActiveSupport::Notifications system you can completely decouple the notification and the actual feed construction.
See http://railscasts.com/episodes/249-notifications-in-rails-3?view=asciicast

Testing methods called on yielded object

I have the following controller test case:
def test_showplain
Cleaner.expect(:parse).with(#somecontent)
Cleaner.any_instance.stubs(:plainversion).returns(#returnvalue)
post :showplain, {:content => #somecontent}
end
This works fine, except that I want the "stubs(:plainversion)" to be an "expects(:plainversion)".
Here's the controller code:
def showplain
Cleaner.parse(params[:content]) do | cleaner |
#output = cleaner.plainversion
end
end
And the Cleaner is simply:
class Cleaner
### other code and methods ###
def self.parse(#content)
cleaner = Cleaner.new(#content)
yield cleaner
cleaner.close
end
def plainversion
### operate on #content and return ###
end
end
Again, I can't figure out how to reliably test the "cleaner" that is made available from the "parse" method. Any suggestions?
This is a little tricky. The easiest a approach will be to break the problem into two pieces: the testing of the controller and the testing of the controller.
You have the testing of the controller set-- just remove your expectation around plainversion call.
Then, separately, you want to test the Cleaner.parse method.
cleaner = Cleaner.new('x');
cleaner.expects :close
Cleaner.expects(:new).returns(cleaner)
called_with = nil
Cleaner.parse('test') do |p|
called_with = p
end
assert_equal p, cleaner
That is not very clear what's going on. Makes me think that there is a simpler version of this. Can cleaner just be a simple function that takes a string and returns another one? Skip all the yielding and variable scoping? That will be much easier to test.
You might find the documentation for Mocha::Expectation#yields useful.
I've made an attempt at showing how you might do what you want in this gist. Note that I had to tweak the code a bit to get it into a self-contained runnable test.

Is there a method I can use across controllers and if so, how do I use it?

I have several controllers that take an instance of different classes each (Email, Call, Letter, etc) and they all have to go through this same substitution:
#email.message.gsub!("{FirstName}", #contact.first_name)
#email.message.gsub!("{Company}", #contact.company_name)
#email.message.gsub!("{Colleagues}", #colleagues.to_sentence)
#email.message.gsub!("{NextWeek}", (Date.today + 7.days).strftime("%A, %B %d"))
#email.message.gsub!("{ContactTitle}", #contact.title )
So, for example, #call.message for Call, #letter.message for Letter, etcetera.
This isn't very dry. I tried the following:
class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
def message_sub(asset, contact, colleagues)
asset.message.gsub!("{FirstName}", contact.first_name)
asset.message.gsub!("{Company}", contact.company_name)
asset.message.gsub!("{Colleagues}", colleagues.to_sentence)
asset.message.gsub!("{NextWeek}", (Date.today + 7.days).strftime("%A, %B %d"))
asset.message.gsub!("{ContactTitle}", contact.title )
end
end
So in, say, the Letter Controller have this:
#letter = Letter.find(params[:letter]) #:letter was passed as a hash of the letter instance
message_sub(#letter, #contact, #colleagues)
#contact_letter.body = #letter.body
But the above doesn't work.
To answer your question directly, you want a module. You can put it in your lib directory.
module MessageSubstitution
def message_sub(asset, contact, colleagues)
asset.message.gsub!("{FirstName}", contact.first_name)
asset.message.gsub!("{Company}", contact.company_name)
asset.message.gsub!("{Colleagues}", colleagues.to_sentence)
asset.message.gsub!("{NextWeek}", (Date.today + 7.days).strftime("%A, %B %d"))
asset.message.gsub!("{ContactTitle}", contact.title )
end
end
Then in your controller
class MyController < ApplicationController
include MessageSubstitution
def action
...
message_sub(#letter, #contact, #colleagues)
end
end
However, as some have already pointed out, this is probably better at the model level:
module MessageSubstitution
def substituted_message(contact, colleagues)
message.gsub("{FirstName}", contact.first_name).
gsub("{Company}", contact.company_name).
gsub("{Colleagues}", colleagues.to_sentence).
gsub("{NextWeek}", (Date.today + 7.days).strftime("%A, %B %d")).
gsub("{ContactTitle}", contact.title )
end
end
Which you would then include at the model level, and then call in your controller like #letter.substituted_message(#contact, #colleagues)
However, I'm a little confused that what you posted didn't work, though. If it's defined on ApplicationController it should work. Module-based solution still better IMO.
I think we are missing information needed to fix this problem. Check out section 3 of this guide: Debugging Rails Applications
Step through your code and check the variables at each point to see if they are what you expect.
When you have time, be sure to read the whole guide. It provides an invaluable set of tools for Rails programming.
As a side note, the code you have provided feels like it is more suited to be in the model than in the controller. Your models may be a good candidate for Single Table Inheritance or your models are so much alike that you can condense them into a more generalized model that covers what you need. If either of these options are a good fit, you could move message_sub out of the controller and into the model.
The problem may lie in the fact that gsub! works on the instance of the String object returned by ActiveRecord. To notify ActiveRecord of an attribute change, you should call the method message=(value). So, your method would be like this:
def message_sub(asset, contact, colleagues)
asset.message = asset.message.
gsub("{FirstName}", contact.first_name).
gsub("{Company}", contact.company_name).
gsub("{Colleagues}", colleagues.to_sentence).
gsub("{NextWeek}", (Date.today + 7.days).strftime("%A, %B %d")).
gsub("{ContactTitle}", contact.title )
end
(I took some artistic license to chain the gsub method, but it's all in your tastes)
It's not clear if the question is about reusing a method across the controller (as the title suggests) or the best way to do message templates.
To reuse the method across the controller , I guess you already answer the question yourself by using a common class. However as Awgy suggested this bit could better be in the Model than in the Controller.
To template message you can use ERB rather gsub or just 'eval' your string
(if you use #{} instead of {} in your message)
firstName=contact.first_name
company = company_name
...
asset.message = eval "\"#{asset.message}\""
(You might even be able to pass directly contact as a 'bindings' to the eval function.
Personally, I wouldn't delegate this to the controller, isn't this something that the Presenter Pattern is for?

What's the correct way to run one controller action from another controller action without an HTTP redirect?

I'd like to be able to dispatch from one controller action to another conditionally, based on a combination of query parameters and data in the database.
What I have right now is something like:
class OldController < ApplicationController
def old_controller_action
if should_use_new_controller
new_params = params.dup
new_params[:controller] = "new_controller_action"
redirect_to new_params
return
end
# rest of old and busted
end
end
class NewController < ApplicationController
def new_controller_action
# new hotness
end
end
This works just fine, but it issues an HTTP redirect, which is slow. I'd like to be able to do this same thing, but within the same HTTP request.
Is there a clean way to do this?
Edit: The bounty will go to someone who can show me a clean way to do this that leaves the controllers and their actions relatively untouched (other than the redirect code itself).
Instead of calling code across actions, extract the code to lib/ or something, and call that code from both controllers.
# lib/foo.rb
module Foo
def self.bar
# ...
end
end
# posts_controller
def index
Foo.bar
end
# things_controller
def index
Foo.bar
end
Create an instance of the controller class:
#my_other_controller = MyOtherController.new
Then call methods on it:
#my_other_controller.some_method(params[:id])
I prefer the module idea, but this should do the trick.
You can also pass parameters as a whole from another controller:
#my_other_controller.params = params
I suspect you want option 3, but lets go through the some alternatives first
Option 1 - Push the controller selection logic into a helper that inserts the right link into your view. Benifits - controllers remain clean, Cons - if decision logic depending on submitted values this approach won't work. If URL is being called by external websites then this won't work.
Option 2 - Push the logic back into your model. Pro's - keeps controller clean. Cons - doesn't work well if you've got lots of sesson, params or render / redirect_to interaction.
Option 3 - Stay within the same controller. I suspect you are trying to replace some existing functionality with some new functionality, but only in some cases. Pro's - Simple and have access to everything you need. Cons - only works if it makes sense to use the same controller i.e. you're working with the same entity such as user, place or company.
Lets look an an example for option 3. My links controller has totally diferent behavour for admins than other users ...
class LinksController < ApplicationController
#...
def new
#Check params and db values to make a choice here
admin? ? new_admin : new_user
end
#...
private
def new_admin
#All of the good stuff - can use params, flash, etc
render :action => 'new_admin'
end
def new_user
#All of the good stuff - can use params, flash, etc
render :action => 'new_user'
end
end
If two controllers are trying to do the same thing, there's a very good chance this should be in a model. Take a good look at your design and -- I'm sorry I don't know your experience level with MVC -- read up on thin controller techniques:
http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model
http://www.robbyonrails.com/articles/2007/06/19/put-your-controllers-on-a-diet-already
http://andrzejonsoftware.blogspot.com/2008/07/mvc-how-to-write-controllers.html
If the problem is that you need the other controller to do the render, then maybe the route should have pointed there to begin with, and still the skinny controller technique should save the day.
If extracting the common code between controllers into a module doesn't work for you, I would use Rack middleware. I haven't seen code that uses ActiveRecord within middleware but I don't know of any reason why it shouldn't be possible since people have used Redis and the like.
Otherwise I think your only option would be to restart processing of the request with something like (untested, pseudo example):
env['REQUEST_URI'] = new_controller_uri_with_your_params
call(env)
This is similar to how integration tests are implemented. But I don't know if everything from call until you hit a controller is idempotent and safe to rerun like this. You could trace through the source and see. But even if it's ok now, it might break in any future version of rails or rack.
Using middleware would avoid this by letting you intercept the request before it's been run. You should still be able to share code with your rails application by extracting it out into common modules included in both places.
Honestly I think just doing the simple thing of factoring the common controller code is likely cleaner, but it's hard to know without the details of your situation so I thought I'd go ahead and suggest this.
Do this:
class OldController < ApplicationController
def old_controller_action
if should_use_new_controller
new_controller_action
end
# rest of old and busted
end
end
and the new controller
class NewController < OldController
def new_controller_action
# new hotness
end
end

Resources