BUILDING A SOCIAL NETWORKING
WEBSITE WITH RUBY ON RAILS

Reopening classes to improve the Session interface

Posted 3 months ago by Raul Parolari

Chapter 7 of RailsSpace refactors the management of the session login-logout feature, presenting 3 methods:

# Method calls:                    # Object:                 
user.login!(session)               # User instance
User.logout!(session, cookies)     # User class
logged_in?                         # controller

This significantly improved the initial version which disseminated in the code accesses to the session internal data structures (although there are still direct accesses to read the user_id, that we will tackle later).

However, there is something a bit odd: the object on which the methods act changes each time: it is an User instance to log in the user, then the User class to log him out, and finally it is the controller (‘self’ is the controller) to verify the log in. Could we have an uniform way to invoke those 3 actions?

Looking inside those methods, it is inmediate to realize that the real object on which the methods act is not user, but session, which belongs to the class CGI::Session (in my machine, file /usr…ruby/1.8/cgi/session.rb). If we only could add methods in there!

But wait: this is Ruby; we can always reopen a class and add functionality! very much like the book did when it reopened the class String to add the beautiful method or_else? (ch 9). Comforted by that thought, we created a file with our session extension in the lib directory (that we will ‘require’ at the top of the application.rb file):

file lib/session_ext
class CGI::Session

  def login!(user)
    self[:user_id] = user.id
  end

  def logout!
    self[:user_id] = nil
  end

  def logged_in?
    not self[:user_id].nil?
  end

end  # CGI::Session

The content of the methods is identical as before (aside from a detail in the logout action regarding the deletion of the long term cookie, that we will perform now in the controller), and the calls are now:

# In class User Controller
  # in login and register actions
  session.login!(@user)       # old: @user.login!(session)

  # in logout action 
  session.logout!             # old: User.logout!(session, ..)
  cookies.delete(:authorization_token)

# In class ApplicationController
  unless session.logged_in?   # old: unless logged_in?
end

[A curious detail: observe how the place of the terms session,login,user exactly reverse in the login! call; of course, as the ‘actor’ is now the session].

The same modifications have to be done in the tests. The only place where I left ‘logged_in?’ is in the layout, as it is friendlier in that context (in the application helper, I made that method call session.logged_in?). Finally, I deleted the methods dealing with session from the User model.

We can at this point observe that it would be nice to also centralize all those accesses to the session hash (like ‘User.find(session[:user_id]’) to retrieve the user_id without exposing the session hash. We can of course achieve that adding a method ‘user_id’ to our Session extension.
But, at this point, observing that the ‘login!’ and ‘logout!’ methods are nothing else than getters/setters on the session, we become tempted to simplify things even further:

class Session_Invalid_UserId < Exception; end

class CGI::Session

 # returns current user id in session
 def user_id
   self[:user_id]
 end

 # sets session to given user (or nil)
 def user_id=(value)
   self[:user_id] = 
     case value
     when Fixnum: value
     when User:   value.id
     when nil:    value  
     else
       raise Session_Invalid_UserId, "invalid user_id #{value}"
     end
 end 

 # returns true if there is a session user 
 def user_id?
   self[:user_id] != nil
 end

end  # CGI::Session

The 3 invocations (session.user_id, session.user_id = @user, session.user_id?) can replace not only the calls login!, logout!, logged_in? but also the accesses to retrieve the user id. Notice also that by doing this we can now detect an error when setting the user_id to something else than a number, an user instance, or nil (for logout).

After having replaced all the strings in the app and test files (using global replaces), we reopened our browser and everything worked ok! Alas, the happiness was short-lived, as the attempt to run the test suite failed!

We discovered that the tests do not use the CGI::Session but rather a simulation of it, called ActionController::TestSession, that it is just an access to a hash called ‘data’. We could then repair the problem, by simply adding in our session_ext file an extension to that class:

class ActionController::TestSession

 def user_id; data[:user_id]; end
 def user_id=(value); data[:user_id] = value; end
 def user_id?; data[:user_id] != nil; end

end

And the test suite ran.. fine (phew!).

Perhaps all the above work seems an unnecessary trouble (disclosure: we certainly shared that thought, when the tests failed miserably and we had not idea why!). However, in large projects it is often useful at a certain point to organize all functionalities in the most apt way (as, for some reason, entropy, or what we programmers call ‘disorder/noise’, has a subtle way to take off exponentially in software systems).

Using Ruby, this means identifying the Classes and Objects around which to coalesce functionalities.
What this exercise tried to demonstrate is that even when those classes are not under our control (like the unfamiliar CGI::Session and the unexpected TestSession), Ruby offers us the possibility to reopen those classes and extend their capabilities in the best way that fits our design.

Comments

There is 1 comment on this post. Post yours →

posted 3 months ago

This is nice. I also recommend looking at the restful_authentication plugin. I’ve used it on a couple sites and it works well.


Post a comment

Required fields in bold.