views:

235

answers:

5

In a current project I need to support finding a User by login credentials and also by email address. I know that in RESTful design you use a GET to find resources. In Rails...

GET /users    # => UsersController.index -- find all the users

GET /users/1  # => UsersController.show -- find a particular user

But I also need something akin to...

GET /users?username=joe&password=mysterio

GET /[email protected]

Is it conventional to add additional routes and actions beyond index and show?

Or is it more common to put conditional logic in the show action to look at the params and detect whether we're finding by one thing or another?

There's a similar issue with PUT requests. In one case I need to set a User to be "active" (user.active = true), and in another case I just need to do a general form-based editing operation.

Thanks guys. Eventually I'm going to figure out this REST stuff.

+2  A: 

The first thing you can do is make your GETs as smart as possible. In your example, this can be handled programmatically. The argument can be processed this way:

  • Is a number? It's a userid;
  • Has a @ in it? It's an email;
  • Otherwise? It's a username.

But I assume that you're not just talking about this example and want something to handle the general case rather than just this specific one.

There are basically two ways of dealing with this:

  1. Add extra path information eg /users/email/[email protected], /users/name/cletus; or
  2. Be more specific in your "top-level" URL eg /user-by-email/[email protected], /user-by-name/cletus.

I would handle it programmatically if you can.

cletus
With the default map.resources the email addy will get the .com parsed off of it as the :format.
Otto
+1  A: 

You might be able to set up different routes for different tasks. So for this case you could have one route to a method in UserControll dedecated to getting a user by email, and another for getting the information by credentials.

geowa4
+2  A: 

I don't know how much of this is convention, but this is what I would do. I would add another action, as long as it's specifically related to that resource. In your example, show is a find by userid, so it makes sense as another action on UsersController. You can turn it into a sentence that makes sense, "get me the user with this email address"

For the other one, GET /users?username=joe&password=mysterio, I would do that as another resource. I assume you're thinking that action would log in the user if the password were correct. The verb GET doesn't make sense in that context.

You probably want a 'session' resource (BTW, this is how restful_auth works). So you would say "create me a session for this user", or something like POST /sessions where the body of the post is the username & password for the user. This also has the good side effect of not saving the password in the history or letting someone capture it on the HTTP proxy.

So your controller code would look something like this:

class UsersController < ActionController::Base

    def show
      @user = User.find_by_id(params[:id])
      # etc ...
    end

    def show_by_email
      @user = User.find_by_email(params[:email)
    end
end

class SessionsController < ActionController::Base
  def create
     # ... validate user credentials, set a cookie or somehow track that the 
     # user is logged in to be able to authenticate in other controllers
  end
end

You would set up your routes like this:

map.connect "/users/byemail", :controller => "users", :action => "show_by_email", :conditions => { :method => :get }
map.resources :users
map.resources :sessions

That will get you URLs like /users/[email protected]. There are issues with encoding the email directly in the URL path, rails sees the '.com' at the end and by default translates that into the :format. There's probably a way around it, but this is what I had working.

Also like cletus says, there are ways to make your route match based on the format of the parts of the URL, like all numbers or alphanumeric, but I don't know off hand how to make that work with the dots in the url.

Otto
+1  A: 

Regarding the "ByEmail" request, have you considered creating a new email resource.

GET /email/foo_at_bar_dot_com

The response could contain a link to the related user.

I see so many people trying to apply RESTful design principles to their URL structure and then mapping those urls to procedural handler code. e.g. GET = Show, or is it GET = Index or ShowByEmail. By doing this you are really just pretending to do a RESTful design and then trying to create a mapping between a resource oriented URL space and procedurally oriented implementation. That is really hard to do and the procedural nature keeps leaking out into the URLs.

Resource oriented design often requires a very different way of thinking about problems that we are used to and unfortunately many of the frameworks out there keep sucking us back into the RPC model.

Darrel Miller
+2  A: 

I'm new to SO, so I can't comment, but the checked green answer is not RESTful.

In a RESTful world, your controller grabs all the parameters and passes it to the model layer for processing. Typically, you shouldn't create another action.

Instead, you should do do something like this:

def show
  @user = User.find_by_login_or_email(params[:user])
  ... #rest of your action
end

Your model can have a method like this:

class User
  self.find_by_login_or_email(params)
    return find_by_login(params[:login]) unless params[:login].blank?
    return find_by_email(params[:email]) unless params[:email].blank?
    nil #both were blank
  end
end

Your view could look like this:

<%= f.text_field :user, :email %>

or

<%= f.text_field :user, :login %>

Note: untested code, so may be buggy...but the general line of thinking is usually not to create new actions for every one-off rule. Instead, look to see if you can push the logic into the models. If your controllers start to have too many non-standard actions, then it may be time to re-evaluate your domain modeling, and perhaps it's refactor the actions to some new models.

ps: you should never pass in passwords via a GET like that

Chris Lee
Roy Fielding's dissertation on REST says absolutely nothing about controllers or models. The RESTful architectural style does not attempt to define about how you go about generating the representations.
Darrel Miller
You're right. What I'm saying is more in line with maintaining the clarity of Rails' CRUD actions and the separation of concern in an MVC framework.
Chris Lee
Ok writing some new code and coming across this exact thing myself.You're absolutely right, that knowledge of "how do I look up a 'thing' with x, y, z information" completely belongs in the model.
Otto
How would you make the case for still having a #search action, though? I'm sure there is a good one, but I'm not really sure what the distinction is. "different method on the model" doesn't cut it for me.
Otto